# 小财管家本体 JSON 流程

## 功能一句话

用大模型作为小财管家的主意图识别器，将用户连续对话转换为受本体字段约束的业务 JSON，并在申请和报销意图不确定时先进入用户确认，而不是用固定规则直接判定。

## 背景与问题

当前小财管家已经具备任务规划、部分运行时状态和申请/报销委派能力，但仍有两个关键缺口：

- 意图识别仍带有较强规则假设。例如“2月20-23日去上海出差辅助国网仿生产环境部署”这类话术，在没有“申请”或“报销”动词时，系统不能仅凭规则直接判定为申请。
- 跨轮对话需要一个贯穿流程的结构化 JSON。该 JSON 必须只承载本体 canonical field，不能由前端、规则或大模型临时发明业务字段。

因此，本轮目标不是重写整个小财管家，而是在现有 `steward` 体系上补齐“LLM 主识别 + 本体 JSON 模板 + 待确认流程 + 上下文记忆”的闭环。

## 目标与非目标

### 目标

- 用大模型 function calling 作为主路径识别用户意图。
- 模型输出必须落到统一业务 JSON 模板，字段来源必须来自本体字段注册表。
- 支持 `travel_application` 和 `travel_reimbursement` 两个业务流程。
- 当用户话术无法确定是申请还是报销时，返回 `pending_flow_confirmation`，由前端展示两个明确选项。
- 跨轮对话持续携带并合并 `steward_state`，直到用户完成、取消或切换业务。
- 规则只做兜底，且响应必须标记 `rule_fallback`，不能伪装成模型判断。
- 用户可见回复使用 Markdown 块结构，重点信息加粗，避免密集换行。

### 非目标

- 本轮不引入 LangChain 或 LangGraph。
- 本轮不迁移申请助手、报销助手和 Orchestrator 的既有核心逻辑。
- 本轮不让大模型直接创建申请单、保存草稿、绑定附件或提交审批。
- 本轮不新增脱离本体字段体系的新业务字段。
- 本轮不改造所有财务场景，只先覆盖出差申请和差旅/费用报销。

## 用户与场景

- 普通员工：在首页或小财管家对话框中说“2月20-23日去上海出差辅助国网仿生产环境部署”。
- 小财管家：先判断该话术包含出差、时间、地点和事由，但缺少“申请还是报销”的明确动作。
- 用户：点击“补办出差申请”或“发起费用报销”。
- 系统：将用户选择写入同一个业务 JSON，并继续用对应流程追问缺字段、生成核对结果或委派现有助手。

示例预期：

```markdown
我识别到你描述的是一次 **上海出差事项**，时间为 **2月20日至2月23日**，事由是 **辅助国网仿生产环境部署**。

但当前还不能确定你要做哪一件事：

1. **补办出差申请**
2. **发起费用报销**

请先选择一个方向，我会继续整理对应材料。
```

## 功能能力

### 输入

- 用户自然语言 `message`。
- 当前时间 `client_now_iso`，用于解析相对日期。
- 附件元信息和 OCR 摘要。
- 当前 `conversation_id`。
- 已持久化 `steward_state`。
- ontology canonical fields 列表。

### 输出

- `steward_state`：贯穿对话的业务 JSON。
- `intent_result`：本轮模型或兜底规则的识别结果。
- `candidate_flows`：存在歧义时的候选流程。
- `next_action`：下一步动作，例如追问、确认流程、渲染申请预览、渲染报销预审。
- `markdown_reply`：面向用户的 Markdown 回复。

### 状态边界

业务 JSON 必须区分业务字段和编排字段：

- 业务字段只允许出现在 `flows.<flow_id>.fields`。
- 业务字段 key 必须是 canonical ontology field。
- 编排字段只能出现在 `active_flow`、`pending_flow_confirmation`、`events`、`status` 等结构里。
- 规则或模型返回的别名字段必须先归一化，例如 `occurred_date -> time_range`、`transport_type -> transport_mode`、`reason_value -> reason`。

### 安全边界

- 保存草稿、创建申请单、提交审批、删除或绑定附件必须等待用户确认。
- LLM 只能产出结构化建议，不直接执行副作用操作。
- 如果模型返回非法字段、非法流程或非法动作，服务端丢弃非法部分并进入保守兜底。

## 业务 JSON 模板

目标模板如下：

```json
{
  "version": "steward.flow_state.v2",
  "active_flow": "",
  "pending_flow_confirmation": {
    "status": "none",
    "source_message": "",
    "reason": "",
    "candidate_flows": []
  },
  "flows": {
    "travel_application": {
      "flow_id": "travel_application",
      "intent": "travel_application_create",
      "status": "idle",
      "fields": {},
      "missing_fields": [],
      "confidence": 0,
      "evidence": []
    },
    "travel_reimbursement": {
      "flow_id": "travel_reimbursement",
      "intent": "travel_reimbursement_draft",
      "status": "idle",
      "fields": {},
      "missing_fields": [],
      "linked_application_claim_id": "",
      "attachments": [],
      "confidence": 0,
      "evidence": []
    }
  },
  "events": []
}
```

候选流程结构：

```json
{
  "flow_id": "travel_application",
  "label": "补办出差申请",
  "confidence": 0.52,
  "reason": "用户描述了出差时间、地点和事由，但没有明确要求报销或提交申请。"
}
```

## 方案设计

### 后端

新增或扩展以下职责：

- `schemas/steward.py`：增加 v2 JSON 状态、候选流程、待确认流程和意图识别响应模型。
- `services/steward_intent_agent.py`：扩展 function schema，允许模型返回 `pending_flow_confirmation` 和 `candidate_flows`。
- `services/steward_model_plan_builder.py`：校验模型输出，只保留合法 flow、合法 action 和 canonical ontology fields。
- `services/steward_flow_state.py`：支持 v1 到 v2 状态兼容、字段 patch 合并、候选流程落态和事件追踪。
- `services/steward_runtime_decision_agent.py`：识别用户点击或输入的流程选择，并把选择写回 `active_flow`。
- `api/v1/endpoints/steward.py`：在 `/steward/plans`、`/steward/plans/stream`、`/steward/runtime-decisions` 中统一返回最新 `steward_state`。

### 前端

- `stewardPlanModel.js`：将 `pending_flow_confirmation` 转为可点击操作。
- `TravelReimbursementCreateView.js`：用户点击候选流程后，优先走 runtime decision，不重新把原句当新任务规划。
- `useStewardPlanFlow.js`：渲染 Markdown 回复和候选流程操作。
- `useTravelReimbursementSessionState.js`：持续保存并传回 `conversation_id` 和 `steward_state`。

### 数据与持久化

- 复用 `AgentConversation.state_json` 持久化 `steward_state`。
- 不新增数据库表。
- 不改变申请单、报销单现有表结构。

### 接口契约

`POST /steward/plans` 和流式计划接口返回：

```json
{
  "planning_source": "llm_function_call",
  "conversation_id": "conv_xxx",
  "steward_state": {},
  "next_action": "confirm_flow",
  "candidate_flows": [],
  "summary": "Markdown 文本"
}
```

运行时确认接口返回：

```json
{
  "decision_source": "llm_function_call",
  "next_action": "continue_selected_flow",
  "steward_state": {},
  "response_text": "Markdown 文本"
}
```

## 算法与公式

主路径不使用关键词打分决定最终意图，而是由 LLM function calling 返回结构化候选结果。

规则兜底仅在模型不可用、超时或结构非法时使用。兜底置信度用于决定是否直接进入候选确认：

$$
confidence(flow) = 0.35t + 0.25l + 0.25v + 0.15a
$$

变量定义：

- `t`：时间线索得分，出现明确日期、日期区间或相对日期时取 1，否则取 0。
- `l`：地点线索得分，出现城市、客户地点或项目地点时取 1，否则取 0。
- `v`：动作线索得分，出现申请、报销、提交、保存草稿等动作词时取 1，否则取 0。
- `a`：附件线索得分，存在票据、发票、行程单、OCR 金额等附件证据时取 1，否则取 0。

当最高候选流程与第二候选流程差值小于阈值时进入确认：

$$
\Delta = confidence(flow_1) - confidence(flow_2) < 0.20
$$

该公式只用于兜底路径，不能覆盖模型主判断。

## 测试方案

### 后端单元测试

- `test_steward_intent_agent.py`：覆盖 function schema 包含 `candidate_flows`、`pending_flow_confirmation`。
- `test_steward_model_plan_builder.py`：覆盖非法字段过滤、别名归一、非法 flow 丢弃。
- `test_steward_flow_state.py`：覆盖 v2 状态合并、候选流程落态、用户选择后 active flow 切换。
- `test_steward_runtime_decision_agent.py`：覆盖用户选择“补办出差申请 / 发起费用报销”。

### 接口测试

- `/steward/plans` 输入“2月20-23日去上海出差辅助国网仿生产环境部署”，返回 `next_action=confirm_flow`。
- `/steward/runtime-decisions` 选择“补办出差申请”后，`active_flow=travel_application`。
- `/steward/runtime-decisions` 选择“发起费用报销”后，`active_flow=travel_reimbursement`。

### 前端测试

- 候选流程按钮只在 `pending_flow_confirmation.status=pending` 时展示。
- 用户点击候选流程后不重复触发新计划。
- Markdown 回复中标题、段落、列表和重点加粗能正确渲染。

### 容器验证

后端测试必须在 Docker 容器内执行：

```bash
docker exec -w /app -e SERVER_VENV_DIR=/tmp/x-financial-server-venv x-financial-main /tmp/x-financial-server-venv/bin/pytest -q server/tests/test_steward_intent_agent.py server/tests/test_steward_model_plan_builder.py server/tests/test_steward_flow_state.py server/tests/test_steward_runtime_decision_agent.py
```

前端构建必须在容器内执行：

```bash
docker exec -w /app/web x-financial-main npm run build
```

单次测试命令最长等待 60 秒，避免任务卡死。

## 指标与验收

- 对“2月20-23日去上海出差辅助国网仿生产环境部署”，系统不再直接判定为申请，而是返回两个候选流程并要求用户确认。
- 用户选择“补办出差申请”后，同一 `conversation_id` 的 `steward_state.active_flow=travel_application`。
- 用户选择“发起费用报销”后，同一 `conversation_id` 的 `steward_state.active_flow=travel_reimbursement`。
- `flows.*.fields` 中不出现非本体字段。
- 模型返回别名字段时，服务端输出仍为 canonical ontology field。
- 模型不可用时，规则兜底结果明确标记 `rule_fallback`。
- 用户未确认前，不创建申请单、不保存报销草稿、不提交审批、不绑定附件。
- 前端候选流程按钮点击后不产生重复消息、不重复规划、不丢失上下文。
- 后端定向测试和前端构建在 `x-financial-main:/app` 通过。

## 风险与开放问题

- 模型供应商对 function calling 的兼容程度不同，需要保留严格的服务端结构校验。
- 旧版 `steward_state.v1` 已有数据需要兼容升级到 v2。
- 用户输入可能同时包含“补申请”和“报销”，这种情况不应进入歧义确认，而应拆成两个任务。
- 过去日期不等于报销，未来日期也不绝对等于申请；最终应由 LLM 主识别，并用候选确认处理低确定性场景。
- 后续如果要支持更多流程，例如审批、制度问答或预算查询，需要先扩展本体业务契约，再扩展本 JSON 模板。
