fix(flowpilot): extract _build_session_detail helper, fix link_ticket 500
The same model_validate bug existed in link_ticket endpoint (line 467). Extracted the manual AISessionDetail construction into a shared helper _build_session_detail() used by both get_session and link_ticket. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,64 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(prefix="/ai-sessions", tags=["ai-sessions"])
|
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:
|
def _require_ai_enabled() -> None:
|
||||||
if not settings.ai_enabled:
|
if not settings.ai_enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -464,8 +522,7 @@ async def link_ticket_to_session(
|
|||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
|
||||||
|
|
||||||
detail = AISessionDetail.model_validate(session)
|
return _build_session_detail(session)
|
||||||
return detail
|
|
||||||
|
|
||||||
|
|
||||||
# ── Search sessions (Command Palette) ──
|
# ── 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:
|
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")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized")
|
||||||
|
|
||||||
# Build step responses
|
return _build_session_detail(session)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# ── Documentation ──
|
# ── Documentation ──
|
||||||
|
|||||||
Reference in New Issue
Block a user