feat: add [TRIAGE_UPDATE] marker extraction and auto-PATCH (Phase 2)
- Add _parse_triage_update_marker() parser following existing marker pattern - Add [TRIAGE_UPDATE] instructions to system prompt with grounding rules - Add QuestionItem.options support in question parser - Wire triage extraction into both main and branch-aware chat paths - Auto-PATCH session: AI only fills null fields (manual edits win) - Evidence items: AI appends only, never modifies existing - Return triage_update in ChatMessageResponse for frontend header sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,10 +133,13 @@ def _parse_questions_marker(ai_content: str) -> tuple[str, list[dict[str, Any]]
|
||||
valid_questions = []
|
||||
for q in questions:
|
||||
if isinstance(q, dict) and q.get("text"):
|
||||
valid_questions.append({
|
||||
item = {
|
||||
"text": q["text"],
|
||||
"context": q.get("context", ""),
|
||||
})
|
||||
}
|
||||
if q.get("options") and isinstance(q["options"], list):
|
||||
item["options"] = q["options"]
|
||||
valid_questions.append(item)
|
||||
|
||||
if not valid_questions:
|
||||
return ai_content, None
|
||||
@@ -147,6 +150,43 @@ def _parse_questions_marker(ai_content: str) -> tuple[str, list[dict[str, Any]]
|
||||
return cleaned, valid_questions
|
||||
|
||||
|
||||
def _parse_triage_update_marker(ai_content: str) -> tuple[str, dict[str, Any] | None]:
|
||||
"""Extract [TRIAGE_UPDATE]...[/TRIAGE_UPDATE] JSON from AI response.
|
||||
|
||||
Returns (cleaned_content, triage_update_dict_or_None).
|
||||
The marker is stripped from display text.
|
||||
"""
|
||||
match = re.search(r'\[TRIAGE_UPDATE\]\s*([\s\S]*?)\s*\[/TRIAGE_UPDATE\]', ai_content)
|
||||
if not match:
|
||||
return ai_content, None
|
||||
|
||||
try:
|
||||
raw = match.group(1).strip()
|
||||
if raw.startswith("```"):
|
||||
raw = re.sub(r'^```(?:json)?\s*', '', raw)
|
||||
raw = re.sub(r'\s*```$', '', raw)
|
||||
triage = json.loads(raw)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning("Failed to parse [TRIAGE_UPDATE] marker: %s", e)
|
||||
return ai_content, None
|
||||
|
||||
if not isinstance(triage, dict):
|
||||
logger.warning("Invalid [TRIAGE_UPDATE] data — expected object")
|
||||
return ai_content, None
|
||||
|
||||
# Only keep recognized fields
|
||||
valid_fields = {"client_name", "asset_name", "issue_category", "triage_hypothesis", "evidence_items"}
|
||||
filtered = {k: v for k, v in triage.items() if k in valid_fields and v is not None}
|
||||
|
||||
if not filtered:
|
||||
return ai_content, None
|
||||
|
||||
cleaned = ai_content[:match.start()] + ai_content[match.end():]
|
||||
cleaned = cleaned.strip()
|
||||
|
||||
return cleaned, filtered
|
||||
|
||||
|
||||
async def create_chat_session(
|
||||
user_id: UUID,
|
||||
account_id: UUID,
|
||||
@@ -190,7 +230,7 @@ async def send_chat_message(
|
||||
images: Optional list of {"media_type": str, "data": str (base64)}
|
||||
for vision content attached to this message.
|
||||
|
||||
Returns (ai_content, suggested_flows, session, fork_metadata, actions_data, questions_data).
|
||||
Returns (ai_content, suggested_flows, session, fork_metadata, actions_data, questions_data, triage_update_data).
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AISession).where(
|
||||
@@ -253,6 +293,19 @@ async def send_chat_message(
|
||||
branch_display, branch_fork_data = _parse_fork_marker(ai_content)
|
||||
branch_display, branch_actions_data = _parse_actions_marker(branch_display)
|
||||
branch_display, branch_questions_data = _parse_questions_marker(branch_display)
|
||||
branch_display, branch_triage_data = _parse_triage_update_marker(branch_display)
|
||||
|
||||
# Auto-PATCH triage from branch response
|
||||
if branch_triage_data:
|
||||
for field in ("client_name", "asset_name", "issue_category", "triage_hypothesis"):
|
||||
if field in branch_triage_data and getattr(session, field) is None:
|
||||
setattr(session, field, branch_triage_data[field])
|
||||
new_evidence = branch_triage_data.get("evidence_items")
|
||||
if new_evidence and isinstance(new_evidence, list):
|
||||
existing = list(session.evidence_items or [])
|
||||
existing.extend(new_evidence)
|
||||
session.evidence_items = existing
|
||||
|
||||
if branch_display != ai_content:
|
||||
# Store stripped content in branch history
|
||||
msgs[-1] = {"role": "assistant", "content": branch_display}
|
||||
@@ -298,7 +351,7 @@ async def send_chat_message(
|
||||
suggested_flows = extract_suggested_flows(
|
||||
await rag_search(query=message, account_id=account_id, db=db, limit=8)
|
||||
)
|
||||
return branch_display, suggested_flows, session, branch_fork_metadata, branch_actions_data, branch_questions_data
|
||||
return branch_display, suggested_flows, session, branch_fork_metadata, branch_actions_data, branch_questions_data, branch_triage_data
|
||||
|
||||
# Auto-title from first message if still default
|
||||
if session.step_count == 0 and message.strip():
|
||||
@@ -341,12 +394,29 @@ async def send_chat_message(
|
||||
# Check for questions marker in AI response
|
||||
display_content, questions_data = _parse_questions_marker(display_content)
|
||||
|
||||
# Check for triage update marker in AI response
|
||||
display_content, triage_update_data = _parse_triage_update_marker(display_content)
|
||||
|
||||
logger.info(
|
||||
"Marker parsing results — actions: %s, questions: %s, fork: %s, raw_length: %d, display_length: %d",
|
||||
bool(actions_data), bool(questions_data), bool(fork_data),
|
||||
"Marker parsing results — actions: %s, questions: %s, fork: %s, triage: %s, raw_length: %d, display_length: %d",
|
||||
bool(actions_data), bool(questions_data), bool(fork_data), bool(triage_update_data),
|
||||
len(ai_content), len(display_content),
|
||||
)
|
||||
|
||||
# Auto-PATCH session with triage metadata if AI inferred new fields
|
||||
if triage_update_data:
|
||||
# Apply non-evidence fields directly (AI only fills null fields — manual edits win)
|
||||
for field in ("client_name", "asset_name", "issue_category", "triage_hypothesis"):
|
||||
if field in triage_update_data and getattr(session, field) is None:
|
||||
setattr(session, field, triage_update_data[field])
|
||||
|
||||
# Append new evidence items (never modify existing)
|
||||
new_evidence = triage_update_data.get("evidence_items")
|
||||
if new_evidence and isinstance(new_evidence, list):
|
||||
existing = list(session.evidence_items or [])
|
||||
existing.extend(new_evidence)
|
||||
session.evidence_items = existing
|
||||
|
||||
# Store DISPLAY content (markers stripped) in conversation_messages.
|
||||
# The format reminder in the user message + system prompt final reminder
|
||||
# are sufficient to keep the AI emitting markers on subsequent turns.
|
||||
@@ -413,4 +483,4 @@ async def send_chat_message(
|
||||
|
||||
suggested_flows = extract_suggested_flows(rag_results)
|
||||
|
||||
return display_content, suggested_flows, session, fork_metadata, actions_data, questions_data
|
||||
return display_content, suggested_flows, session, fork_metadata, actions_data, questions_data, triage_update_data
|
||||
|
||||
Reference in New Issue
Block a user