"""Wire schemas for the support-ticket domain.""" from __future__ import annotations from typing import Literal from pydantic import BaseModel, Field class CreateTicketBody(BaseModel): title: str = Field(min_length=1, max_length=255) body: str = Field(min_length=1, max_length=8000) category: str = "other" priority: str = "normal" # When the ticket is filed from a pre-file deflection session, link it so # the funnel can record an escalation (best-effort; ignored if unknown). deflect_event_id: str | None = None class DeflectBody(BaseModel): title: str = Field(default="", max_length=255) body: str = Field(min_length=1, max_length=8000) class ReplyBody(BaseModel): body: str = Field(min_length=1, max_length=8000) class ResolveBody(BaseModel): resolution: str = Field(min_length=1, max_length=8000) class RateBody(BaseModel): """3-point CSAT, mirror caliber: 1=不满意 / 2=一般 / 3=满意.""" score: int = Field(ge=1, le=3) class AssignBody(BaseModel): assignee_id: str class CopilotMessage(BaseModel): role: str # "user" | "assistant" content: str = Field(max_length=8000) class CopilotChatBody(BaseModel): """One question from the web assistant page. ``message`` is the whole request. The thread the model is shown is the SERVER's, keyed by person — exactly as in Feishu — and is not accepted from the client. It was, until #3413, and that stopped being acceptable the moment this surface gained a write path: "the caller supplies what the assistant remembers" is a place to put words in the assistant's mouth and then act on them. ``messages`` remains only so a page still running the previous bundle keeps working through a deploy; its last user turn is read and the rest ignored. """ message: str | None = Field(default=None, max_length=8000) messages: list[CopilotMessage] = Field(default_factory=list, max_length=40) def question(self) -> str: if self.message and self.message.strip(): return self.message.strip() for turn in reversed(self.messages): if turn.role == "user" and turn.content.strip(): return turn.content.strip() return "" class CopilotFeedbackBody(BaseModel): """A verdict on one stored answer, and optionally why it did not work.""" message_id: str helpful: bool #: One of ``feedback.REASON_CLASSIFICATIONS``. Only meaningful with #: ``helpful=false``; an unknown value is refused rather than stored raw. reason: str | None = Field(default=None, max_length=32) class CopilotEscalateBody(BaseModel): """Hand the conversation to a human. ``messages`` is ignored when the server holds a transcript for this person: the ticket must quote what the assistant actually said, not what the page says it said. It stays accepted for the copilot-disabled case, where there is no server-side thread because no turn ever ran. """ #: Which stored thread to quote. The page passes back the id its last #: answer carried; it selects a conversation, it does not authorise one. conversation_id: str | None = Field(default=None, max_length=64) messages: list[CopilotMessage] = Field(default_factory=list, max_length=40) category: str = "other" priority: str = "normal" class KbCreateBody(BaseModel): title: str = Field(min_length=1, max_length=255) body: str = Field(min_length=1, max_length=16000) category: str = "other" publish: bool = False # Alternate phrasings of the same question, one per line. Part of the # retrieval text (title-weighted), not of the answer the employee reads. search_aliases: str | None = Field(default=None, max_length=4000) language: str | None = Field(default=None, max_length=8) class KbUpdateBody(BaseModel): title: str | None = Field(default=None, max_length=255) body: str | None = Field(default=None, max_length=16000) category: str | None = None # "" is a real edit here (drop every alias); None means "leave alone". search_aliases: str | None = Field(default=None, max_length=4000) language: str | None = Field(default=None, max_length=8) class KbStatusBody(BaseModel): status: Literal["draft", "published", "archived"] class AttributesBody(BaseModel): category: str | None = None priority: str | None = None class TicketMessageOut(BaseModel): id: str author_id: str | None author_display: str | None kind: str body: str created_at: str class TicketOut(BaseModel): id: str ticket_number: str requester_id: str requester_display: str | None title: str body: str category: str priority: str status: str source: str assignee_id: str | None assignee_display: str | None resolution: str | None created_at: str updated_at: str | None first_response_at: str | None resolved_at: str | None closed_at: str | None # Present only on the detail view. messages: list[TicketMessageOut] | None = None __all__ = [ "AssignBody", "AttributesBody", "CreateTicketBody", "ReplyBody", "ResolveBody", "TicketMessageOut", "TicketOut", ]