from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, HttpUrl class StrictPayload(BaseModel): model_config = ConfigDict(extra="forbid") class ReleasePayload(StrictPayload): release_no: str = Field(min_length=1, max_length=64) title: str = Field(min_length=1, max_length=128) change_summary: str = Field(min_length=1, max_length=1000) class ItemPayload(StrictPayload): # `fund_market` 必须在列内:`admin_service` 有专门的 `FUND_MARKET_FIELDS` 校验与 # `namespace=fund_market / config_key=default` 读取路径,缺了它会让行情配置 # 走 API 一律 422、只能靠工具内默认值运行(曾因此被回退过一次)。 namespace: Literal["agent_tools", "memory", "relationship", "runtime", "fund_market"] item_key: str = Field(min_length=1, max_length=128) value_json: dict[str, Any] schema_version: Literal["1"] = "1" class EndpointPayload(StrictPayload): endpoint_code: str = Field(min_length=1, max_length=64) provider: str = Field(min_length=1, max_length=32) model_name: str = Field(min_length=1, max_length=128) base_url: HttpUrl secret_ref: str = Field(pattern=r"^env:[A-Z][A-Z0-9_]{0,100}$") capabilities: list[str] = Field(min_length=1, max_length=20) allowed_data_levels: list[str] = Field(min_length=1, max_length=20) context_window: int = Field(gt=0) timeout_ms: int = Field(default=15000, ge=100, le=120000) class FallbackPayload(StrictPayload): endpoint_id: int = Field(gt=0) fallback_order: int = Field(ge=1, le=2) retryable_error_codes: list[str] = Field(default_factory=list) class RoutingPayload(StrictPayload): rule_code: str = Field(min_length=1, max_length=64) agent_type: str = Field(min_length=2, max_length=32) task_type: str = Field(min_length=1, max_length=48) model_policy: str = Field(min_length=1, max_length=32) primary_endpoint_id: int = Field(gt=0) fallbacks: list[FallbackPayload] = Field(default_factory=list, max_length=2) max_attempts: int = Field(default=2, ge=1, le=3) latency_budget_ms: int = Field(default=15000, ge=100, le=120000) priority: int = Field(default=100, ge=0) class PromptPayload(StrictPayload): release_id: int = Field(gt=0) prompt_code: str = Field(min_length=1, max_length=64) task_type: str = Field(min_length=1, max_length=48) agent_type: str | None = Field(default=None, max_length=32) version: int = Field(ge=1) system_prompt: str = Field(min_length=1, max_length=32000) user_prompt_template: str = Field(min_length=1, max_length=32000) input_schema: dict[str, Any] | None = None output_schema: dict[str, Any] | None = None class IntentPayload(StrictPayload): agent_type: str = Field(min_length=2, max_length=32) intent_code: str = Field(min_length=1, max_length=64) intent_name: str = Field(min_length=1, max_length=128) description: str | None = Field(default=None, max_length=500) examples: list[str] classifier_instruction: str | None = None confidence_threshold: str = Field(default="0.6000", pattern=r"^(0(\.\d{1,4})?|1(\.0{1,4})?)$") max_clarification_rounds: int = Field(default=2, ge=0, le=10) transfer_on_failure: bool = True allowed_tools: list[str] = Field(default_factory=list) priority: int = Field(default=100, ge=0) version: int = Field(default=1, ge=1) #: `agent_reply_template.chk_template_scene` 允许的取值。**必须逐字对齐**: #: 此前 `ReplyPayload.scene` 只校验长度(`min_length=1, max_length=32`), #: 于是传一个不在列内的场景会**穿过接口校验、撞上数据库 CHECK 约束**, #: 最终以 `500` 冒出(`(3819, "Check constraint 'chk_template_scene' is violated.")`)—— #: 那本该是一次 `422` 参数校验失败。2026-09-13 由接口逐条测试发现。 ReplyScene = Literal[ "disclaimer", "low_confidence", "compliance_block", "transfer", "model_failure", "system_busy", "clarification", ] class ReplyPayload(StrictPayload): template_code: str = Field(min_length=1, max_length=64) scene: ReplyScene title: str = Field(min_length=1, max_length=128) content_text: str = Field(min_length=1, max_length=32000) variables: list[str] = Field(default_factory=list) locale: str = Field(default="zh-CN", max_length=16) version: int = Field(default=1, ge=1) class NegativePayload(StrictPayload): rule_code: str = Field(min_length=1, max_length=64) word_pattern: str = Field(min_length=1, max_length=256) match_type: Literal["contains", "exact"] = "contains" category: str = Field(min_length=1, max_length=32) severity: Literal["block", "replace", "warn"] = "block" applicable_agents: list[str] | None = None safe_reply_template_code: str | None = Field(default=None, max_length=64) class ReviewPayload(StrictPayload): decision: Literal["approved", "rejected"] comment: str = Field(default="", max_length=1000) #: `svc_handover_ticket.status` 的取值,**必须逐字对齐数据库 CHECK 约束** #: (`chk_handoff_status`,见 `docs/02-数据库建表设计.md` §7.2)。 #: 与 `ReplyScene` 同一个教训:不在列内的值若穿到数据库,会以 500 冒出而不是 422。 HandoverStatus = Literal[ "pending", "assigned", "processing", "resolved", "closed", "cancelled", ] class HandoverAssignPayload(StrictPayload): """分配工单给一个坐席(`sys_user.id`)。""" assignee_id: int = Field(gt=0) class HandoverResolvePayload(StrictPayload): """解决工单必须给结论(`resolution` 落库)。""" resolution: str = Field(min_length=2, max_length=2000) class HandoverClosePayload(StrictPayload): """关闭工单可选补一条说明,追加在结论之后。""" note: str = Field(default="", max_length=2000) class HandoverCancelPayload(StrictPayload): """取消工单必须给原因(写进 `resolution`)。""" reason: str = Field(min_length=2, max_length=500) class EmptyPayload(StrictPayload): pass