feat: add cockpit triage backend foundation (Phase 1)

- Migration 071: add client_name, asset_name, issue_category,
  triage_hypothesis, evidence_items columns to ai_sessions
- TriageUpdate schema for AI-inferred header updates in chat responses
- QuestionItem.options field for quick-reply buttons
- PATCH /ai-sessions/{id}/triage endpoint for manual header edits
- POST /ai-sessions/{id}/handoff-draft streaming endpoint for conclude modal
- Structured handoff fields (root_cause, steps_taken, recommendations)
  on resolve/escalate requests, passed through to ResolutionOutputGenerator
- Triage fields exposed in AISessionDetail response for session resume

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-01 22:30:48 +00:00
parent cb750d52f6
commit 15781baeb7
5 changed files with 270 additions and 4 deletions

View File

@@ -19,7 +19,13 @@ class ResolutionOutputGenerator:
def __init__(self, db: AsyncSession):
self.db = db
async def generate_all(self, session_id: UUID) -> list[SessionResolutionOutput]:
async def generate_all(
self,
session_id: UUID,
root_cause: str | None = None,
steps_taken: list[str] | None = None,
recommendations: str | None = None,
) -> list[SessionResolutionOutput]:
result = await self.db.execute(
select(AISession).where(AISession.id == session_id)
)
@@ -27,7 +33,12 @@ class ResolutionOutputGenerator:
if not session:
raise ValueError(f"Session {session_id} not found")
context = self._build_session_context(session)
context = self._build_session_context(
session,
root_cause=root_cause,
steps_taken=steps_taken,
recommendations=recommendations,
)
outputs = []
for output_type, prompt in [
@@ -82,13 +93,41 @@ class ResolutionOutputGenerator:
await self.db.flush()
return output
def _build_session_context(self, session: AISession) -> str:
def _build_session_context(
self,
session: AISession,
root_cause: str | None = None,
steps_taken: list[str] | None = None,
recommendations: str | None = None,
) -> str:
parts = [
f"Problem: {session.problem_summary or 'Unknown'}",
f"Domain: {session.problem_domain or 'Unknown'}",
f"Resolution: {session.resolution_summary or 'Not specified'}",
f"Steps taken: {session.step_count}",
]
# Structured handoff fields from cockpit conclude modal
if root_cause:
parts.append(f"Root cause: {root_cause}")
if steps_taken:
parts.append("Steps performed:")
for step in steps_taken:
parts.append(f" - {step}")
if recommendations:
parts.append(f"Recommendations: {recommendations}")
# Triage metadata
if getattr(session, 'client_name', None):
parts.append(f"Client: {session.client_name}")
if getattr(session, 'triage_hypothesis', None):
parts.append(f"Hypothesis: {session.triage_hypothesis}")
if getattr(session, 'evidence_items', None):
parts.append("Evidence collected:")
for item in session.evidence_items:
icon = {"confirmed": "", "ruled_out": "", "pending": "?"}.get(item.get("status", ""), "?")
parts.append(f" {icon} {item.get('text', '')}")
msgs = session.conversation_messages or []
if msgs:
parts.append("\nConversation highlights:")