docs: 更新 Agent 协议文档与部署配置
- 更新 Agent API 端点文档 - 更新 SSE 事件与输入输出文档 - 新增 deploy/.env.prod.example 配置模板
This commit is contained in:
@@ -81,3 +81,15 @@ SOCIAL_LLM__PROVIDER_KEYS__MOONSHOT=
|
|||||||
SOCIAL_LLM__PROVIDER_KEYS__DEEPSEEK=
|
SOCIAL_LLM__PROVIDER_KEYS__DEEPSEEK=
|
||||||
SOCIAL_LLM__PROVIDER_KEYS__ARK=
|
SOCIAL_LLM__PROVIDER_KEYS__ARK=
|
||||||
SOCIAL_LLM__PROVIDER_KEYS__ZAI=
|
SOCIAL_LLM__PROVIDER_KEYS__ZAI=
|
||||||
|
|
||||||
|
############
|
||||||
|
# App 版本更新配置
|
||||||
|
############
|
||||||
|
# 安装包目录,相对于项目根目录下的 deploy/static/
|
||||||
|
SOCIAL_APP_VERSION__RELEASES_DIR=releases
|
||||||
|
# 当前版本号(语义化版本)
|
||||||
|
SOCIAL_APP_VERSION__CURRENT_VERSION=0.1.0
|
||||||
|
# 当前构建号(整数,每次打包递增)
|
||||||
|
SOCIAL_APP_VERSION__CURRENT_BUILD=1
|
||||||
|
# 下载链接基础域名(生产环境需配置)
|
||||||
|
SOCIAL_APP_VERSION__DOWNLOAD_BASE_URL=
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Agent API Endpoints
|
# Agent API Endpoints
|
||||||
|
|
||||||
本文档列出所有 Agent 相关的 API 端点。
|
本文档以当前后端实现为准,描述 `/api/v1/agent` 的 HTTP 接口契约。
|
||||||
|
|
||||||
Base URL: `/api/v1/agent`
|
Base URL: `/api/v1/agent`
|
||||||
|
|
||||||
@@ -8,353 +8,220 @@ Base URL: `/api/v1/agent`
|
|||||||
|
|
||||||
## 端点清单
|
## 端点清单
|
||||||
|
|
||||||
| 方法 | 路径 | 描述 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|---|---|---|
|
||||||
| POST | `/runs` | 发起 Agent 运行 |
|
| POST | `/runs` | 创建一次 agent run(异步入队) |
|
||||||
| GET | `/runs/{thread_id}/events` | SSE 事件流 |
|
| GET | `/runs/{thread_id}/events` | 订阅 SSE 事件流 |
|
||||||
| GET | `/history` | 获取对话历史快照 |
|
| GET | `/history` | 获取历史快照(按天分页) |
|
||||||
| POST | `/attachments` | 上传附件 |
|
| POST | `/attachments` | 上传用户图片附件 |
|
||||||
| GET | `/attachments/signed-url` | 生成附件签名 URL |
|
| GET | `/attachments/signed-url` | 获取附件临时签名链接 |
|
||||||
| POST | `/transcribe` | 语音转文字 (ASR) |
|
| POST | `/transcribe` | WAV 音频转写 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. POST /runs
|
## 1) POST `/runs`
|
||||||
|
|
||||||
发起一个 Agent 运行任务。
|
发起一次运行请求,后端会先持久化用户消息,再将命令放入异步队列。
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
|
|
||||||
Request Body: `RunAgentInput`
|
- Body: `RunAgentInput`
|
||||||
|
- 详细结构见 `docs/protocols/agent/run-agent-input.md`
|
||||||
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
|
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
```typescript
|
`202 Accepted`
|
||||||
|
|
||||||
|
```ts
|
||||||
{
|
{
|
||||||
taskId: string, // 任务 ID
|
taskId: string;
|
||||||
threadId: string, // 会话 ID
|
threadId: string;
|
||||||
runId: string, // 运行 ID
|
runId: string;
|
||||||
created: string // ISO-8601 时间戳
|
created: boolean; // 是否新建了会话
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example
|
### 错误码
|
||||||
|
|
||||||
```bash
|
- `401` 未认证
|
||||||
curl -X POST https://api.example.com/api/v1/agent/runs \
|
- `422` 请求结构校验失败
|
||||||
-H "Content-Type: application/json" \
|
- `429` 超过 run 请求速率限制
|
||||||
-d '{
|
|
||||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"runId": "run-001",
|
|
||||||
"state": {},
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"id": "msg-001",
|
|
||||||
"role": "user",
|
|
||||||
"content": "帮我查一下北京今天的天气"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tools": [],
|
|
||||||
"context": [],
|
|
||||||
"forwardedProps": {}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"taskId": "task-abc123",
|
|
||||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"runId": "run-001",
|
|
||||||
"created": "2026-03-16T10:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. GET /runs/{thread_id}/events
|
## 2) GET `/runs/{thread_id}/events`
|
||||||
|
|
||||||
获取 SSE 事件流,用于实时接收 Agent 运行过程中的事件。
|
订阅指定 thread 的实时事件流。
|
||||||
|
|
||||||
### Path Parameters
|
### Path
|
||||||
|
|
||||||
| 参数 | 类型 | 描述 |
|
| 参数 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|---|---|---|
|
||||||
| thread_id | string | 会话 ID |
|
| `thread_id` | string | 会话 ID |
|
||||||
|
|
||||||
### Query Parameters
|
### Query
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 描述 |
|
| 参数 | 类型 | 默认 | 说明 |
|
||||||
|------|------|--------|------|
|
|---|---|---|---|
|
||||||
| Last-Event-ID | string | - | 可选,用于断点续传的事件 ID |
|
| `idle_limit` | integer | `300` | 最大空闲轮询次数(1-3600) |
|
||||||
| idle_limit | integer | 300 | 最大空闲轮询次数 (1-3600) |
|
|
||||||
|
|
||||||
### Headers
|
### Headers
|
||||||
|
|
||||||
| 参数 | 描述 |
|
| Header | 必填 | 说明 |
|
||||||
|------|------|
|
|---|---|---|
|
||||||
| Last-Event-ID | 可选的事件 ID,用于从指定位置恢复 |
|
| `Accept: text/event-stream` | 否 | 建议显式设置 |
|
||||||
|
| `Last-Event-ID` | 否 | 从指定游标断点续流 |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
SSE (Server-Sent Events) 流,Content-Type: `text/event-stream`
|
- `200 OK`
|
||||||
|
- `Content-Type: text/event-stream`
|
||||||
|
- 事件类型与字段见 `docs/protocols/agent/sse-events.md`
|
||||||
|
- 空闲时会发送 keep-alive 注释行 `: keep-alive`
|
||||||
|
|
||||||
事件类型详情见 [sse-events.md](./sse-events.md)
|
### 错误码
|
||||||
|
|
||||||
### Example
|
- `401` 未认证
|
||||||
|
- `403` 非会话所有者
|
||||||
```javascript
|
- `422` `Last-Event-ID` 非法
|
||||||
const eventSource = new EventSource(
|
- `429` 超过 SSE 连接数限制
|
||||||
'https://api.example.com/api/v1/agent/runs/550e8400-e29b-41d4-a716-446655440000/events'
|
|
||||||
);
|
|
||||||
|
|
||||||
eventSource.addEventListener('run.started', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Started:', data);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('text.delta', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Delta:', data.data.delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('run.finished', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Finished:', data);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. GET /history
|
## 3) GET `/history`
|
||||||
|
|
||||||
获取对话历史快照。
|
返回历史快照(`HistorySnapshotResponse`),不是 SSE 包装事件。
|
||||||
|
|
||||||
### Query Parameters
|
### Query
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 描述 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|---|---|---|---|
|
||||||
| threadId | string | 否 | 会话 ID,不指定则返回最新会话 |
|
| `threadId` | string | 否 | 指定会话,不传则取当前用户最近会话 |
|
||||||
| before | date | 否 | 日期格式 `YYYY-MM-DD`,返回该日期之前的快照 |
|
| `before` | `YYYY-MM-DD` | 否 | 返回该日期之前最近一天的快照 |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
{
|
{
|
||||||
scope: "history_day",
|
scope: "history_day";
|
||||||
threadId: string | null,
|
threadId: string | null;
|
||||||
day: string | null, // ISO date format "YYYY-MM-DD"
|
day: string | null; // YYYY-MM-DD
|
||||||
hasMore: boolean,
|
hasMore: boolean;
|
||||||
messages: HistoryMessage[]
|
messages: Array<{
|
||||||
|
id: string;
|
||||||
|
seq: number;
|
||||||
|
role: "user" | "assistant" | "tool";
|
||||||
|
content: string;
|
||||||
|
url?: string | null; // user 附件签名链接
|
||||||
|
ui_schema?: object | null; // assistant/tool 的编译后 UI
|
||||||
|
timestamp: string; // ISO-8601
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
|
### 说明
|
||||||
|
|
||||||
### Example
|
- 若用户没有任何会话:返回
|
||||||
|
- `threadId = null`
|
||||||
```bash
|
- `day = null`
|
||||||
curl "https://api.example.com/api/v1/agent/history?threadId=550e8400-e29b-41d4-a716-446655440000&before=2026-03-15"
|
- `hasMore = false`
|
||||||
```
|
- `messages = []`
|
||||||
|
|
||||||
### Response Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scope": "history_day",
|
|
||||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"day": "2026-03-15",
|
|
||||||
"hasMore": false,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"id": "msg-001",
|
|
||||||
"seq": 1,
|
|
||||||
"role": "user",
|
|
||||||
"content": "帮我创建一个日程",
|
|
||||||
"url": null,
|
|
||||||
"timestamp": "2026-03-15T10:00:00Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg-002",
|
|
||||||
"seq": 2,
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "好的,我来帮您创建日程。",
|
|
||||||
"uiSchema": {
|
|
||||||
"version": "2.0",
|
|
||||||
"locale": "zh-CN",
|
|
||||||
"status": "success",
|
|
||||||
"theme": "default",
|
|
||||||
"root": {
|
|
||||||
"type": "stack",
|
|
||||||
"appearance": "card",
|
|
||||||
"children": [
|
|
||||||
{"type": "text", "content": "日程已创建", "role": "title"},
|
|
||||||
{"type": "badge", "label": "SUCCESS", "status": "success"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestamp": "2026-03-15T10:00:05Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. POST /attachments
|
## 4) POST `/attachments`
|
||||||
|
|
||||||
上传附件到存储。
|
上传图片附件,返回可直接用于 `RunAgentInput.messages[].content[].url` 的签名链接。
|
||||||
|
|
||||||
### Form Data
|
### Request
|
||||||
|
|
||||||
| 参数 | 类型 | 描述 |
|
- `multipart/form-data`
|
||||||
|------|------|------|
|
|
||||||
| threadId | string | 会话 ID |
|
| 字段 | 类型 | 说明 |
|
||||||
| file | file | 要上传的文件 |
|
|---|---|---|
|
||||||
|
| `threadId` | string | 会话 ID |
|
||||||
|
| `file` | file | 附件文件 |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
{
|
{
|
||||||
attachment: {
|
attachment: {
|
||||||
bucket: string,
|
bucket: string;
|
||||||
path: string,
|
path: string;
|
||||||
mimeType: string,
|
mimeType: string;
|
||||||
size: number,
|
url: string;
|
||||||
url: string // 临时访问 URL
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example
|
### 错误码
|
||||||
|
|
||||||
```bash
|
- `401` 未认证
|
||||||
curl -X POST https://api.example.com/api/v1/agent/attachments \
|
- `403` 非会话所有者
|
||||||
-F "threadId=550e8400-e29b-41d4-a716-446655440000" \
|
- `413` 附件超过大小限制
|
||||||
-F "file=@/path/to/image.png"
|
- `422` 文件类型不支持/空文件等
|
||||||
```
|
- `503` 存储服务不可用
|
||||||
|
|
||||||
### Limits
|
|
||||||
|
|
||||||
- 最大文件大小: 5MB
|
|
||||||
- 支持的文件类型: 见后端配置
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. GET /attachments/signed-url
|
## 5) GET `/attachments/signed-url`
|
||||||
|
|
||||||
生成附件的签名 URL,用于直接访问存储中的文件。
|
对已有存储对象重新签名。
|
||||||
|
|
||||||
### Query Parameters
|
### Query
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 描述 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|---|---|---|---|
|
||||||
| bucket | string | 是 | 存储桶名称 |
|
| `bucket` | string | 是 | 存储桶 |
|
||||||
| path | string | 是 | 文件路径 |
|
| `path` | string | 是 | 对象路径 |
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
{
|
{
|
||||||
bucket: string,
|
bucket: string;
|
||||||
path: string,
|
path: string;
|
||||||
url: string // 签名 URL
|
url: string;
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl "https://api.example.com/api/v1/agent/attachments/signed-url?bucket=agent-inputs&path=user-123/image.png"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"bucket": "agent-inputs",
|
|
||||||
"path": "user-123/image.png",
|
|
||||||
"url": "https://storage.example.com/agent-inputs/user-123/image.png?signature=abc123..."
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. POST /transcribe
|
## 6) POST `/transcribe`
|
||||||
|
|
||||||
语音转文字 (ASR)。
|
WAV 音频转写。
|
||||||
|
|
||||||
### Request
|
### Request
|
||||||
|
|
||||||
Form Data:
|
- `multipart/form-data`
|
||||||
|
|
||||||
| 参数 | 类型 | 描述 |
|
| 字段 | 类型 | 说明 |
|
||||||
|------|------|------|
|
|---|---|---|
|
||||||
| audio | file | 音频文件 (WAV 格式) |
|
| `audio` | file | WAV 文件 |
|
||||||
|
|
||||||
### Headers
|
|
||||||
|
|
||||||
| 参数 | 描述 |
|
|
||||||
|------|------|
|
|
||||||
| content-length | 文件大小 |
|
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
{
|
{
|
||||||
transcript: string, // 转录文本
|
transcript: string;
|
||||||
language: string, // 检测到的语言
|
|
||||||
duration: number // 音频时长 (秒)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example
|
### 限制
|
||||||
|
|
||||||
```bash
|
- 内容类型:`audio/wav` / `audio/x-wav` / `audio/wave`
|
||||||
curl -X POST https://api.example.com/api/v1/agent/transcribe \
|
- 文件大小:最大 10MB
|
||||||
-F "audio=@/path/to/audio.wav"
|
- 速率限制:20 次/分钟/用户
|
||||||
```
|
|
||||||
|
|
||||||
### Response Example
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"transcript": "今天天气真不错",
|
|
||||||
"language": "zh-CN",
|
|
||||||
"duration": 3.5
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limits
|
|
||||||
|
|
||||||
- 支持格式: `audio/wav`, `audio/x-wav`, `audio/wave`
|
|
||||||
- 最大文件大小: 10MB
|
|
||||||
- 速率限制: 20 次/分钟/用户
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 错误响应
|
## 通用错误
|
||||||
|
|
||||||
所有端点可能返回以下错误:
|
当前实现的错误主体为 FastAPI `detail` 字段:
|
||||||
|
|
||||||
| 状态码 | 描述 |
|
|
||||||
|--------|------|
|
|
||||||
| 400 | 请求参数错误 |
|
|
||||||
| 401 | 未认证 |
|
|
||||||
| 403 | 无权限 |
|
|
||||||
| 404 | 资源不存在 |
|
|
||||||
| 413 | 请求体过大 |
|
|
||||||
| 422 | 数据验证失败 |
|
|
||||||
| 429 | 速率限制 |
|
|
||||||
| 500 | 服务器内部错误 |
|
|
||||||
|
|
||||||
### Error Response Format
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "错误详情信息"
|
"detail": "..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ interface TaskAcceptedResponse {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
created: string; // ISO-8601 timestamp
|
created: boolean; // 是否新建会话
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -359,7 +359,7 @@ interface HistoryMessageTool {
|
|||||||
seq: number;
|
seq: number;
|
||||||
role: "tool";
|
role: "tool";
|
||||||
content: string;
|
content: string;
|
||||||
uiSchema: UiSchemaRenderer | null; // 由 tool_agent_output.ui_hints 编译
|
ui_schema: UiSchemaRenderer | null; // 由 tool_agent_output.ui_hints 编译
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ interface HistoryMessageAssistant {
|
|||||||
seq: number;
|
seq: number;
|
||||||
role: "assistant";
|
role: "assistant";
|
||||||
content: string;
|
content: string;
|
||||||
uiSchema: UiSchemaRenderer | null; // 由 worker_agent_output.ui_hints 编译
|
ui_schema: UiSchemaRenderer | null; // 由 worker_agent_output.ui_hints 编译
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ interface UiSchemaRenderer {
|
|||||||
"seq": 2,
|
"seq": 2,
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": "好的,我来帮您创建日程。",
|
"content": "好的,我来帮您创建日程。",
|
||||||
"uiSchema": {
|
"ui_schema": {
|
||||||
"version": "2.0",
|
"version": "2.0",
|
||||||
"locale": "zh-CN",
|
"locale": "zh-CN",
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -447,3 +447,4 @@ interface UiSchemaRenderer {
|
|||||||
- binary content 的 url 必须是有效的 signed URL,由 `/api/v1/agent/attachments` 端点生成
|
- binary content 的 url 必须是有效的 signed URL,由 `/api/v1/agent/attachments` 端点生成
|
||||||
- backend 验证通过后,会将 binary url 转换为内部存储路径
|
- backend 验证通过后,会将 binary url 转换为内部存储路径
|
||||||
- tools 为空数组时,prompt 中不会包含工具说明
|
- tools 为空数组时,prompt 中不会包含工具说明
|
||||||
|
- `RunAgentInput` 同时接受 camelCase 与 snake_case 别名输入(推荐统一使用 camelCase)
|
||||||
|
|||||||
+180
-337
@@ -1,370 +1,213 @@
|
|||||||
# Agent SSE Events
|
# Agent SSE Events
|
||||||
|
|
||||||
本文档记录 Agent Runtime 产生的所有 Server-Sent Events (SSE) 事件,用于前端实时展示。
|
本文档描述 `GET /api/v1/agent/runs/{thread_id}/events` 的事件协议。
|
||||||
|
|
||||||
## 事件流转架构
|
---
|
||||||
|
|
||||||
|
## 1) 事件管道
|
||||||
|
|
||||||
|
后端事件流转如下:
|
||||||
|
|
||||||
|
1. Runtime 直接产出 AG-UI 事件(如 `RUN_STARTED`、`TOOL_CALL_RESULT`)
|
||||||
|
2. `agui_codec` 仅做协议对齐与字段净化(例如移除仅后端内部统计字段)
|
||||||
|
3. 事件同时:
|
||||||
|
- 持久化到数据库(用于 history)
|
||||||
|
- 发布到 Redis Stream(用于 SSE)
|
||||||
|
4. `/runs/{thread_id}/events` 从 Redis Stream 读取并输出 SSE
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) SSE 帧格式
|
||||||
|
|
||||||
|
每条事件遵循标准 SSE:
|
||||||
|
|
||||||
|
```text
|
||||||
|
id: <redis-stream-id>
|
||||||
|
event: <AG-UI-EVENT-TYPE>
|
||||||
|
data: <json>
|
||||||
|
|
||||||
```
|
|
||||||
pipeline.emit()
|
|
||||||
↓
|
|
||||||
AgentScopeEventPipeline.emit()
|
|
||||||
├─→ store.persist() → 持久化到数据库
|
|
||||||
└─→ bus.publish() → 发布到 Redis Stream
|
|
||||||
↓
|
|
||||||
前端通过 GET /runs/{thread_id}/events 读取
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 事件统一格式
|
- `id` 可用于断点续流(`Last-Event-ID`)
|
||||||
|
- `event` 与 JSON 内 `type` 一致(例如 `RUN_STARTED`)
|
||||||
|
- 空闲时可能出现 keep-alive 注释帧:
|
||||||
|
|
||||||
所有事件在 Redis 中传输时都包含以下字段:
|
```text
|
||||||
|
: keep-alive
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: string, // 事件类型
|
|
||||||
threadId: string, // 会话 ID
|
|
||||||
runId: string, // 运行 ID
|
|
||||||
data: object // 事件数据
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Orchestrator 生命周期事件
|
## 3) 事件类型(当前实现)
|
||||||
|
|
||||||
### run.started
|
### 3.1 Run 生命周期
|
||||||
|
|
||||||
Agent 开始运行时触发。
|
#### `RUN_STARTED`
|
||||||
|
|
||||||
```typescript
|
```json
|
||||||
{
|
{
|
||||||
type: "run.started",
|
"type": "RUN_STARTED",
|
||||||
threadId: "xxx",
|
"threadId": "...",
|
||||||
runId: "yyy",
|
"runId": "..."
|
||||||
data: {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### run.finished
|
#### `RUN_FINISHED`
|
||||||
|
|
||||||
Agent 成功完成时触发。
|
```json
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
{
|
||||||
type: "run.finished",
|
"type": "RUN_FINISHED",
|
||||||
threadId: "xxx",
|
"threadId": "...",
|
||||||
runId: "yyy",
|
"runId": "..."
|
||||||
data: {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### run.error
|
#### `RUN_ERROR`
|
||||||
|
|
||||||
Agent 运行出错时触发。
|
```json
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
{
|
||||||
type: "run.error",
|
"type": "RUN_ERROR",
|
||||||
threadId: "xxx",
|
"threadId": "...",
|
||||||
runId: "yyy",
|
"runId": "...",
|
||||||
data: {
|
"message": "runtime execution failed",
|
||||||
message: "runtime execution failed"
|
"code": null
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3.2 阶段事件
|
||||||
|
|
||||||
|
#### `STEP_STARTED`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "STEP_STARTED",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"stepName": "router" | "worker"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `STEP_FINISHED`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "STEP_FINISHED",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"stepName": "router" | "worker"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Tool 事件
|
||||||
|
|
||||||
|
#### `TOOL_CALL_START`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "TOOL_CALL_START",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"messageId": "...",
|
||||||
|
"toolCallId": "...",
|
||||||
|
"toolCallName": "...",
|
||||||
|
"stage": "worker"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `TOOL_CALL_ARGS`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "TOOL_CALL_ARGS",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"messageId": "...",
|
||||||
|
"toolCallId": "...",
|
||||||
|
"toolCallName": "...",
|
||||||
|
"args": {},
|
||||||
|
"stage": "worker"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `TOOL_CALL_END`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "TOOL_CALL_END",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"messageId": "...",
|
||||||
|
"toolCallId": "...",
|
||||||
|
"toolCallName": "...",
|
||||||
|
"stage": "worker"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `TOOL_CALL_RESULT`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "TOOL_CALL_RESULT",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"messageId": "...",
|
||||||
|
"role": "tool",
|
||||||
|
"stage": "worker",
|
||||||
|
"tool_name": "...",
|
||||||
|
"tool_call_id": "...",
|
||||||
|
"tool_call_args": {},
|
||||||
|
"status": "success" | "failed",
|
||||||
|
"result_summary": "...",
|
||||||
|
"ui_schema": {},
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 文本完成事件
|
||||||
|
|
||||||
|
#### `TEXT_MESSAGE_END`
|
||||||
|
|
||||||
|
当前实现仅在 worker 输出完成后发送完整结果,不发送 token delta 事件。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "TEXT_MESSAGE_END",
|
||||||
|
"threadId": "...",
|
||||||
|
"runId": "...",
|
||||||
|
"messageId": "...",
|
||||||
|
"role": "assistant",
|
||||||
|
"stage": "worker",
|
||||||
|
"status": "success" | "partial_success" | "failed",
|
||||||
|
"answer": "...",
|
||||||
|
"key_points": [],
|
||||||
|
"result_type": "execution_report" | "clarification" | "error_report" | "unknown",
|
||||||
|
"suggested_actions": [],
|
||||||
|
"error": null,
|
||||||
|
"ui_schema": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`inputTokens`、`outputTokens`、`cost`、`latencyMs`、`model` 属于后端内部统计字段,不在 SSE 对外协议中暴露。
|
||||||
|
|
||||||
|
### 3.5 快照事件
|
||||||
|
|
||||||
|
编码器支持以下 AG-UI 类型映射:
|
||||||
|
|
||||||
|
- `STATE_SNAPSHOT`
|
||||||
|
- `MESSAGES_SNAPSHOT`
|
||||||
|
|
||||||
|
当前 `/runs/{thread_id}/events` 主流程通常不主动产出这两类事件;历史查询请使用 `/history`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Step 阶段事件
|
## 4) 字段命名约定
|
||||||
|
|
||||||
### step.start
|
- 事件顶层通用字段使用 AG-UI 风格:`type`、`threadId`、`runId`
|
||||||
|
- 部分业务字段沿运行时模型历史命名保留下划线:
|
||||||
|
- `tool_name`
|
||||||
|
- `tool_call_id`
|
||||||
|
- `tool_call_args`
|
||||||
|
- `ui_schema`
|
||||||
|
|
||||||
阶段开始时触发。
|
这部分命名属于当前后端实现约束,文档与实现保持一致。
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "step.start",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
stepName: "router" | "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### step.finish
|
|
||||||
|
|
||||||
阶段结束时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "step.finish",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
stepName: "router" | "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Worker 运行时事件
|
|
||||||
|
|
||||||
### 3.1 文本消息事件
|
|
||||||
|
|
||||||
#### text.start
|
|
||||||
|
|
||||||
文本消息开始时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "text.start",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
role: "assistant",
|
|
||||||
stage: "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### text.delta
|
|
||||||
|
|
||||||
文本内容增量更新时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "text.delta",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
delta: "这是新增的文本内容",
|
|
||||||
stage: "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### text.end
|
|
||||||
|
|
||||||
文本消息结束时触发,包含完整输出和使用统计。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "text.end",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
role: "assistant",
|
|
||||||
stage: "worker",
|
|
||||||
workerAgentOutput: {
|
|
||||||
status: "success" | "partial_success" | "failed",
|
|
||||||
answer: "主回复文本",
|
|
||||||
key_points: ["要点1", "要点2"],
|
|
||||||
result_type: "execution_report" | "clarification" | "error_report" | "unknown",
|
|
||||||
suggested_actions: ["建议操作1"],
|
|
||||||
error: null | { code: string, message: string },
|
|
||||||
ui_hints: { ... } | null // 仅在 ui_mode 非空时存在
|
|
||||||
},
|
|
||||||
model: "gpt-4o",
|
|
||||||
inputTokens: 1000,
|
|
||||||
outputTokens: 500,
|
|
||||||
cost: 0.025,
|
|
||||||
latencyMs: 1500
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**workerAgentOutput 详细结构:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// WorkerAgentOutputLite
|
|
||||||
{
|
|
||||||
status: "success" | "partial_success" | "failed",
|
|
||||||
answer: string,
|
|
||||||
key_points: string[],
|
|
||||||
result_type: "execution_report" | "clarification" | "error_report" | "unknown",
|
|
||||||
suggested_actions: string[],
|
|
||||||
error: { code: string, message: string } | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// WorkerAgentOutputRich (当 ui.ui_mode 非空时)
|
|
||||||
{
|
|
||||||
// ... WorkerAgentOutputLite 字段
|
|
||||||
ui_hints: {
|
|
||||||
ui_mode: string,
|
|
||||||
ui_state: object,
|
|
||||||
actions: UiHintAction[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.2 工具调用事件
|
|
||||||
|
|
||||||
#### tool.start
|
|
||||||
|
|
||||||
工具调用开始时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "tool.start",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
toolCallId: "call-abc",
|
|
||||||
toolName: "calendar_read",
|
|
||||||
stage: "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### tool.args
|
|
||||||
|
|
||||||
工具调用参数时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "tool.args",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
toolCallId: "call-abc",
|
|
||||||
toolName: "calendar_read",
|
|
||||||
args: { start_date: "2024-01-01", end_date: "2024-01-07" },
|
|
||||||
stage: "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### tool.end
|
|
||||||
|
|
||||||
工具调用结束时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "tool.end",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
toolCallId: "call-abc",
|
|
||||||
toolName: "calendar_read",
|
|
||||||
stage: "worker"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### tool.result
|
|
||||||
|
|
||||||
工具执行结果时触发。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
type: "tool.result",
|
|
||||||
threadId: "xxx",
|
|
||||||
runId: "yyy",
|
|
||||||
data: {
|
|
||||||
messageId: "msg-xxx",
|
|
||||||
toolCallId: "call-abc",
|
|
||||||
toolName: "calendar_read",
|
|
||||||
stage: "worker",
|
|
||||||
toolAgentOutput: {
|
|
||||||
tool_name: "calendar_read",
|
|
||||||
tool_call_id: "call-abc",
|
|
||||||
tool_call_args: { start_date: "2024-01-01", end_date: "2024-01-07" },
|
|
||||||
status: "success" | "failed",
|
|
||||||
result_summary: "找到3个事件...",
|
|
||||||
ui_hints: null,
|
|
||||||
attachments: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**toolAgentOutput 详细结构:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
tool_name: string,
|
|
||||||
tool_call_id: string,
|
|
||||||
tool_call_args: object | null,
|
|
||||||
status: "success" | "failed",
|
|
||||||
result_summary: string,
|
|
||||||
ui_hints: object | null,
|
|
||||||
attachments: Array<{
|
|
||||||
bucket: string,
|
|
||||||
path: string,
|
|
||||||
mime_type: string,
|
|
||||||
url: string
|
|
||||||
}> | null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 使用统计字段
|
|
||||||
|
|
||||||
`text.end` 事件中包含使用统计:
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `model` | string | 使用的模型名称 |
|
|
||||||
| `inputTokens` | number | 输入 token 数量 |
|
|
||||||
| `outputTokens` | number | 输出 token 数量 |
|
|
||||||
| `cost` | number | 花费(美元) |
|
|
||||||
| `latencyMs` | number | 延迟(毫秒) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 前端接收示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const eventSource = new EventSource(`/runs/${threadId}/events`);
|
|
||||||
|
|
||||||
eventSource.addEventListener('run.started', (e) => {
|
|
||||||
console.log('Agent started:', JSON.parse(e.data));
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('step.start', (e) => {
|
|
||||||
console.log('Step started:', JSON.parse(e.data));
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('text.delta', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Text delta:', data.data.delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('tool.start', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Tool called:', data.data.toolName);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('tool.result', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Tool result:', data.data.toolAgentOutput);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('text.end', (e) => {
|
|
||||||
const data = JSON.parse(e.data);
|
|
||||||
console.log('Worker output:', data.data.workerAgentOutput);
|
|
||||||
console.log('Usage:', {
|
|
||||||
inputTokens: data.data.inputTokens,
|
|
||||||
outputTokens: data.data.outputTokens,
|
|
||||||
cost: data.data.cost
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('run.finished', (e) => {
|
|
||||||
console.log('Agent finished:', JSON.parse(e.data));
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('run.error', (e) => {
|
|
||||||
console.log('Agent error:', JSON.parse(e.data));
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|||||||
+82
-399
@@ -1,432 +1,115 @@
|
|||||||
# 前后端数据流通指南
|
# 前后端数据流通指南(Agent Chat)
|
||||||
|
|
||||||
本文档描述前端如何与后端 Agent 系统交互,以及如何渲染后端返回的 UI 数据。
|
本文档仅描述**当前后端实现**的 runs/events/history 数据流,不定义视觉细节。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 整体交互流程
|
## 1) 总体流程
|
||||||
|
|
||||||
```
|
1. 客户端 `POST /api/v1/agent/runs` 提交 `RunAgentInput`
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
2. 后端返回 `202` + `taskId/threadId/runId/created`
|
||||||
│ 前端 │
|
3. 客户端 `GET /api/v1/agent/runs/{threadId}/events` 订阅 SSE
|
||||||
├─────────────────────────────────────────────────────────────────────────┤
|
4. 后端输出 AG-UI 事件(如 `RUN_STARTED`、`TOOL_CALL_RESULT`、`TEXT_MESSAGE_END`)
|
||||||
│ │
|
5. 客户端按需 `GET /api/v1/agent/history` 拉取历史快照(按天)
|
||||||
│ ┌──────────────┐ POST /runs ┌──────────────┐ │
|
|
||||||
│ │ 构造请求 │ ──────────────────▶│ 后端 │ │
|
|
||||||
│ │ (多模态数据) │ │ (任务入队) │ │
|
|
||||||
│ └──────────────┘◀────────────────── └──────────────┘ 202 Accepted │
|
|
||||||
│ │ taskId │
|
|
||||||
│ │ │
|
|
||||||
│ │ GET /runs/{threadId}/events │
|
|
||||||
│ │──────────────────────────────────────────────────────────▶ │
|
|
||||||
│ │ SSE 事件流 ◀───────────────── │
|
|
||||||
│ │ │
|
|
||||||
│ ┌─────┴──────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ 渲染事件 │ │
|
|
||||||
│ │ - text.delta: 追加文本 │ │
|
|
||||||
│ │ - tool.result: 渲染 toolAgentOutput.ui_hints → UiSchema │ │
|
|
||||||
│ │ - text.end: 渲染 workerAgentOutput.ui_hints → UiSchema │ │
|
|
||||||
│ └────────────────────────────────────────────────────────────────┘ │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 后端 │
|
|
||||||
├─────────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ POST /runs │
|
|
||||||
│ → 验证请求 │
|
|
||||||
│ → 任务入队列 (Taskiq) │
|
|
||||||
│ │
|
|
||||||
│ Worker 执行 │
|
|
||||||
│ → emit 事件到 Redis Stream │
|
|
||||||
│ → UiHints → ui_compiler.compile() → UiSchemaRenderer │
|
|
||||||
│ │
|
|
||||||
│ 持久化 │
|
|
||||||
│ → AgentChatMessage (含 uiSchema) │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 多模态数据处理
|
## 2) `/runs` 请求与响应
|
||||||
|
|
||||||
### 2.1 前端上传附件流程
|
### 请求
|
||||||
|
|
||||||
```javascript
|
- Body: `RunAgentInput`
|
||||||
// 方式一:先上传附件,获取签名 URL
|
- user message 可为纯文本,也可为文本+binary(图片 URL)
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('threadId', threadId);
|
|
||||||
formData.append('file', imageFile);
|
|
||||||
|
|
||||||
const uploadResponse = await fetch('/api/v1/agent/attachments', {
|
### 响应
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
const { attachment } = await uploadResponse.json();
|
|
||||||
// attachment.url 是临时访问 URL
|
|
||||||
|
|
||||||
// 方式二:直接使用已有的签名 URL(如果有)
|
```json
|
||||||
const imageUrl = 'https://storage.example.com/...';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 构造 RunAgentInput
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const runInput = {
|
|
||||||
threadId: threadId,
|
|
||||||
runId: `run-${Date.now()}`,
|
|
||||||
state: {},
|
|
||||||
messages: [
|
|
||||||
{
|
{
|
||||||
id: `msg-${Date.now()}`,
|
"taskId": "...",
|
||||||
role: 'user',
|
"threadId": "...",
|
||||||
content: [
|
"runId": "...",
|
||||||
{ type: 'text', text: '这张图片里有什么?' },
|
"created": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`created` 语义:是否在本次请求中创建了新会话。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) `/runs/{threadId}/events` 事件流
|
||||||
|
|
||||||
|
### SSE 形式
|
||||||
|
|
||||||
|
```text
|
||||||
|
id: <stream-id>
|
||||||
|
event: <EVENT_TYPE>
|
||||||
|
data: <json>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件类型
|
||||||
|
|
||||||
|
以 `docs/protocols/agent/sse-events.md` 为准。当前重点是:
|
||||||
|
|
||||||
|
- 运行生命周期:`RUN_STARTED` / `RUN_FINISHED` / `RUN_ERROR`
|
||||||
|
- 阶段:`STEP_STARTED` / `STEP_FINISHED`
|
||||||
|
- 工具:`TOOL_CALL_START` / `TOOL_CALL_ARGS` / `TOOL_CALL_END` / `TOOL_CALL_RESULT`
|
||||||
|
- 文本完成:`TEXT_MESSAGE_END`
|
||||||
|
|
||||||
|
### 文本流策略
|
||||||
|
|
||||||
|
当前后端不提供 token 级 `TEXT_MESSAGE_CONTENT` 增量流作为主路径;
|
||||||
|
而是在 worker 完成后通过 `TEXT_MESSAGE_END` 一次性携带完整语义结果。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) `/history` 快照
|
||||||
|
|
||||||
|
`GET /api/v1/agent/history` 返回 `HistorySnapshotResponse`:
|
||||||
|
|
||||||
|
```json
|
||||||
{
|
{
|
||||||
type: 'binary',
|
"scope": "history_day",
|
||||||
mimeType: 'image/png',
|
"threadId": "...",
|
||||||
url: attachment.url // 签名 URL
|
"day": "2026-03-16",
|
||||||
|
"hasMore": false,
|
||||||
|
"messages": []
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tools: [], // 或传入工具定义
|
|
||||||
context: [],
|
|
||||||
forwardedProps: {}
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 发送请求
|
说明:
|
||||||
|
|
||||||
```javascript
|
- 这是普通 JSON 响应,不是 SSE 事件包装。
|
||||||
const response = await fetch('/api/v1/agent/runs', {
|
- `messages` 已按 seq 升序组织。
|
||||||
method: 'POST',
|
- `before` 采用 `YYYY-MM-DD`,语义是向更早日期翻页。
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(runInput)
|
|
||||||
});
|
|
||||||
// 返回 202 Accepted
|
|
||||||
const { taskId, threadId, runId, created } = await response.json();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 事件流渲染
|
## 5) events 与 history 的一致性机制
|
||||||
|
|
||||||
### 3.1 订阅事件
|
### 5.1 语义来源一致
|
||||||
|
|
||||||
```javascript
|
两条链路都来自同一运行时输出(worker/tool output)及其持久化元数据。
|
||||||
class AgentEventHandler {
|
|
||||||
constructor(threadId) {
|
|
||||||
this.eventSource = new EventSource(`/api/v1/agent/runs/${threadId}/events`);
|
|
||||||
this.setupListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupListeners() {
|
### 5.2 UI 编译器一致
|
||||||
this.eventSource.addEventListener('run.started', this.onRunStarted);
|
|
||||||
this.eventSource.addEventListener('step.start', this.onStepStart);
|
|
||||||
this.eventSource.addEventListener('text.delta', this.onTextDelta);
|
|
||||||
this.eventSource.addEventListener('text.message.start', this.onTextStart);
|
|
||||||
this.eventSource.addEventListener('tool.call.start', this.onToolStart);
|
|
||||||
this.eventSource.addEventListener('tool.call.args', this.onToolArgs);
|
|
||||||
this.eventSource.addEventListener('tool.call.result', this.onToolResult);
|
|
||||||
this.eventSource.addEventListener('text.message.end', this.onTextEnd);
|
|
||||||
this.eventSource.addEventListener('run.finished', this.onRunFinished);
|
|
||||||
this.eventSource.addEventListener('run.error', this.onRunError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 事件处理与渲染
|
两条链路都使用后端 `ui_compiler.compile(...)` 将 `ui_hints` 编译为可渲染结构:
|
||||||
|
|
||||||
#### text.delta - 文本增量
|
- events:在 runtime 发送事件前编译,字段名为 `ui_schema`
|
||||||
|
- history:在历史转换时编译,字段名为 `ui_schema`
|
||||||
|
|
||||||
```javascript
|
### 5.3 当前命名差异(实现现状)
|
||||||
onTextDelta(event) {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
const { delta, messageId } = data.data;
|
|
||||||
// 追加到对应消息的文本内容
|
|
||||||
appendTextToMessage(messageId, delta);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### tool.result - 工具结果
|
两条链路字段命名已统一:
|
||||||
|
|
||||||
```javascript
|
- events: `ui_schema`(snake_case)
|
||||||
onToolResult(event) {
|
- history: `ui_schema`(snake_case)
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
const { toolAgentOutput, toolCallId } = data.data;
|
|
||||||
|
|
||||||
if (toolAgentOutput?.ui_hints) {
|
|
||||||
// 后端返回的是 UiHints,需要编译为 UiSchemaRenderer
|
|
||||||
const uiSchema = compileUiHints(toolAgentOutput.ui_hints);
|
|
||||||
renderUiSchema(uiSchema, toolCallId);
|
|
||||||
} else {
|
|
||||||
// 纯文本结果
|
|
||||||
renderText(toolAgentOutput.result_summary, toolCallId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### text.end - Worker 完成
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
onTextEnd(event) {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
const { workerAgentOutput, inputTokens, outputTokens, cost } = data.data;
|
|
||||||
|
|
||||||
if (workerAgentOutput?.ui_hints) {
|
|
||||||
// 渲染 UiHints → UiSchemaRenderer
|
|
||||||
const uiSchema = compileUiHints(workerAgentOutput.ui_hints);
|
|
||||||
renderUiSchema(uiSchema, messageId);
|
|
||||||
} else {
|
|
||||||
// 纯文本回复
|
|
||||||
renderText(workerAgentOutput?.answer, messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示使用统计
|
|
||||||
showUsageStats({ inputTokens, outputTokens, cost });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. History 对话历史渲染
|
## 6) 推荐消费顺序(面向客户端重构)
|
||||||
|
|
||||||
### 4.1 获取历史
|
1. 先以 `/history` 获取首屏快照
|
||||||
|
2. 再接入 `/events` 处理后续增量
|
||||||
```javascript
|
3. 以 `runId` + `messageId/toolCallId` 做去重与合并
|
||||||
const response = await fetch(
|
4. 统一消费 `ui_schema`
|
||||||
`/api/v1/agent/history?threadId=${threadId}&before=${date}`
|
|
||||||
);
|
|
||||||
const { messages } = await response.json();
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 渲染历史消息
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
messages.forEach(msg => {
|
|
||||||
switch (msg.role) {
|
|
||||||
case 'user':
|
|
||||||
renderUserMessage(msg);
|
|
||||||
break;
|
|
||||||
case 'assistant':
|
|
||||||
if (msg.uiSchema) {
|
|
||||||
// 直接渲染 UiSchemaRenderer(后端已编译好)
|
|
||||||
renderUiSchema(msg.uiSchema);
|
|
||||||
} else {
|
|
||||||
// 纯文本
|
|
||||||
renderText(msg.content);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'tool':
|
|
||||||
// tool 结果也可能有 uiSchema
|
|
||||||
if (msg.uiSchema) {
|
|
||||||
renderUiSchema(msg.uiSchema);
|
|
||||||
} else {
|
|
||||||
renderText(msg.content);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. UiSchemaRenderer 渲染
|
|
||||||
|
|
||||||
### 5.1 数据结构
|
|
||||||
|
|
||||||
后端 `ui_compiler.compile()` 输出的结构:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface UiSchemaRenderer {
|
|
||||||
version: "2.0";
|
|
||||||
locale: string;
|
|
||||||
status: "info" | "success" | "warning" | "error" | "pending";
|
|
||||||
theme: "default" | "light" | "dark";
|
|
||||||
meta?: {
|
|
||||||
requestId?: string;
|
|
||||||
toolId?: string;
|
|
||||||
traceId?: string;
|
|
||||||
userId?: string;
|
|
||||||
};
|
|
||||||
root: UiLayoutNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 布局节点
|
|
||||||
interface UiStackNode {
|
|
||||||
type: "stack";
|
|
||||||
direction: "vertical" | "horizontal";
|
|
||||||
gap?: number;
|
|
||||||
appearance: "plain" | "card" | "section";
|
|
||||||
status?: UiStatus;
|
|
||||||
align?: LayoutAlign;
|
|
||||||
justify?: LayoutJustify;
|
|
||||||
wrap?: boolean;
|
|
||||||
children: UiNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UiGridNode {
|
|
||||||
type: "grid";
|
|
||||||
columns: number;
|
|
||||||
gap?: number;
|
|
||||||
appearance: LayoutAppearance;
|
|
||||||
status?: UiStatus;
|
|
||||||
children: UiNode[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 基础节点
|
|
||||||
type UiNode =
|
|
||||||
| UiTextNode // 文本
|
|
||||||
| UiIconNode // 图标
|
|
||||||
| UiBadgeNode // 标签
|
|
||||||
| UiButtonNode // 按钮
|
|
||||||
| UiKvNode // 键值对
|
|
||||||
| UiDividerNode; // 分割线
|
|
||||||
|
|
||||||
// 文本节点
|
|
||||||
interface UiTextNode {
|
|
||||||
type: "text";
|
|
||||||
content: string;
|
|
||||||
format: "plain" | "markdown";
|
|
||||||
role: "title" | "subtitle" | "body" | "caption" | "code";
|
|
||||||
status?: UiStatus;
|
|
||||||
maxLines?: number;
|
|
||||||
visible?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮节点
|
|
||||||
interface UiButtonNode {
|
|
||||||
type: "button";
|
|
||||||
label: string;
|
|
||||||
style: "primary" | "secondary" | "ghost" | "danger";
|
|
||||||
disabled?: boolean;
|
|
||||||
icon?: UiIconSpec;
|
|
||||||
action: UiActionPayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 键值对节点
|
|
||||||
interface UiKvNode {
|
|
||||||
type: "kv";
|
|
||||||
items: UiKvItem[];
|
|
||||||
columns?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UiKvItem {
|
|
||||||
key: string;
|
|
||||||
label?: string;
|
|
||||||
value: any;
|
|
||||||
copyable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action payloads
|
|
||||||
type UiActionPayload =
|
|
||||||
| { type: "navigation"; path: string; params?: Record<string, any> }
|
|
||||||
| { type: "url"; url: string; target?: "_self" | "_blank" }
|
|
||||||
| { type: "event"; event: string; payload?: Record<string, any> }
|
|
||||||
| { type: "tool"; toolId: string; params?: Record<string, any> }
|
|
||||||
| { type: "copy"; content: string; successMessage?: string }
|
|
||||||
| { type: "payload"; payload: Record<string, any>; submitTo?: string };
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 统一渲染器实现
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class UiSchemaRendererWidget extends StatelessWidget {
|
|
||||||
final UiSchemaRenderer schema;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return renderNode(schema.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget renderNode(UiNode node) {
|
|
||||||
switch (node.type) {
|
|
||||||
case 'text':
|
|
||||||
return renderTextNode(node as UiTextNode);
|
|
||||||
case 'icon':
|
|
||||||
return renderIconNode(node as UiIconNode);
|
|
||||||
case 'badge':
|
|
||||||
return renderBadgeNode(node as UiBadgeNode);
|
|
||||||
case 'button':
|
|
||||||
return renderButtonNode(node as UiButtonNode);
|
|
||||||
case 'kv':
|
|
||||||
return renderKvNode(node as UiKvNode);
|
|
||||||
case 'divider':
|
|
||||||
return renderDividerNode(node as UiDividerNode);
|
|
||||||
case 'stack':
|
|
||||||
return renderStackNode(node as UiStackNode);
|
|
||||||
case 'grid':
|
|
||||||
return renderGridNode(node as UiGridNode);
|
|
||||||
default:
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 事件渲染 vs History 渲染一致性
|
|
||||||
|
|
||||||
**关键点**:两者使用同一个渲染器
|
|
||||||
|
|
||||||
| 数据来源 | 数据格式 | 处理方式 |
|
|
||||||
|---------|---------|---------|
|
|
||||||
| events text.end | `workerAgentOutput.ui_hints` | compile → UiSchemaRenderer → 渲染 |
|
|
||||||
| events tool.result | `toolAgentOutput.ui_hints` | compile → UiSchemaRenderer → 渲染 |
|
|
||||||
| history assistant | `msg.uiSchema` | 直接渲染 |
|
|
||||||
| history tool | `msg.uiSchema` | 直接渲染 |
|
|
||||||
|
|
||||||
**编译函数**(前端需要实现):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 将后端 UiHints 编译为 UiSchemaRenderer
|
|
||||||
function compileUiHints(hints) {
|
|
||||||
// 这里可以调用后端的 compile 接口
|
|
||||||
// 或者在前端实现相同的编译逻辑
|
|
||||||
return fetch('/api/v1/agent/compile-ui-hints', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(hints)
|
|
||||||
}).then(r => r.json());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. UiHints 介绍(可选)
|
|
||||||
|
|
||||||
如果前端需要自己处理 UiHints,以下是结构:
|
|
||||||
|
|
||||||
### UiHintsPayload
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface UiHintsPayload {
|
|
||||||
version: "2.1";
|
|
||||||
intent: "message" | "data" | "list" | "status" | "form" | "mixed";
|
|
||||||
status: "info" | "success" | "warning" | "error" | "pending";
|
|
||||||
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
body?: string;
|
|
||||||
bodyFormat?: "plain" | "markdown";
|
|
||||||
|
|
||||||
items?: UiHintKvItem[];
|
|
||||||
listItems?: UiHintListItem[];
|
|
||||||
sections?: UiHintSection[];
|
|
||||||
actions?: UiHintAction[];
|
|
||||||
icon?: UiHintIcon;
|
|
||||||
meta?: Record<string, any>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:History API 返回的消息已经包含了编译好的 `uiSchema`,前端不需要自己编译。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 总结
|
|
||||||
|
|
||||||
1. **多模态**:前端上传附件 → 获取签名 URL → 构造 RunAgentInput → POST /runs
|
|
||||||
2. **实时事件**:订阅 /runs/{threadId}/events → 处理 text.delta/tool.result/text.end → 渲染 UiSchema
|
|
||||||
3. **历史消息**:GET /history → 遍历消息 → 有 uiSchema 则渲染,否则渲染纯文本
|
|
||||||
4. **一致性**:events 和 history 使用同一个 UiSchemaRenderer 渲染组件
|
|
||||||
|
|||||||
Reference in New Issue
Block a user