diff --git a/server/src/app/schemas/orchestrator.py b/server/src/app/schemas/orchestrator.py index 9489f25..2fb760e 100644 --- a/server/src/app/schemas/orchestrator.py +++ b/server/src/app/schemas/orchestrator.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field @@ -12,6 +13,7 @@ OrchestratorStatus = Literal["succeeded", "blocked", "failed"] class OrchestratorRequest(BaseModel): source: OrchestratorSource = Field(description="请求来源。") user_id: str | None = Field(default=None, description="当前用户 ID,任务触发可为空。") + conversation_id: str | None = Field(default=None, description="多轮对话会话 ID。") message: str | None = Field(default=None, description="用户消息或任务描述。") task_id: str | None = Field(default=None, description="任务资产 ID,schedule 触发时优先使用。") context_json: dict[str, Any] = Field( @@ -34,6 +36,7 @@ class OrchestratorTraceSummary(BaseModel): class OrchestratorResponse(BaseModel): run_id: str = Field(description="本次运行的唯一 run_id。") + conversation_id: str | None = Field(default=None, description="当前会话 ID。") selected_agent: OrchestratorAgent | None = Field( default=None, description="最终路由到的下游 Agent。", @@ -44,3 +47,37 @@ class OrchestratorResponse(BaseModel): result: dict[str, Any] = Field(default_factory=dict, description="对前端可直接展示的最小结果。") requires_confirmation: bool = Field(default=False, description="是否需要用户或管理员确认。") trace_summary: OrchestratorTraceSummary = Field(description="简化后的 Trace 摘要。") + + +class ConversationMessageRead(BaseModel): + id: str = Field(description="消息 ID。") + role: str = Field(description="消息角色。") + content: str = Field(description="消息正文。") + run_id: str | None = Field(default=None, description="关联运行 ID。") + message_json: dict[str, Any] = Field(default_factory=dict, description="扩展消息载荷。") + created_at: datetime | None = Field(default=None, description="消息创建时间。") + + +class ConversationRead(BaseModel): + conversation_id: str = Field(description="会话 ID。") + user_id: str | None = Field(default=None, description="所属用户 ID。") + source: str | None = Field(default=None, description="来源。") + entry_source: str | None = Field(default=None, description="入口来源。") + title: str | None = Field(default=None, description="会话标题。") + last_run_id: str | None = Field(default=None, description="最近一次运行 ID。") + last_scenario: str | None = Field(default=None, description="最近场景。") + last_intent: str | None = Field(default=None, description="最近意图。") + draft_claim_id: str | None = Field(default=None, description="关联草稿单 ID。") + state_json: dict[str, Any] = Field(default_factory=dict, description="会话状态。") + message_count: int = Field(default=0, ge=0, description="消息数量。") + updated_at: datetime | None = Field(default=None, description="更新时间。") + messages: list[ConversationMessageRead] = Field(default_factory=list, description="历史消息。") + + +class ConversationLookupResponse(BaseModel): + found: bool = Field(default=False, description="是否找到可恢复会话。") + conversation: ConversationRead | None = Field(default=None, description="会话详情。") + + +class ConversationDeleteResponse(BaseModel): + deleted_count: int = Field(default=0, ge=0, description="删除的会话数量。") diff --git a/server/src/app/schemas/settings.py b/server/src/app/schemas/settings.py index a68283b..c1d324a 100644 --- a/server/src/app/schemas/settings.py +++ b/server/src/app/schemas/settings.py @@ -41,6 +41,10 @@ class SettingsAdminForm(BaseModel): return value.strip() +class SettingsSessionForm(BaseModel): + conversationRetentionDays: int = Field(default=3, ge=1, le=10) + + class SettingsLlmForm(BaseModel): mainProvider: str = Field(min_length=1, max_length=64) mainModel: str = Field(min_length=1, max_length=255) @@ -159,6 +163,7 @@ class SettingsMailForm(BaseModel): class SettingsRead(BaseModel): companyForm: SettingsCompanyForm adminForm: SettingsAdminForm + sessionForm: SettingsSessionForm llmForm: SettingsLlmForm renderForm: SettingsRenderForm logForm: SettingsLogForm @@ -168,6 +173,7 @@ class SettingsRead(BaseModel): class SettingsWrite(BaseModel): companyForm: SettingsCompanyForm adminForm: SettingsAdminForm + sessionForm: SettingsSessionForm llmForm: SettingsLlmForm renderForm: SettingsRenderForm logForm: SettingsLogForm diff --git a/server/src/app/schemas/user_agent.py b/server/src/app/schemas/user_agent.py index a0d17ad..37b4b50 100644 --- a/server/src/app/schemas/user_agent.py +++ b/server/src/app/schemas/user_agent.py @@ -34,6 +34,89 @@ class UserAgentDraftPayload(BaseModel): status: str | None = Field(default=None, description="当前报销草稿状态。") +class UserAgentReviewRiskBrief(BaseModel): + title: str = Field(description="风险或注意事项标题。") + level: str = Field(default="info", description="级别,例如 info / warning / high。") + content: str = Field(description="面向用户展示的摘要说明。") + + +class UserAgentReviewSlotCard(BaseModel): + key: str = Field(description="槽位键名。") + label: str = Field(description="槽位展示名。") + value: str = Field(default="", description="当前识别值。") + source: str = Field(default="system", description="字段来源,例如 user_text / ocr / page_context。") + confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="识别置信度。") + required: bool = Field(default=True, description="是否为关键字段。") + confirmed: bool = Field(default=False, description="是否可视为已确认。") + status: str = Field(default="identified", description="identified / inferred / missing。") + hint: str = Field(default="", description="字段补充提示。") + + +class UserAgentReviewDocumentField(BaseModel): + label: str = Field(description="字段名。") + value: str = Field(default="", description="字段值。") + source: str = Field(default="ocr", description="字段来源。") + + +class UserAgentReviewDocumentCard(BaseModel): + index: int = Field(description="票据顺序号,从 1 开始。") + filename: str = Field(description="原始文件名。") + document_type: str = Field(default="other", description="票据候选类型。") + suggested_expense_type: str = Field(default="other", description="建议归属费用类型。") + scene_label: str = Field(default="", description="面向用户展示的场景标签。") + summary: str = Field(default="", description="逐票据摘要。") + avg_score: float = Field(default=0.0, ge=0.0, le=1.0, description="OCR 平均得分。") + warnings: list[str] = Field(default_factory=list, description="该票据的识别提示。") + fields: list[UserAgentReviewDocumentField] = Field( + default_factory=list, + description="逐票据关键字段。", + ) + + +class UserAgentReviewClaimGroup(BaseModel): + group_code: str = Field(description="候选报销组编码。") + title: str = Field(description="候选报销组标题。") + expense_type: str = Field(description="归属费用类型编码。") + scene_label: str = Field(description="归属费用类型名称。") + document_indexes: list[int] = Field(default_factory=list, description="挂入该组的票据序号。") + amount_total: float = Field(default=0.0, ge=0.0, description="该组候选金额。") + rationale: str = Field(default="", description="为什么建议这样分组。") + + +class UserAgentReviewAction(BaseModel): + label: str = Field(description="动作名称。") + action_type: str = Field(description="动作类型。") + description: str = Field(default="", description="动作说明。") + emphasis: str = Field(default="secondary", description="primary / secondary / warning。") + + +class UserAgentReviewPayload(BaseModel): + intent_summary: str = Field(description="系统对本次报销意图的结构化摘要。") + scenario: str = Field(description="当前场景。") + intent: str = Field(description="当前意图。") + missing_slots: list[str] = Field(default_factory=list, description="当前仍缺失的关键槽位。") + risk_briefs: list[UserAgentReviewRiskBrief] = Field( + default_factory=list, + description="历史风险和当前注意事项。", + ) + slot_cards: list[UserAgentReviewSlotCard] = Field( + default_factory=list, + description="待确认槽位卡片。", + ) + document_cards: list[UserAgentReviewDocumentCard] = Field( + default_factory=list, + description="逐票据识别卡片。", + ) + claim_groups: list[UserAgentReviewClaimGroup] = Field( + default_factory=list, + description="候选报销分单建议。", + ) + confirmation_actions: list[UserAgentReviewAction] = Field( + default_factory=list, + description="面向前端渲染的确认动作卡片。", + ) + + class UserAgentRequest(BaseModel): run_id: str = Field(description="关联的 AgentRun.run_id。") user_id: str | None = Field(default=None, description="当前请求用户 ID。") @@ -57,5 +140,9 @@ class UserAgentResponse(BaseModel): description="建议的下一步动作。", ) draft_payload: UserAgentDraftPayload | None = Field(default=None, description="可选草稿内容。") + review_payload: UserAgentReviewPayload | None = Field( + default=None, + description="结构化预审结果,用于前端确认面板。", + ) risk_flags: list[str] = Field(default_factory=list, description="本次回答关联的风险标签。") requires_confirmation: bool = Field(default=False, description="是否需要人工确认。")