diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py index 891bbb92..07d76d06 100644 --- a/backend/app/api/endpoints/ai_sessions.py +++ b/backend/app/api/endpoints/ai_sessions.py @@ -52,6 +52,64 @@ logger = logging.getLogger(__name__) router = APIRouter(prefix="/ai-sessions", tags=["ai-sessions"]) +def _build_session_detail(session: AISession) -> AISessionDetail: + """Build AISessionDetail from ORM session with properly mapped steps. + + AISessionDetail.model_validate(session) fails because the ORM steps + relationship uses 'id' while AISessionStepResponse expects 'step_id'. + This helper manually maps all fields to avoid that validation error. + """ + step_responses = [] + for step in (session.steps or []): + options = [] + if step.options_presented: + options = [ + StepOptionSchema( + label=opt.get("label", ""), + value=opt.get("value", ""), + followup_hint=opt.get("followup_hint"), + ) + for opt in step.options_presented + ] + content = step.content or {} + step_responses.append(AISessionStepResponse( + step_id=step.id, + step_order=step.step_order, + step_type=step.step_type, + content=content, + context_message=step.context_message, + options=options, + allow_free_text=content.get("allow_free_text", True), + allow_skip=content.get("allow_skip", True), + confidence_tier=session.confidence_tier, + confidence_score=step.confidence_at_step, + )) + + return AISessionDetail( + id=session.id, + status=session.status, + intake_type=session.intake_type, + intake_content=session.intake_content or {}, + problem_summary=session.problem_summary, + problem_domain=session.problem_domain, + confidence_tier=session.confidence_tier, + step_count=session.step_count, + session_rating=session.session_rating, + psa_ticket_id=session.psa_ticket_id, + psa_connection_id=session.psa_connection_id, + escalation_reason=session.escalation_reason, + matched_flow_id=session.matched_flow_id, + match_score=getattr(session, 'match_score', None), + resolution_summary=session.resolution_summary, + resolution_action=getattr(session, 'resolution_action', None), + session_feedback=session.session_feedback, + ticket_data=session.ticket_data, + created_at=session.created_at, + resolved_at=session.resolved_at, + steps=step_responses, + ) + + def _require_ai_enabled() -> None: if not settings.ai_enabled: raise HTTPException( @@ -464,8 +522,7 @@ async def link_ticket_to_session( if not session: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found") - detail = AISessionDetail.model_validate(session) - return detail + return _build_session_detail(session) # ── Search sessions (Command Palette) ── @@ -618,60 +675,7 @@ async def get_session( if session.user_id != current_user.id and session.escalated_to_id != current_user.id and not is_handler: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized") - # Build step responses - step_responses = [] - for step in session.steps: - options = [] - if step.options_presented: - options = [ - StepOptionSchema( - label=opt.get("label", ""), - value=opt.get("value", ""), - followup_hint=opt.get("followup_hint"), - ) - for opt in step.options_presented - ] - content = step.content or {} - step_responses.append(AISessionStepResponse( - step_id=step.id, - step_order=step.step_order, - step_type=step.step_type, - content=content, - context_message=step.context_message, - options=options, - allow_free_text=content.get("allow_free_text", True), - allow_skip=content.get("allow_skip", True), - confidence_tier=session.confidence_tier, - confidence_score=step.confidence_at_step, - )) - - # Build detail manually — AISessionDetail.model_validate(session) fails because - # the ORM relationship 'steps' has 'id' not 'step_id', causing validation errors. - # Instead, extract non-step fields from ORM and set step_responses separately. - detail = AISessionDetail( - id=session.id, - status=session.status, - intake_type=session.intake_type, - intake_content=session.intake_content or {}, - problem_summary=session.problem_summary, - problem_domain=session.problem_domain, - confidence_tier=session.confidence_tier, - step_count=session.step_count, - session_rating=session.session_rating, - psa_ticket_id=session.psa_ticket_id, - psa_connection_id=session.psa_connection_id, - escalation_reason=session.escalation_reason, - matched_flow_id=session.matched_flow_id, - match_score=getattr(session, 'match_score', None), - resolution_summary=session.resolution_summary, - resolution_action=getattr(session, 'resolution_action', None), - session_feedback=session.session_feedback, - ticket_data=session.ticket_data, - created_at=session.created_at, - resolved_at=session.resolved_at, - steps=step_responses, - ) - return detail + return _build_session_detail(session) # ── Documentation ──