1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
163 lines
6.0 KiB
Python
163 lines
6.0 KiB
Python
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
|