2026-09-09 21:55:37 +08:00
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Any, Literal
|
|
|
|
|
|
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentRequestMetadata(BaseModel):
|
|
|
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
|
|
|
|
|
|
locale: str | None = None
|
|
|
|
|
client_version: str | None = None
|
|
|
|
|
ui_entry: str | None = None
|
2026-09-10 19:51:08 +08:00
|
|
|
# 仅由受理服务写入 Outbox,客户端 API 不接收该字段。
|
|
|
|
|
chitchat_streak: int = Field(default=0, ge=0, le=5)
|
2026-09-11 16:11:30 +08:00
|
|
|
# 客服澄清轮次只来自服务端会话行,客户端不得提交或覆盖。
|
|
|
|
|
clarification_round: int = Field(default=0, ge=0, le=2)
|
|
|
|
|
# 仅供客服在当前短期会话内消解指代的已脱敏上下文,不是长期记忆或客户画像。
|
|
|
|
|
session_context: tuple[str, ...] = Field(default_factory=tuple, max_length=6)
|
2026-09-09 21:55:37 +08:00
|
|
|
|
|
|
|
|
|
2026-09-10 22:01:43 +08:00
|
|
|
class ConversationTurn(BaseModel):
|
|
|
|
|
"""会话中的一轮对话(短期记忆)。
|
|
|
|
|
|
|
|
|
|
只保留角色与正文:模型用它解析指代("那它风险高吗"里的"它"指哪只基金),
|
|
|
|
|
不需要意图、置信度这类内部字段——把内部字段一并喂给模型既增加噪声,
|
|
|
|
|
也扩大了"模型看到不该看的东西"的面。
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
role: Literal["user", "assistant"]
|
|
|
|
|
content: str
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 21:55:37 +08:00
|
|
|
class AgentRequest(BaseModel):
|
|
|
|
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
|
|
|
|
|
|
|
|
agent_type: str
|
|
|
|
|
message: str
|
|
|
|
|
session_id: str
|
|
|
|
|
idempotency_key: str
|
|
|
|
|
metadata: AgentRequestMetadata = Field(default_factory=AgentRequestMetadata)
|
2026-09-10 22:01:43 +08:00
|
|
|
# 本会话中**本次之前**的对话,按时间正序(旧 → 新)。
|
|
|
|
|
# 默认空元组:既有构造点(API 受理路径、单测、验收脚本)无需改动即可继续工作。
|
|
|
|
|
history: tuple[ConversationTurn, ...] = ()
|
2026-09-09 21:55:37 +08:00
|
|
|
|
|
|
|
|
@field_validator("message")
|
|
|
|
|
@classmethod
|
|
|
|
|
def message_must_not_be_blank(cls, value: str) -> str:
|
|
|
|
|
if not value.strip():
|
|
|
|
|
raise ValueError("message must not be blank")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
@field_validator("idempotency_key")
|
|
|
|
|
@classmethod
|
|
|
|
|
def idempotency_key_must_be_valid(cls, value: str) -> str:
|
|
|
|
|
if not 16 <= len(value) <= 128 or not value.replace("-", "").replace("_", "").isalnum():
|
|
|
|
|
raise ValueError("idempotency_key must be 16-128 alphanumeric characters")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RequestContext(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
user_id: str
|
|
|
|
|
trace_id: str
|
|
|
|
|
roles: tuple[str, ...] = ()
|
|
|
|
|
customer_ids: tuple[str, ...] = ()
|
|
|
|
|
data_scope: str = "self"
|
|
|
|
|
portal: str = "api"
|
|
|
|
|
clarification_round: int = Field(default=0, ge=0, le=10)
|
|
|
|
|
permissions: tuple[str, ...] = ()
|
|
|
|
|
permission_scopes: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentDefinition(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
agent_type: str
|
|
|
|
|
version: str
|
|
|
|
|
allowed_tools: tuple[str, ...] = ()
|
|
|
|
|
allowed_roles: tuple[str, ...] = ()
|
|
|
|
|
allowed_portals: tuple[str, ...] = ()
|
|
|
|
|
supported_intents: tuple[str, ...] = ("general",)
|
2026-09-10 17:36:39 +08:00
|
|
|
requires_model_intent_classification: bool = True
|
2026-09-11 16:11:30 +08:00
|
|
|
# 长期/画像记忆属于客户数据能力;默认保留既有 Agent 行为,客服需显式关闭。
|
|
|
|
|
recalls_customer_memory: bool = True
|
2026-09-09 21:55:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ResolvedAgentConfig(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
config_version: str
|
|
|
|
|
prompt_version: str
|
|
|
|
|
model_endpoint: str
|
|
|
|
|
allowed_tools: tuple[str, ...] = ()
|
|
|
|
|
timeout_seconds: int = Field(default=60, gt=0)
|
|
|
|
|
release_id: int | None = None
|
|
|
|
|
allowed_tools_by_intent: dict[str, tuple[str, ...]] = Field(default_factory=dict)
|
|
|
|
|
negative_rules: tuple[tuple[str, str], ...] = ()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecalledMemory(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
memory_uuid: str
|
|
|
|
|
customer_id: str
|
|
|
|
|
content: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SourceReference(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
source_type: Literal["knowledge", "memory", "relationship", "tool"]
|
|
|
|
|
source_id: str
|
|
|
|
|
title: str | None = None
|
|
|
|
|
score: float | None = Field(default=None, ge=0, le=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ToolCallRecord(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
tool_name: str
|
|
|
|
|
status: Literal["succeeded", "failed", "denied"]
|
|
|
|
|
input_summary: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
output_summary: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IntentResult(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
intent: str
|
|
|
|
|
confidence: float = Field(ge=0, le=1)
|
|
|
|
|
needs_clarification: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CoreResult(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
text: str
|
|
|
|
|
intent: IntentResult | None = None
|
|
|
|
|
source_references: tuple[SourceReference, ...] = ()
|
|
|
|
|
tool_calls: tuple[ToolCallRecord, ...] = ()
|
2026-09-11 16:11:30 +08:00
|
|
|
# 请求澄清时由持久化层安全递增会话轮次;达到上限后必须改为人工转接。
|
|
|
|
|
clarification_required: bool = False
|
2026-09-09 21:55:37 +08:00
|
|
|
transfer_required: bool = False
|
|
|
|
|
transfer_reason: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentResult(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
run_id: str
|
|
|
|
|
result: CoreResult
|
|
|
|
|
usage: dict[str, int] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RunProgressEvent(BaseModel):
|
|
|
|
|
model_config = ConfigDict(frozen=True)
|
|
|
|
|
|
|
|
|
|
event_type: Literal["start", "tools", "delta", "replace", "done", "error"]
|
|
|
|
|
run_id: str
|
|
|
|
|
payload: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class DomainEvent:
|
|
|
|
|
event_id: str
|
|
|
|
|
event_type: str
|
|
|
|
|
aggregate_type: str
|
|
|
|
|
aggregate_id: str
|
|
|
|
|
trace_id: str
|
|
|
|
|
payload: dict[str, Any]
|
|
|
|
|
occurred_at: datetime
|