From 00e1e701caae4868204dd0102a53b737b27ce3a3 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 11 Mar 2026 09:12:43 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20procedural=20KB=20import=20=E2=80=94=20a?= =?UTF-8?q?dd=20title=20field,=20correct=20step=20types,=20validation=20ga?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _build_procedural_tree now maps AI node types (step, action, warning) to valid procedural types (procedure_step, section_header, procedure_end) - Generates 'title' field from content text — fixes "Unnamed step" in editor - Auto-appends procedure_end step if AI didn't generate one - Adds procedural validation gate at commit endpoint (same as troubleshooting) Co-Authored-By: Claude Opus 4.6 --- backend/app/api/endpoints/kb_accelerator.py | 67 +++++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/backend/app/api/endpoints/kb_accelerator.py b/backend/app/api/endpoints/kb_accelerator.py index efb154f0..238175a6 100644 --- a/backend/app/api/endpoints/kb_accelerator.py +++ b/backend/app/api/endpoints/kb_accelerator.py @@ -28,6 +28,7 @@ from app.core.rate_limit import limiter from app.core.subscriptions import get_plan_limits from app.core.ai_quota_service import get_user_plan from app.core.ai_tree_validator import validate_generated_tree +from app.core.tree_validation import validate_procedural_structure from app.core.kb_extraction_service import extract_text from app.core.kb_conversion_service import convert_document from app.models.kb_import import KBImport, KBImportNode @@ -561,6 +562,22 @@ async def commit_import( "validation_errors": validation_errors, }, ) + else: + # Procedural/maintenance validation + is_valid, proc_errors = validate_procedural_structure(tree_structure) + if not is_valid: + error_messages = [e.get("message", str(e)) for e in proc_errors] + logger.warning( + "KB commit blocked: procedural flow failed validation with %d errors: %s", + len(proc_errors), "; ".join(error_messages[:5]), + ) + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail={ + "message": "The converted flow has structural issues that need to be fixed before committing.", + "validation_errors": error_messages, + }, + ) # Build intake_form for procedural flows intake_form = None @@ -881,17 +898,59 @@ def _demote_decision_to_action(node: dict, siblings: list[dict], index: int) -> # Delete the broken _repair_tree and replace with the working version # by removing the first broken attempt def _build_procedural_tree(nodes: list[KBImportNode]) -> dict: - """Build a procedural tree_structure from import nodes.""" + """Build a procedural tree_structure from import nodes. + + Maps AI node types to valid procedural step types: + - step/action/warning → procedure_step + - section_header → section_header + Adds a procedure_end step at the end if missing. + Each step requires 'title' (from content text) and 'content' fields. + """ + # Type mapping from AI output to valid step types + TYPE_MAP = { + "step": "procedure_step", + "action": "procedure_step", + "warning": "procedure_step", + "question": "procedure_step", + "resolution": "procedure_step", + "section_header": "section_header", + "procedure_step": "procedure_step", + "procedure_end": "procedure_end", + } + steps = [] for node in sorted(nodes, key=lambda n: n.node_order): content = node.content - step = { + raw_type = node.node_type + step_type = TYPE_MAP.get(raw_type, "procedure_step") + + step_content = content.get("content", "") + step_title = content.get("title") or content.get("question") or step_content[:80] or "Step" + + step: dict = { "id": content.get("original_id", str(node.id)), - "type": node.node_type, - "content": content.get("content", ""), + "type": step_type, + "title": step_title, + "content": step_content, } + + # Preserve content_type if present + content_type = content.get("content_type") + if content_type: + step["content_type"] = content_type + steps.append(step) + # Ensure a procedure_end exists at the end + has_end = any(s["type"] == "procedure_end" for s in steps) + if not has_end and steps: + steps.append({ + "id": "procedure-end", + "type": "procedure_end", + "title": "Procedure Complete", + "content": "All steps have been completed.", + }) + return { "id": "root", "type": "procedural",