310 lines
9.0 KiB
Python
310 lines
9.0 KiB
Python
"""Pydantic schemas for FlowPilot AI sessions."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional, Any
|
|
from uuid import UUID
|
|
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ── Intake ──
|
|
|
|
class AISessionCreateRequest(BaseModel):
|
|
"""Start a new FlowPilot or chat session."""
|
|
session_type: str = Field(
|
|
"guided",
|
|
pattern="^(guided|chat)$",
|
|
description="Session type: guided (FlowPilot) or chat (assistant)",
|
|
)
|
|
intake_type: str = Field(
|
|
"free_text",
|
|
pattern="^(free_text|psa_ticket|screenshot|log_paste|combined)$",
|
|
)
|
|
intake_content: dict[str, Any] = Field(
|
|
...,
|
|
description=(
|
|
"Intake payload. Shape depends on intake_type: "
|
|
"{text: str} for free_text, "
|
|
"{text?: str, image_urls?: list[str]} for screenshot, "
|
|
"{text?: str, log_content?: str} for log_paste, "
|
|
"{ticket_id: str, psa_connection_id: str} for psa_ticket, "
|
|
"any combination for combined."
|
|
),
|
|
)
|
|
psa_ticket_id: Optional[str] = None
|
|
psa_connection_id: Optional[UUID] = None
|
|
|
|
|
|
class AISessionCreateResponse(BaseModel):
|
|
"""Response after starting a session — includes the first FlowPilot step."""
|
|
session_id: UUID
|
|
status: str
|
|
confidence_tier: str
|
|
problem_summary: str | None = None
|
|
problem_domain: str | None = None
|
|
matched_flow_id: UUID | None = None
|
|
matched_flow_name: str | None = None
|
|
match_score: float | None = None
|
|
first_step: AISessionStepResponse
|
|
psa_context_status: str | None = None # loaded | unavailable | None (no PSA)
|
|
|
|
|
|
# ── Step interaction ──
|
|
|
|
class StepOptionSchema(BaseModel):
|
|
"""A selectable option presented to the engineer."""
|
|
label: str
|
|
value: str
|
|
followup_hint: str | None = None
|
|
|
|
|
|
class AISessionStepResponse(BaseModel):
|
|
"""A FlowPilot step rendered in the session UI."""
|
|
step_id: UUID
|
|
step_order: int
|
|
step_type: str
|
|
content: dict[str, Any]
|
|
context_message: str | None = None
|
|
options: list[StepOptionSchema] = []
|
|
allow_free_text: bool = True
|
|
allow_skip: bool = True
|
|
confidence_tier: str
|
|
confidence_score: float
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class StepResponseRequest(BaseModel):
|
|
"""Engineer's response to a FlowPilot step."""
|
|
selected_option: str | None = None
|
|
free_text_input: str | None = None
|
|
was_skipped: bool = False
|
|
action_result: dict[str, Any] | None = None
|
|
|
|
|
|
class StepResponseResponse(BaseModel):
|
|
"""FlowPilot's next step after processing the engineer's response."""
|
|
session_id: UUID
|
|
status: str
|
|
confidence_tier: str
|
|
confidence_score: float
|
|
next_step: AISessionStepResponse | None = None
|
|
resolution_suggested: bool = False
|
|
resolution_summary: str | None = None
|
|
|
|
|
|
# ── Resolution / Escalation ──
|
|
|
|
class ResolveSessionRequest(BaseModel):
|
|
"""Close a session as resolved."""
|
|
resolution_summary: str = Field(..., min_length=5, max_length=2000)
|
|
resolution_action: str | None = None
|
|
session_rating: int | None = Field(None, ge=1, le=5)
|
|
session_feedback: str | None = None
|
|
|
|
|
|
class EscalateSessionRequest(BaseModel):
|
|
"""Escalate a session to another engineer."""
|
|
escalation_reason: str = Field(..., min_length=5, max_length=2000)
|
|
escalated_to_id: UUID | None = None
|
|
|
|
|
|
class DocumentationStep(BaseModel):
|
|
"""A step in the documentation trail."""
|
|
step_number: int
|
|
step_type: str
|
|
description: str
|
|
engineer_response: str | None = None
|
|
outcome: str | None = None
|
|
|
|
|
|
class SessionDocumentation(BaseModel):
|
|
"""Auto-generated session documentation."""
|
|
problem_summary: str
|
|
problem_domain: str | None = None
|
|
intake_summary: str
|
|
diagnostic_steps: list[DocumentationStep]
|
|
resolution_summary: str | None = None
|
|
escalation_reason: str | None = None
|
|
total_steps: int
|
|
duration_display: str | None = None
|
|
generated_at: datetime
|
|
|
|
|
|
class SessionCloseResponse(BaseModel):
|
|
"""Response after resolving or escalating."""
|
|
session_id: UUID
|
|
status: str
|
|
documentation: SessionDocumentation | None = None
|
|
psa_push_status: str = "no_psa" # sent | pending_retry | no_psa | failed
|
|
psa_push_error: str | None = None
|
|
member_mapping_warning: str | None = None
|
|
|
|
|
|
class StatusUpdateRequest(BaseModel):
|
|
"""Generate a mid-session or post-session status update."""
|
|
audience: str = Field(
|
|
...,
|
|
pattern="^(ticket_notes|client_update|email_draft)$",
|
|
description="Who is this update for?",
|
|
)
|
|
length: str = Field(
|
|
"detailed",
|
|
pattern="^(quick|detailed)$",
|
|
description="Quick (1-2 sentences) or detailed breakdown",
|
|
)
|
|
context: str = Field(
|
|
"status",
|
|
pattern="^(status|resolution|escalation)$",
|
|
description="What type of communication: mid-session status, resolution close-out, or escalation handoff",
|
|
)
|
|
|
|
|
|
class StatusUpdateResponse(BaseModel):
|
|
"""Generated status update content."""
|
|
content: str
|
|
audience: str
|
|
length: str
|
|
context: str
|
|
session_status: str
|
|
steps_completed: int
|
|
time_spent_display: str | None = None
|
|
client_name: str | None = None
|
|
generated_at: datetime
|
|
|
|
|
|
class RateSessionRequest(BaseModel):
|
|
"""Submit post-session rating."""
|
|
rating: int = Field(..., ge=1, le=5)
|
|
feedback: str | None = None
|
|
|
|
|
|
class PickupSessionRequest(BaseModel):
|
|
"""Pick up an escalated session as a new engineer."""
|
|
resume_mode: str = Field("continue", pattern="^(continue|fresh)$")
|
|
additional_context: str | None = None
|
|
|
|
|
|
class LinkTicketRequest(BaseModel):
|
|
"""Link a PSA ticket to an in-progress session."""
|
|
psa_ticket_id: str
|
|
psa_connection_id: UUID
|
|
|
|
|
|
# ── List / Detail ──
|
|
|
|
class AISessionSummary(BaseModel):
|
|
"""Compact session for list views."""
|
|
id: UUID
|
|
session_type: str = "guided"
|
|
title: str | None = None
|
|
status: str
|
|
intake_type: str
|
|
problem_summary: str | None = None
|
|
problem_domain: str | None = None
|
|
confidence_tier: str
|
|
step_count: int
|
|
session_rating: int | None = None
|
|
psa_ticket_id: str | None = None
|
|
escalation_reason: str | None = None
|
|
created_at: datetime
|
|
resolved_at: datetime | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class AISessionDetail(AISessionSummary):
|
|
"""Full session detail with steps (guided) or messages (chat)."""
|
|
intake_content: dict[str, Any]
|
|
matched_flow_id: UUID | None = None
|
|
match_score: float | None = None
|
|
resolution_summary: str | None = None
|
|
resolution_action: str | None = None
|
|
escalation_reason: str | None = None
|
|
session_feedback: str | None = None
|
|
psa_ticket_id: str | None = None
|
|
psa_connection_id: UUID | None = None
|
|
ticket_data: dict[str, Any] | None = None
|
|
steps: list[AISessionStepResponse] = []
|
|
conversation_messages: list[dict[str, Any]] = [] # Chat sessions store messages here
|
|
pending_task_lane: dict[str, Any] | None = None
|
|
is_branching: bool = False
|
|
active_branch_id: str | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Chat session ──
|
|
|
|
class ChatSessionCreateResponse(BaseModel):
|
|
"""Response after creating a chat session on ai_sessions."""
|
|
session_id: UUID
|
|
session_type: str = "chat"
|
|
title: str
|
|
status: str = "active"
|
|
|
|
|
|
class ChatMessageRequest(BaseModel):
|
|
"""Send a message in a chat session."""
|
|
message: str = Field(..., min_length=1, max_length=8000)
|
|
upload_ids: list[UUID] = Field(default_factory=list, max_length=10)
|
|
|
|
|
|
class ForkBranchInfo(BaseModel):
|
|
"""Branch info returned when a fork is created."""
|
|
branch_id: str
|
|
label: str
|
|
|
|
|
|
class ForkMetadata(BaseModel):
|
|
"""Metadata returned when the AI suggests a diagnostic fork."""
|
|
fork_point_id: str
|
|
fork_reason: str
|
|
branches: list[ForkBranchInfo]
|
|
active_branch_id: str
|
|
|
|
|
|
class ActionItem(BaseModel):
|
|
"""A single action item for the engineer."""
|
|
label: str
|
|
command: str | None = None
|
|
description: str = ""
|
|
|
|
|
|
class QuestionItem(BaseModel):
|
|
"""A question the AI needs answered by the engineer."""
|
|
text: str
|
|
context: str = ""
|
|
|
|
|
|
class ChatMessageResponse(BaseModel):
|
|
"""AI response to a chat message."""
|
|
content: str
|
|
suggested_flows: list[dict[str, Any]] = []
|
|
fork: ForkMetadata | None = None
|
|
actions: list[ActionItem] | None = None
|
|
questions: list[QuestionItem] | None = None
|
|
|
|
|
|
class SaveTaskLaneRequest(BaseModel):
|
|
"""Save the full task lane state (AI items + user responses)."""
|
|
questions: list[QuestionItem] = Field(default_factory=list, max_length=50)
|
|
actions: list[ActionItem] = Field(default_factory=list, max_length=50)
|
|
responses: list[dict[str, Any]] = Field(
|
|
default_factory=list,
|
|
max_length=100,
|
|
description="User's in-progress task responses with state/value",
|
|
)
|
|
|
|
|
|
class AISessionSearchResult(BaseModel):
|
|
"""Lightweight session result for Command Palette / autocomplete."""
|
|
id: UUID
|
|
problem_summary: str | None = None
|
|
problem_domain: str | None = None
|
|
status: str
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|