fix: handle truncated AI responses and relax progressive tree validation
- Strip unclosed [TREE_UPDATE] and [METADATA] blocks from display when response is truncated by max_tokens - Increase send_message max_tokens from 2000 to 8000 to prevent truncation of large tree JSON - Use lightweight validation for progressive tree updates (valid root node only) instead of strict 5-node minimum — strict validation still applies at final /generate step Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,13 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]:
|
|||||||
except (json.JSONDecodeError, ValueError) as e:
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
logger.warning("Failed to parse tree update JSON: %s", e)
|
logger.warning("Failed to parse tree update JSON: %s", e)
|
||||||
result["content"] = raw_response[: tree_match.start()] + raw_response[tree_match.end() :]
|
result["content"] = raw_response[: tree_match.start()] + raw_response[tree_match.end() :]
|
||||||
|
else:
|
||||||
|
# Handle truncated response — opening tag exists but no closing tag
|
||||||
|
# (happens when max_tokens cuts off the JSON block)
|
||||||
|
truncated_match = re.search(r"\[TREE_UPDATE\][\s\S]*$", raw_response)
|
||||||
|
if truncated_match:
|
||||||
|
logger.warning("Truncated [TREE_UPDATE] block detected (no closing tag) — stripping from display")
|
||||||
|
result["content"] = raw_response[: truncated_match.start()]
|
||||||
|
|
||||||
# Extract [PHASE:name]
|
# Extract [PHASE:name]
|
||||||
phase_match = re.search(r"\[PHASE:(\w+)\]", result["content"])
|
phase_match = re.search(r"\[PHASE:(\w+)\]", result["content"])
|
||||||
@@ -205,6 +212,11 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]:
|
|||||||
except (json.JSONDecodeError, ValueError) as e:
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
logger.warning("Failed to parse metadata JSON: %s", e)
|
logger.warning("Failed to parse metadata JSON: %s", e)
|
||||||
result["content"] = result["content"][: meta_match.start()] + result["content"][meta_match.end() :]
|
result["content"] = result["content"][: meta_match.start()] + result["content"][meta_match.end() :]
|
||||||
|
else:
|
||||||
|
truncated_meta = re.search(r"\[METADATA\][\s\S]*$", result["content"])
|
||||||
|
if truncated_meta:
|
||||||
|
logger.warning("Truncated [METADATA] block detected — stripping from display")
|
||||||
|
result["content"] = result["content"][: truncated_meta.start()]
|
||||||
|
|
||||||
# Clean up extra whitespace from marker removal
|
# Clean up extra whitespace from marker removal
|
||||||
result["content"] = re.sub(r"\n{3,}", "\n\n", result["content"]).strip()
|
result["content"] = re.sub(r"\n{3,}", "\n\n", result["content"]).strip()
|
||||||
@@ -294,18 +306,21 @@ async def send_message(
|
|||||||
response_text, input_tokens, output_tokens = await provider.generate_text(
|
response_text, input_tokens, output_tokens = await provider.generate_text(
|
||||||
system_prompt=system_prompt,
|
system_prompt=system_prompt,
|
||||||
messages=provider_messages,
|
messages=provider_messages,
|
||||||
max_tokens=2000,
|
max_tokens=8000,
|
||||||
)
|
)
|
||||||
|
|
||||||
parsed = _parse_ai_response(response_text)
|
parsed = _parse_ai_response(response_text)
|
||||||
|
|
||||||
# Validate tree update if present
|
# Validate tree update if present (lightweight check for progressive builds —
|
||||||
|
# only require valid root structure, not min node counts)
|
||||||
tree_update = parsed["tree_update"]
|
tree_update = parsed["tree_update"]
|
||||||
if tree_update:
|
if tree_update:
|
||||||
errors = validate_generated_tree(tree_update)
|
if not isinstance(tree_update, dict) or tree_update.get("type") != "decision":
|
||||||
if errors:
|
logger.warning("AI tree update rejected: root must be a decision node")
|
||||||
logger.warning("AI tree update failed validation: %s", errors)
|
tree_update = None
|
||||||
tree_update = None # Silently discard invalid updates
|
elif not tree_update.get("id"):
|
||||||
|
logger.warning("AI tree update rejected: root node missing id")
|
||||||
|
tree_update = None
|
||||||
|
|
||||||
# Update session state
|
# Update session state
|
||||||
history.append({"role": "assistant", "content": parsed["content"], "timestamp": now_iso})
|
history.append({"role": "assistant", "content": parsed["content"], "timestamp": now_iso})
|
||||||
|
|||||||
Reference in New Issue
Block a user