feat: 报销预审会话状态管理与工作台交互增强

- 新增差旅报销会话状态管理与对话模型重构
- 增强风险观测服务与运行时聊天上下文作用域
- 优化工作台图标资源、助理意图识别与摘要工具
- 完善报销创建视图样式与差旅详情页标准调整交互
- 补充风险观测、运行时聊天与报销端点测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-04 11:03:29 +08:00
parent 87da5df91b
commit 1cbf3fee44
60 changed files with 4156 additions and 393 deletions

View File

@@ -0,0 +1,220 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import date
from typing import Any
from app.schemas.steward import StewardPlanRequest
from app.services.ontology_field_registry import normalize_ontology_form_values
from app.services.runtime_chat import RuntimeChatService
STEWARD_INTENT_FUNCTION_NAME = "submit_steward_intent_plan"
@dataclass(frozen=True, slots=True)
class StewardIntentAgentResult:
payload: dict[str, Any]
model_call_traces: list[dict[str, Any]]
class StewardIntentAgent:
"""使用大模型 function calling 识别小财管家的复合财务意图。"""
def __init__(self, runtime_chat_service: RuntimeChatService) -> None:
self.runtime_chat_service = runtime_chat_service
self.last_call_traces: list[dict[str, Any]] = []
def detect(
self,
request: StewardPlanRequest,
*,
base_date: date,
canonical_fields: list[str],
) -> StewardIntentAgentResult | None:
result = self.runtime_chat_service.complete_with_tool_call(
self._build_messages(request, base_date=base_date, canonical_fields=canonical_fields),
tools=[self._build_intent_tool_schema(canonical_fields)],
tool_choice={
"type": "function",
"function": {"name": STEWARD_INTENT_FUNCTION_NAME},
},
max_tokens=1800,
temperature=0.1,
timeout_seconds=18,
max_attempts=1,
)
self.last_call_traces = result.calls_as_dicts()
if result.tool_call is None or result.tool_call.name != STEWARD_INTENT_FUNCTION_NAME:
return None
return StewardIntentAgentResult(
payload=result.tool_call.arguments,
model_call_traces=self.last_call_traces,
)
@staticmethod
def _build_messages(
request: StewardPlanRequest,
*,
base_date: date,
canonical_fields: list[str],
) -> list[dict[str, Any]]:
context_payload = {
"message": request.message,
"base_date": base_date.isoformat(),
"client_now_iso": request.client_now_iso,
"user_id": request.user_id,
"canonical_ontology_fields": canonical_fields,
"review_form_values": normalize_ontology_form_values(
request.context_json.get("review_form_values")
),
"context_json": {
key: value
for key, value in request.context_json.items()
if key
in {
"entry_source",
"session_type",
"role_codes",
"username",
"name",
"department_name",
"employee_grade",
"employee_no",
"client_timezone_offset_minutes",
}
},
"attachments": [
{
"index": index + 1,
"name": item.name,
"media_type": item.media_type,
"ocr_summary": item.ocr_summary,
"ocr_fields": item.ocr_fields,
}
for index, item in enumerate(request.attachments)
if item.name
],
}
return [
{
"role": "system",
"content": (
"你是 X-Financial 的小财管家意图识别智能体。"
"你必须通过 function calling 输出结构化计划,不能只返回普通文本。"
"当前版本只支持 expense_application 和 reimbursement 两类任务;"
"你只做识别、拆解、归集和确认点规划,不能执行入库、绑定附件或提交审批。"
"所有 ontology_fields 只能使用调用方给出的 canonical_ontology_fields"
"如果输入里出现 occurred_date、transport_type、reason_value 等别名,必须映射为 canonical 字段。"
"相对日期必须以 base_date 为准转换为明确日期。"
"thinking_events 只能是面向用户的过程摘要,不能暴露内部推理链。"
),
},
{
"role": "user",
"content": json.dumps(context_payload, ensure_ascii=False),
},
]
@staticmethod
def _build_intent_tool_schema(canonical_fields: list[str]) -> dict[str, Any]:
return {
"type": "function",
"function": {
"name": STEWARD_INTENT_FUNCTION_NAME,
"description": "提交小财管家的复合财务意图识别结果。",
"parameters": {
"type": "object",
"properties": {
"thinking_events": {
"type": "array",
"items": {
"type": "object",
"properties": {
"stage": {"type": "string"},
"title": {"type": "string"},
"content": {"type": "string"},
},
"required": ["stage", "title", "content"],
},
},
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"task_type": {
"type": "string",
"enum": ["expense_application", "reimbursement"],
},
"title": {"type": "string"},
"summary": {"type": "string"},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
},
"ontology_fields": {
"type": "object",
"additionalProperties": {"type": "string"},
},
"missing_fields": {
"type": "array",
"items": {
"type": "string",
"enum": canonical_fields,
},
},
},
"required": [
"task_type",
"title",
"summary",
"confidence",
"ontology_fields",
"missing_fields",
],
},
},
"attachment_groups": {
"type": "array",
"items": {
"type": "object",
"properties": {
"target_task_index": {
"type": "integer",
"minimum": 1,
},
"scene": {"type": "string"},
"scene_label": {"type": "string"},
"attachment_names": {
"type": "array",
"items": {"type": "string"},
},
"excluded_attachment_names": {
"type": "array",
"items": {"type": "string"},
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
},
"rationale": {"type": "string"},
},
"required": [
"scene",
"scene_label",
"attachment_names",
"excluded_attachment_names",
"confidence",
"rationale",
],
},
},
},
"required": ["thinking_events", "tasks", "attachment_groups"],
},
},
}