fix: KB Accelerator tree builder, approve all, canvas delete button
- Fix _build_troubleshooting_tree() to handle deep nesting, warning nodes, and DAG deduplication (placed set prevents duplicate IDs) - Fix step_sync VARCHAR(255) overflow on publish by truncating title - Add "Approve All" button to KB review screen - Add delete button (hover-reveal) to flow canvas nodes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -608,53 +608,81 @@ async def delete_import(
|
||||
|
||||
|
||||
def _build_troubleshooting_tree(nodes: list[KBImportNode]) -> dict:
|
||||
"""Build a troubleshooting tree_structure from import nodes."""
|
||||
"""Build a troubleshooting tree_structure from import nodes.
|
||||
|
||||
The tree editor expects a deeply nested structure where each decision
|
||||
node's `children` array contains all reachable descendant nodes.
|
||||
Action/solution nodes use `title`/`description` (not `question`).
|
||||
|
||||
The AI generates a DAG (shared nodes reachable from multiple paths),
|
||||
but the tree editor requires unique IDs — each node can only appear
|
||||
once. We embed each node the first time it's encountered; subsequent
|
||||
references just use next_node_id / options[].next_node_id to point
|
||||
back to the already-embedded node.
|
||||
"""
|
||||
if not nodes:
|
||||
return {"id": "root", "type": "decision", "question": "Empty", "children": []}
|
||||
|
||||
# Map original IDs to proper tree node structure
|
||||
# Map original IDs to import nodes
|
||||
original_id_map: dict[str, KBImportNode] = {}
|
||||
for node in nodes:
|
||||
orig_id = node.content.get("original_id", str(node.id))
|
||||
original_id_map[orig_id] = node
|
||||
|
||||
def _build_node(import_node: KBImportNode) -> dict:
|
||||
# Track which nodes have been placed in the tree to avoid duplicates
|
||||
placed: set[str] = set()
|
||||
|
||||
def _build_node(import_node: KBImportNode) -> dict | None:
|
||||
content = import_node.content
|
||||
node_type = import_node.node_type
|
||||
node_id = content.get("original_id", str(import_node.id))
|
||||
|
||||
# Already placed in the tree — don't create a duplicate.
|
||||
# The reference (next_node_id / options) is sufficient.
|
||||
if node_id in placed:
|
||||
return None
|
||||
placed.add(node_id)
|
||||
|
||||
question_text = content.get("question", "")
|
||||
|
||||
if node_type == "resolution":
|
||||
return {
|
||||
"id": content.get("original_id", str(import_node.id)),
|
||||
"id": node_id,
|
||||
"type": "solution",
|
||||
"question": content.get("question", ""),
|
||||
"children": [],
|
||||
"title": question_text,
|
||||
"description": content.get("description", ""),
|
||||
}
|
||||
|
||||
if node_type == "action":
|
||||
result = {
|
||||
"id": content.get("original_id", str(import_node.id)),
|
||||
if node_type in ("action", "warning"):
|
||||
result: dict = {
|
||||
"id": node_id,
|
||||
"type": "action",
|
||||
"question": content.get("question", ""),
|
||||
"children": [],
|
||||
"title": question_text,
|
||||
"description": content.get("description", ""),
|
||||
}
|
||||
next_id = content.get("next_node_id")
|
||||
if next_id and next_id in original_id_map:
|
||||
result["next_node_id"] = next_id
|
||||
return result
|
||||
|
||||
# question/decision type
|
||||
# question/decision type — recursively build children
|
||||
options = content.get("options", [])
|
||||
children = []
|
||||
for opt in options:
|
||||
next_id = opt.get("next_node_id")
|
||||
if next_id and next_id in original_id_map:
|
||||
child_node = _build_node(original_id_map[next_id])
|
||||
children.append(child_node)
|
||||
if child_node is not None:
|
||||
children.append(child_node)
|
||||
# If the child is an action with a next_node_id, also
|
||||
# build that target as a sibling (the tree editor
|
||||
# expects reachable nodes nested under the decision)
|
||||
_collect_action_chain(child_node, children)
|
||||
|
||||
return {
|
||||
"id": content.get("original_id", str(import_node.id)),
|
||||
"id": node_id,
|
||||
"type": "decision",
|
||||
"question": content.get("question", ""),
|
||||
"question": question_text,
|
||||
"options": [
|
||||
{"label": opt.get("label", ""), "next_node_id": opt.get("next_node_id", "")}
|
||||
for opt in options
|
||||
@@ -662,8 +690,26 @@ def _build_troubleshooting_tree(nodes: list[KBImportNode]) -> dict:
|
||||
"children": children,
|
||||
}
|
||||
|
||||
def _collect_action_chain(node: dict, siblings: list[dict]) -> None:
|
||||
"""Follow action node next_node_id chains and add targets as siblings."""
|
||||
if node.get("type") != "action":
|
||||
return
|
||||
next_id = node.get("next_node_id")
|
||||
if not next_id or next_id not in original_id_map:
|
||||
return
|
||||
# Don't add if already in this siblings list or already placed
|
||||
if any(s["id"] == next_id for s in siblings):
|
||||
return
|
||||
target = _build_node(original_id_map[next_id])
|
||||
if target is None:
|
||||
return
|
||||
siblings.append(target)
|
||||
# Continue chain if the target is also an action
|
||||
_collect_action_chain(target, siblings)
|
||||
|
||||
root_node = nodes[0]
|
||||
return _build_node(root_node)
|
||||
result = _build_node(root_node)
|
||||
return result or {"id": "root", "type": "decision", "question": "Empty", "children": []}
|
||||
|
||||
|
||||
def _build_procedural_tree(nodes: list[KBImportNode]) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user