fix: KB tree builder demotes decisions with < 2 branches to actions
Decision nodes with fewer than 2 buildable child targets now get demoted to action nodes instead of creating invalid tree structures. Also adds id fields to option objects (required by tree editor). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -667,26 +667,60 @@ def _build_troubleshooting_tree(nodes: list[KBImportNode]) -> dict:
|
|||||||
|
|
||||||
# question/decision type — recursively build children
|
# question/decision type — recursively build children
|
||||||
options = content.get("options", [])
|
options = content.get("options", [])
|
||||||
children = []
|
|
||||||
|
# Count how many options point to buildable (not-yet-placed) targets
|
||||||
|
buildable_targets = []
|
||||||
for opt in options:
|
for opt in options:
|
||||||
next_id = opt.get("next_node_id")
|
next_id = opt.get("next_node_id")
|
||||||
|
if next_id and next_id in original_id_map and next_id not in placed:
|
||||||
|
buildable_targets.append(next_id)
|
||||||
|
|
||||||
|
# Decision nodes MUST have at least 2 branches to pass validation.
|
||||||
|
# If fewer than 2 buildable targets, demote to action node.
|
||||||
|
if len(buildable_targets) < 2:
|
||||||
|
demoted: dict = {
|
||||||
|
"id": node_id,
|
||||||
|
"type": "action",
|
||||||
|
"title": question_text,
|
||||||
|
"description": content.get("description", ""),
|
||||||
|
}
|
||||||
|
if buildable_targets:
|
||||||
|
demoted["next_node_id"] = buildable_targets[0]
|
||||||
|
elif options:
|
||||||
|
# All targets already placed; reference first option's target
|
||||||
|
first_next = options[0].get("next_node_id")
|
||||||
|
if first_next:
|
||||||
|
demoted["next_node_id"] = first_next
|
||||||
|
return demoted
|
||||||
|
|
||||||
|
# Build children for decision node
|
||||||
|
children = []
|
||||||
|
built_options = []
|
||||||
|
for opt in options:
|
||||||
|
next_id = opt.get("next_node_id")
|
||||||
|
opt_id = opt.get("id", f"opt-{node_id}-{len(built_options)}")
|
||||||
if next_id and next_id in original_id_map:
|
if next_id and next_id in original_id_map:
|
||||||
child_node = _build_node(original_id_map[next_id])
|
child_node = _build_node(original_id_map[next_id])
|
||||||
if child_node is not None:
|
if child_node is not None:
|
||||||
children.append(child_node)
|
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)
|
_collect_action_chain(child_node, children)
|
||||||
|
built_options.append({
|
||||||
|
"id": opt_id,
|
||||||
|
"label": opt.get("label", ""),
|
||||||
|
"next_node_id": next_id,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
built_options.append({
|
||||||
|
"id": opt_id,
|
||||||
|
"label": opt.get("label", ""),
|
||||||
|
"next_node_id": next_id or "",
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": node_id,
|
"id": node_id,
|
||||||
"type": "decision",
|
"type": "decision",
|
||||||
"question": question_text,
|
"question": question_text,
|
||||||
"options": [
|
"options": built_options,
|
||||||
{"label": opt.get("label", ""), "next_node_id": opt.get("next_node_id", "")}
|
|
||||||
for opt in options
|
|
||||||
],
|
|
||||||
"children": children,
|
"children": children,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user