fix: improve AI corrective prompt clarity and add global next_node_id validation

- Rewrote CORRECTIVE_PROMPT_TEMPLATE to clearly distinguish option→child
  vs action→sibling next_node_id semantics with concrete examples
- Added global check in ai_tree_validator that action next_node_ids
  actually reference existing nodes in the tree (was silently unchecked)
- Added max_tokens truncation warning to branch_detail logger
- Added test for action next_node_id referencing nonexistent node

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-23 23:45:39 -05:00
parent 6a76e61792
commit fe43f5cb46
3 changed files with 33 additions and 2 deletions

View File

@@ -97,7 +97,16 @@ CORRECTIVE_PROMPT_TEMPLATE = """Your previous JSON was invalid for ResolutionFlo
Validation errors:
{error_list}
IMPORTANT: If any error mentions a next_node_id referencing a non-existent child, you must ensure every option's next_node_id exactly matches the "id" field of one of the node's direct children. The child node must exist in the "children" array of the same parent node.
CRITICAL RULES TO FIX THESE ERRORS:
1. Decision node options → next_node_id MUST match the "id" of a direct child in that SAME decision node's "children" array.
Example: if decision node has children [A, B, C], then option next_node_id must be "A", "B", or "C".
2. Action node → next_node_id MUST match the "id" of a SIBLING node — another child of the SAME parent decision node.
Example: if parent decision has children [action-1, solution-1, solution-2], then action-1's next_node_id must be "solution-1" or "solution-2".
The next node must ALREADY EXIST in the parent's children array — do NOT nest the next node inside the action node.
3. Every referenced ID must physically exist somewhere in the tree as a node with that exact "id" value.
Return a corrected full JSON object only. No markdown, no prose, no code fences.
Fix ALL listed errors while maintaining the same troubleshooting/procedural logic."""
@@ -224,6 +233,12 @@ async def generate_branch_detail(
len(response.content),
response.usage.output_tokens,
)
if response.stop_reason == "max_tokens":
logger.warning(
"branch_detail attempt=%d hit max_tokens limit (%d output tokens) — response may be truncated",
attempt,
response.usage.output_tokens,
)
raw_text = _strip_markdown_fences(response.content[0].text) if response.content else ""
if not raw_text:
logger.warning("branch_detail attempt=%d returned empty text, stop_reason=%s", attempt, response.stop_reason)

View File

@@ -40,7 +40,8 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
# Collect all node IDs and validate structure
all_ids: set[str] = set()
all_referenced_ids: set[str] = set()
all_referenced_ids: set[str] = set() # option next_node_ids (already checked locally)
action_next_ids: set[str] = set() # action next_node_ids (checked globally below)
node_count = 0
solution_count = 0
@@ -121,6 +122,7 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
)
else:
all_referenced_ids.add(next_id)
action_next_ids.add(next_id)
elif node_type == "solution":
solution_count += 1
@@ -131,6 +133,13 @@ def validate_generated_tree(tree: dict[str, Any]) -> list[str]:
_validate_node(tree, "root")
# Check that all action next_node_ids actually exist in the tree
for ref_id in action_next_ids:
if ref_id not in all_ids:
errors.append(
f"Action next_node_id '{ref_id}' references a node that does not exist in the tree"
)
# Global checks
if node_count < 5:
errors.append(

View File

@@ -124,6 +124,13 @@ class TestReferenceIntegrity:
errors = validate_generated_tree(tree)
assert any("non-existent child" in e for e in errors)
def test_action_next_node_id_references_nonexistent_node(self):
"""Action next_node_id pointing to a node that doesn't exist anywhere in the tree."""
tree = _make_valid_tree()
tree["children"][1]["next_node_id"] = "ghost-node"
errors = validate_generated_tree(tree)
assert any("ghost-node" in e for e in errors)
def test_duplicate_option_ids(self):
tree = _make_valid_tree()
tree["options"][0]["id"] = "same"