Files
ars-backend/docs/ChatSession/chat-session-api.md
ROOG 59d4831f00 main: 增强工具调用与消息流程
- 支持 tool.call 和 tool.result 消息类型处理
- 引入 Tool 调度与执行逻辑,支持超时与结果截断
- 增加 ToolRegistry 和 ToolExecutor 管理工具定义与执行
- 更新上下文构建与消息映射逻辑,适配工具闭环处理
- 扩展配置与环境变量,支持 Tool 调用相关选项
- 增强单元测试覆盖工具调用与执行情景
- 更新文档和 OpenAPI,新增工具相关说明与模型定义
2025-12-22 12:36:59 +08:00

228 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ChatSession & Message APIMVP-1 + Agent Run MVP-0
基地址:`http://localhost:8000/api`FrankenPHP 容器 8000 端口)
认证方式JWT`Authorization: Bearer {token}`
自然语言:中文
## 变更记录
- 2025-02-14新增 ChatSession 创建、消息追加、增量查询接口;支持状态门禁与 dedupe 幂等。
- 2025-02-14MVP-1.1 增加会话列表、会话更新(重命名/状态变更),列表附带最后一条消息摘要。
- 2025-02-15Agent Run MVP-0 —— RunDispatcher + AgentRunJob + DummyProvider自动在 user.prompt 后触发一次 Run落地 run.status / agent.message。
- 2025-12-18Agent Run 可靠性增强 —— 并发幂等、终态去重、取消语义加强、Provider 超时/重试/错误归一SSE gap 回补与心跳。
- 2025-12-19AgentProvider Streaming 接入 —— ProviderEvent 统一事件流,新增 message.delta 输出与 OpenAI-compatible 适配器。
- 2025-12-21Tool 子 Run 模式 —— Provider 支持 tool.delta→tool.call父 Run 调度子 Run 执行工具并写入 tool.result。
## 本次变更摘要2025-12-21
- RunDispatcher 并发幂等:同 trigger_message_id 只产生一个 RUNNING且仅新建时 dispatch。
- RunLoop/OutputSink 幂等agent.message、run.status、tool.call、tool.result 均采用 dedupe_key。
- Cancel 强化:多检查点取消,确保不落 agent.message 且落 CANCELED 终态;父 Run 取消会终止等待的子 Run。
- Provider 可靠性:超时/重试/429/5xx错误落库包含 retryable/http_status/provider/latency_ms。
- StreamingAgentProvider 产出 message.delta / tool.delta / donefinish_reason=tool_calls 会触发子 Run 执行工具。
- 工具闭环tool.callrole=AGENT落库→子 Run 调度→tool.resultrole=TOOL回灌→进入下一轮 LLM。
## 领域模型
- `ChatSession``session_id`(UUID)、`session_name``status`(`OPEN`/`LOCKED`/`CLOSED`)、`last_seq`
- `Message``message_id`(UUID)、`session_id``role`(`USER`/`AGENT`/`TOOL`/`SYSTEM`)、`type`(字符串)、`content``payload`(json)、`seq`(会话内递增)、`reply_to`(UUID)、`dedupe_key`
- 幂等:`UNIQUE (session_id, dedupe_key)`;同一 dedupe_key 返回已有消息。
- 状态门禁:`CLOSED` 禁止追加,例外 `role=SYSTEM && type in [run.status, error]``LOCKED` 禁止 `role=USER && type=user.prompt`
- 会话缓存:`chat_sessions.last_message_id` 记录最后一条消息;`appendMessage` 事务内同步更新 `last_seq``last_message_id``updated_at`
- 工具消息:`tool.call`role=AGENT携带 tool_call_id/name/arguments`tool.result`role=TOOL携带 parent_run_id/run_id/status/result
## 接口
### 创建会话
- `POST /sessions`
- 请求体字段
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| session_name | 否 | string(≤255) | 会话名称 |
- 响应 201JSON
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| session_id | uuid | 主键 |
| session_name | string|null | 会话名 |
| status | enum | `OPEN|LOCKED|CLOSED` |
| last_seq | int | 当前最大 seq |
| last_message_id | uuid|null | 最后一条消息 |
| created_at, updated_at | datetime | 时间戳 |
- 错误401 未授权
### 追加消息
- `POST /sessions/{session_id}/messages`
- 请求体字段
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| role | 是 | enum | `USER|AGENT|TOOL|SYSTEM` |
| type | 是 | string(≤64) | 如 `user.prompt`/`agent.message`/`message.delta` 等 |
| content | 否 | string | 文本内容 |
| payload | 否 | object | jsonb 结构 |
| reply_to | 否 | uuid | 引用消息 |
| dedupe_key | 否 | string(≤128) | 幂等键 |
- 响应 201JSON
字段:`message_id, session_id, seq, role, type, content, payload, reply_to, dedupe_key, created_at`
- 幂等:同 session + dedupe_key 返回已存在的消息(同 `message_id/seq`)。
- 错误401 未授权403 违反状态门禁CLOSED 禁止LOCKED 禁止 user.prompt404 session 不存在422 校验失败。
### 按序增量查询
- `GET /sessions/{session_id}/messages?after_seq=0&limit=50`
- 查询参数
| 参数 | 默认 | 类型 | 说明 |
| --- | --- | --- | --- |
| after_seq | 0 | int | 仅返回 seq 大于该值 |
| limit | 50 | int(≤200) | 返回数量上限 |
- 响应 200`data` 数组,元素字段同“追加消息”响应。
- 错误401/404/422
### 会话列表
- `GET /sessions?page=1&per_page=15&status=OPEN&q=keyword`
- 查询参数
| 参数 | 默认 | 类型 | 说明 |
| --- | --- | --- | --- |
| page | 1 | int | 分页页码 |
| per_page | 15 | int(≤100) | 分页大小 |
| status | - | enum | 过滤 `OPEN|LOCKED|CLOSED` |
| q | - | string | ILIKE 模糊匹配 session_name |
- 响应 200分页结构`data/links/meta``data` 每项字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| session_id | uuid | 会话主键 |
| session_name | string|null | 名称 |
| status | enum | `OPEN|LOCKED|CLOSED` |
| last_seq | int | 当前最大 seq |
| last_message_id | uuid|null | 最后一条消息 |
| last_message_at | datetime|null | 最后一条消息时间 |
| last_message_preview | string | content 截断 120空内容返回空字符串 |
| last_message_role | string|null | 最后消息角色 |
| last_message_type | string|null | 最后消息类型 |
| created_at, updated_at | datetime | 时间戳 |
- 排序:`updated_at` DESC
- 错误401/422
### 会话更新
- `PATCH /sessions/{session_id}`
请求体(至少一项,否则 422
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| session_name | 否 | string 1..255 | 自动 trim |
| status | 否 | enum | `OPEN|LOCKED|CLOSED` |
- 规则:
- `CLOSED` 不可改回 `OPEN`(返回 403
- 任意更新都会刷新 `updated_at`
- 响应 200字段同“会话列表”项。
- 错误401 未授权403 状态门禁404 session 不存在422 校验失败。
### 获取会话详情
- `GET /sessions/{session_id}`
- 响应 200字段同“会话列表”项。
- 错误401 未授权404 session 不存在。
### 归档会话Archive
- `POST /sessions/{session_id}/archive`
- 行为:将 `status` 置为 `CLOSED`,更新 `updated_at`,幂等(重复归档返回当前状态)。
- 响应 200字段同“会话列表”项status=CLOSED
- 错误401 未授权404 session 不存在。
### 获取单条消息(带会话校验)
- `GET /sessions/{session_id}/messages/{message_id}`
- 行为:校验 `message.session_id` 与路径参数一致,否则 404。
- 响应 200字段同“追加消息”响应。
- 错误401 未授权404 不存在或不属于该会话。
### SSE 实时增量
- `GET /sessions/{session_id}/sse?after_seq=123`
- 头部:`Accept: text/event-stream`,可带 `Last-Event-ID`(优先于 query用于断线续传。
- 查询参数
| 参数 | 默认 | 类型 | 说明 |
| --- | --- | --- | --- |
| after_seq | 0 | int | backlog 起始 seq若有 Last-Event-ID 则覆盖) |
| limit | 200 | int(≤500) | backlog 最多条数 |
- SSE 输出格式:
```
id: {seq}
event: message
data: {...message json...}
```
- `id` 为消息 `seq`,便于续传;`data` 为消息 JSON同追加消息响应字段
- Backlog建立连接后先补发 `seq > after_seq` 的消息order asc最多 `limit` 条),再进入实时订阅。
- 实时Redis channel `session:{session_id}:messages` 发布消息 IDSSE 侧读取后按 seq 去重、推送。
- Gap 回补:若订阅推送的 seq 与 last_sent_seq 存在缺口,会主动回补 backlog。
- 心跳:周期输出 `: ping` 保活。
- 错误401 未授权404 session 不存在。
## Agent Run MVP-0RunDispatcher + AgentRunJob
### 流程概述
1. 用户追加 `role=USER && type=user.prompt` 后Controller 自动调用 `RunDispatcher->dispatchForPrompt`。
2. 并发保护:同会话只允许一个 RUNNING同一个 `trigger_message_id` 幂等复用已有 `run_id`。
3. 立即写入 `run.status`SYSTEM/run.statuspayload `{run_id,status:'RUNNING',trigger_message_id}`dedupe_key=`run:trigger:{message_id}`)。
4. 仅在新建 RUNNING 时推送 `AgentRunJob(session_id, run_id)` 到队列(测试环境 QUEUE=sync 会同步执行)。
5. RunLoop默认 HttpAgentProvider未配置 endpoint 时回退 DummyAgentProvider
- 终态检测:若已 DONE/FAILED/CANCELED 则直接返回。
- Cancel 检查:存在 `run.cancel.request`(payload.run_id) 则写入 `run.status=CANCELED`,不产出 agent.message。
- ContextBuilder提取最近 20 条 USER/AGENT 消息type in user.prompt/agent.messageseq 升序提供给 Provider。
- Provider 以 Streaming 事件流产出文本增量message.delta
- OutputSink 持续写入 `message.delta`,最终写入 `agent.message`payload 含 run_id, providerdedupe_key=`run:{run_id}:agent:message`)与 `run.status=DONE`dedupe_key=`run:{run_id}:status:DONE`)。
6. 异常ProviderException 写入 `error` + `run.status=FAILED`dedupeerror payload 包含 retryable/http_status/provider/latency_ms。
### Run 相关消息类型(落库即真相源)
| type | role | payload 关键字段 | 说明 |
| --- | --- | --- | --- |
| run.status | SYSTEM | run_id, status(RUNNING/DONE/CANCELED/FAILED), trigger_message_id?, error? | Run 生命周期事件CLOSED 状态下允许写入 |
| agent.message | AGENT | run_id, provider | Provider 的一次性回复 |
| message.delta | AGENT | run_id, delta_index | Provider 的增量输出Streaming |
| run.cancel.request | USER/SYSTEM | run_id | CancelChecker 依据该事件判断是否中止 |
| error | SYSTEM | run_id, message, retryable?, http_status?, provider?, latency_ms?, raw_message? | 任务异常时落库 |
### 触发 Run调试入口
- `POST /sessions/{session_id}/runs`
- 请求体字段
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| trigger_message_id | 是 | uuid | 通常为 `user.prompt` 消息 ID |
- 行为:同 `trigger_message_id` 幂等;若已有 RUNNING 则复用其 run_id。
- 响应 201`{ run_id }`
- 错误401 未授权404 session 不存在或 trigger_message 不属于该 session。
## cURL 示例
```bash
# 创建会话
SESSION_ID=$(curl -s -X POST http://localhost:8000/api/sessions \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"session_name":"Demo"}' | jq -r '.data.session_id')
# 追加消息(支持 dedupe_key 幂等)
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"role":"USER","type":"user.prompt","content":"hello","dedupe_key":"k1"}'
# 增量查询
curl -s "http://localhost:8000/api/sessions/$SESSION_ID/messages?after_seq=0&limit=50" \
-H "Authorization: Bearer $TOKEN"
# 归档
curl -X POST http://localhost:8000/api/sessions/$SESSION_ID/archive \
-H "Authorization: Bearer $TOKEN"
# 获取单条消息
curl -s http://localhost:8000/api/sessions/$SESSION_ID/messages/{message_id} \
-H "Authorization: Bearer $TOKEN"
# SSE断线续传可带 Last-Event-ID
curl -N http://localhost:8000/api/sessions/$SESSION_ID/sse?after_seq=10 \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"
# 手动触发 Run调试用实际 user.prompt 会自动触发)
curl -s -X POST http://localhost:8000/api/sessions/$SESSION_ID/runs \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"trigger_message_id":"'$MESSAGE_ID'"}'
```