feat: 实现 AgentScope ReAct Runner 两阶段执行并重构事件处理
This commit is contained in:
@@ -1,220 +0,0 @@
|
||||
# Agent Chat Messages Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
Chat messages in agent conversations with metadata for tracking and telemetry.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Message Metadata
|
||||
|
||||
```typescript
|
||||
interface AgentChatMessageMetadata {
|
||||
run_id?: string; // Unique run identifier
|
||||
stage?: string; // Processing stage (e.g., "intent", "execution")
|
||||
latency_ms?: number; // Processing latency in milliseconds
|
||||
message_id?: string; // Message identifier
|
||||
[key: string]: any; // Additional custom fields allowed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| metadata | jsonb | Message metadata including run_id, stage, latency_ms, message_id |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Basic Message Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "run_1773286460762"
|
||||
}
|
||||
```
|
||||
|
||||
### Stage Completion Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "run_1773286460762",
|
||||
"stage": "intent",
|
||||
"latency_ms": 2610,
|
||||
"message_id": "intent-run_1773286460762"
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Execution Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "run_1773287162123",
|
||||
"stage": "tool_execution",
|
||||
"latency_ms": 1500,
|
||||
"message_id": "tool_run_abc123",
|
||||
"tool_name": "calendar_create_event"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User Message Attachments
|
||||
|
||||
### Overview
|
||||
|
||||
When a user sends a message with binary attachments (e.g., images), the frontend uploads the file to storage first, then sends a signed URL to the backend. The backend parses the signed URL to extract storage metadata and persists it with the message.
|
||||
|
||||
### Flow
|
||||
|
||||
```
|
||||
Frontend Backend
|
||||
────────────────────────────────────────────────────────────────
|
||||
1. Upload file
|
||||
POST /api/v1/agent/attachments
|
||||
──────────────────────────────>
|
||||
<──────────────────────────────
|
||||
{bucket, path, mime_type, url: signed_url}
|
||||
|
||||
2. Send message with binary block
|
||||
POST /api/v1/agent/runs
|
||||
content: [
|
||||
{type: "text", text: "..."},
|
||||
{type: "binary", mimeType: "image/jpeg", url: signed_url}
|
||||
]
|
||||
──────────────────────────────>
|
||||
|
||||
3. Backend parses signed URL
|
||||
parse_signed_url(url) → {bucket, path}
|
||||
|
||||
4. Persist to database
|
||||
metadata.user_message_attachments = {bucket, path, mime_type}
|
||||
|
||||
5. Return history (GET /history)
|
||||
<──────────────────────────────
|
||||
messages: [{
|
||||
id: "msg-1",
|
||||
seq: 1,
|
||||
role: "user",
|
||||
content: "...",
|
||||
metadata: {
|
||||
user_message_attachments: {bucket, path, mime_type}
|
||||
},
|
||||
timestamp: "2026-03-13T10:00:00Z"
|
||||
}]
|
||||
|
||||
6. Resolve temporary URL for rendering
|
||||
GET /api/v1/agent/attachments/signed-url?bucket=...&path=...
|
||||
──────────────────────────────>
|
||||
<──────────────────────────────
|
||||
{bucket, path, url}
|
||||
```
|
||||
|
||||
### Signed URL Format
|
||||
|
||||
Supabase signed URL format:
|
||||
```
|
||||
https://{project}.supabase.co/storage/v1/object/sign/{bucket}/{path}?token={jwt}
|
||||
```
|
||||
|
||||
Backend parses to extract:
|
||||
- `bucket`: URL path segment after `/sign/`
|
||||
- `path`: Remaining path after bucket
|
||||
|
||||
### Metadata Schema
|
||||
|
||||
```typescript
|
||||
interface UserMessageAttachments {
|
||||
bucket: string; // Storage bucket name
|
||||
path: string; // Object storage path
|
||||
mime_type: string; // MIME type (e.g., "image/jpeg")
|
||||
}
|
||||
|
||||
interface AgentChatMessageMetadata {
|
||||
// ... existing fields
|
||||
user_message_attachments?: UserMessageAttachments;
|
||||
}
|
||||
```
|
||||
|
||||
### Database Storage
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| metadata | jsonb | Contains user_message_attachments with bucket, path, mime_type |
|
||||
|
||||
### Example
|
||||
|
||||
**Request (POST /runs):**
|
||||
```json
|
||||
{
|
||||
"threadId": "thread-123",
|
||||
"runId": "run-456",
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "帮我看看这张图"},
|
||||
{
|
||||
"type": "binary",
|
||||
"mimeType": "image/jpeg",
|
||||
"url": "https://xxx.supabase.co/storage/v1/object/sign/agent-files/agent-inputs/u/t/r/img.jpg?token=xxx"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Stored Metadata:**
|
||||
```json
|
||||
{
|
||||
"user_message_attachments": {
|
||||
"bucket": "agent-files",
|
||||
"path": "agent-inputs/u/t/r/img.jpg",
|
||||
"mime_type": "image/jpeg"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**History Response (GET /history):**
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-1",
|
||||
"seq": 1,
|
||||
"role": "user",
|
||||
"content": "帮我看看这张图",
|
||||
"metadata": {
|
||||
"user_message_attachments": {
|
||||
"bucket": "agent-files",
|
||||
"path": "agent-inputs/u/t/r/img.jpg",
|
||||
"mime_type": "image/jpeg"
|
||||
}
|
||||
},
|
||||
"timestamp": "2026-03-13T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Attachment URL Response (GET /attachments/signed-url):**
|
||||
```json
|
||||
{
|
||||
"bucket": "agent-files",
|
||||
"path": "agent-inputs/u/t/r/img.jpg",
|
||||
"url": "https://xxx.supabase.co/storage/v1/object/sign/agent-files/agent-inputs/u/t/r/img.jpg?token=yyy"
|
||||
}
|
||||
```
|
||||
@@ -1,59 +0,0 @@
|
||||
# Agent Chat Sessions Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
Agent chat session state snapshot for preserving conversation context.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Session State Snapshot
|
||||
|
||||
```typescript
|
||||
interface SessionStateSnapshot {
|
||||
// Reserved for future use
|
||||
// Currently unused, allowing custom extensions
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| state_snapshot | jsonb | Session state for preserving conversation context |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Empty State
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
### Future Usage Example
|
||||
|
||||
```json
|
||||
{
|
||||
"conversation_context": {
|
||||
"last_topic": "calendar_events",
|
||||
"mentioned_dates": ["2026-03-15", "2026-03-20"]
|
||||
},
|
||||
"agent_memory": {
|
||||
"user_preferences": {
|
||||
"timezone": "Asia/Shanghai",
|
||||
"language": "zh-CN"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,360 @@
|
||||
# Agent API Endpoints
|
||||
|
||||
本文档列出所有 Agent 相关的 API 端点。
|
||||
|
||||
Base URL: `/api/v1/agent`
|
||||
|
||||
---
|
||||
|
||||
## 端点清单
|
||||
|
||||
| 方法 | 路径 | 描述 |
|
||||
|------|------|------|
|
||||
| POST | `/runs` | 发起 Agent 运行 |
|
||||
| GET | `/runs/{thread_id}/events` | SSE 事件流 |
|
||||
| GET | `/history` | 获取对话历史快照 |
|
||||
| POST | `/attachments` | 上传附件 |
|
||||
| GET | `/attachments/signed-url` | 生成附件签名 URL |
|
||||
| POST | `/transcribe` | 语音转文字 (ASR) |
|
||||
|
||||
---
|
||||
|
||||
## 1. POST /runs
|
||||
|
||||
发起一个 Agent 运行任务。
|
||||
|
||||
### Request
|
||||
|
||||
Request Body: `RunAgentInput`
|
||||
|
||||
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
|
||||
|
||||
### Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
taskId: string, // 任务 ID
|
||||
threadId: string, // 会话 ID
|
||||
runId: string, // 运行 ID
|
||||
created: string // ISO-8601 时间戳
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.example.com/api/v1/agent/runs \
|
||||
-H "Content-Type: application/json" \
|
||||
-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
|
||||
|
||||
获取 SSE 事件流,用于实时接收 Agent 运行过程中的事件。
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| thread_id | string | 会话 ID |
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| 参数 | 类型 | 默认值 | 描述 |
|
||||
|------|------|--------|------|
|
||||
| Last-Event-ID | string | - | 可选,用于断点续传的事件 ID |
|
||||
| idle_limit | integer | 300 | 最大空闲轮询次数 (1-3600) |
|
||||
|
||||
### Headers
|
||||
|
||||
| 参数 | 描述 |
|
||||
|------|------|
|
||||
| Last-Event-ID | 可选的事件 ID,用于从指定位置恢复 |
|
||||
|
||||
### Response
|
||||
|
||||
SSE (Server-Sent Events) 流,Content-Type: `text/event-stream`
|
||||
|
||||
事件类型详情见 [sse-events.md](./sse-events.md)
|
||||
|
||||
### Example
|
||||
|
||||
```javascript
|
||||
const eventSource = new EventSource(
|
||||
'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
|
||||
|
||||
获取对话历史快照。
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
|------|------|------|------|
|
||||
| threadId | string | 否 | 会话 ID,不指定则返回最新会话 |
|
||||
| before | date | 否 | 日期格式 `YYYY-MM-DD`,返回该日期之前的快照 |
|
||||
|
||||
### Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
scope: "history_day",
|
||||
threadId: string | null,
|
||||
day: string | null, // ISO date format "YYYY-MM-DD"
|
||||
hasMore: boolean,
|
||||
messages: HistoryMessage[]
|
||||
}
|
||||
```
|
||||
|
||||
详细数据结构见 [run-agent-input.md](./run-agent-input.md)
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl "https://api.example.com/api/v1/agent/history?threadId=550e8400-e29b-41d4-a716-446655440000&before=2026-03-15"
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
上传附件到存储。
|
||||
|
||||
### Form Data
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| threadId | string | 会话 ID |
|
||||
| file | file | 要上传的文件 |
|
||||
|
||||
### Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
attachment: {
|
||||
bucket: string,
|
||||
path: string,
|
||||
mimeType: string,
|
||||
size: number,
|
||||
url: string // 临时访问 URL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.example.com/api/v1/agent/attachments \
|
||||
-F "threadId=550e8400-e29b-41d4-a716-446655440000" \
|
||||
-F "file=@/path/to/image.png"
|
||||
```
|
||||
|
||||
### Limits
|
||||
|
||||
- 最大文件大小: 5MB
|
||||
- 支持的文件类型: 见后端配置
|
||||
|
||||
---
|
||||
|
||||
## 5. GET /attachments/signed-url
|
||||
|
||||
生成附件的签名 URL,用于直接访问存储中的文件。
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
|------|------|------|------|
|
||||
| bucket | string | 是 | 存储桶名称 |
|
||||
| path | string | 是 | 文件路径 |
|
||||
|
||||
### Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
bucket: string,
|
||||
path: string,
|
||||
url: string // 签名 URL
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
语音转文字 (ASR)。
|
||||
|
||||
### Request
|
||||
|
||||
Form Data:
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| audio | file | 音频文件 (WAV 格式) |
|
||||
|
||||
### Headers
|
||||
|
||||
| 参数 | 描述 |
|
||||
|------|------|
|
||||
| content-length | 文件大小 |
|
||||
|
||||
### Response
|
||||
|
||||
```typescript
|
||||
{
|
||||
transcript: string, // 转录文本
|
||||
language: string, // 检测到的语言
|
||||
duration: number // 音频时长 (秒)
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.example.com/api/v1/agent/transcribe \
|
||||
-F "audio=@/path/to/audio.wav"
|
||||
```
|
||||
|
||||
### Response Example
|
||||
|
||||
```json
|
||||
{
|
||||
"transcript": "今天天气真不错",
|
||||
"language": "zh-CN",
|
||||
"duration": 3.5
|
||||
}
|
||||
```
|
||||
|
||||
### Limits
|
||||
|
||||
- 支持格式: `audio/wav`, `audio/x-wav`, `audio/wave`
|
||||
- 最大文件大小: 10MB
|
||||
- 速率限制: 20 次/分钟/用户
|
||||
|
||||
---
|
||||
|
||||
## 错误响应
|
||||
|
||||
所有端点可能返回以下错误:
|
||||
|
||||
| 状态码 | 描述 |
|
||||
|--------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未认证 |
|
||||
| 403 | 无权限 |
|
||||
| 404 | 资源不存在 |
|
||||
| 413 | 请求体过大 |
|
||||
| 422 | 数据验证失败 |
|
||||
| 429 | 速率限制 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "错误详情信息"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,449 @@
|
||||
# Agent Run Input Protocol
|
||||
|
||||
> **NOTE**: This document defines the RunAgentInput data structure for `POST /api/v1/agent/runs`.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`POST /api/v1/agent/runs` accepts `RunAgentInput` as request body to initiate an agent execution.
|
||||
|
||||
---
|
||||
|
||||
## RunAgentInput Schema
|
||||
|
||||
```typescript
|
||||
interface RunAgentInput {
|
||||
threadId: string; // 必须是有效 UUID
|
||||
runId: string; // 最大 128 字符
|
||||
parentRunId?: string;
|
||||
state?: any;
|
||||
messages: Message[]; // 最多 200 条
|
||||
tools?: Tool[];
|
||||
context?: Context[];
|
||||
forwardedProps?: any;
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Constraints |
|
||||
|-------|------|-------------|
|
||||
| `threadId` | string | 必须为有效 UUID |
|
||||
| `runId` | string | 最大 128 字符 |
|
||||
| `messages` | array | 最多 200 条,**必须恰好包含 1 条 user message** |
|
||||
| `state` | any | - |
|
||||
| `tools` | array | 可选 |
|
||||
| `context` | array | 可选 |
|
||||
| `forwardedProps` | any | - |
|
||||
|
||||
---
|
||||
|
||||
## Message Types
|
||||
|
||||
### UserMessage
|
||||
|
||||
```typescript
|
||||
interface UserMessage {
|
||||
id: string;
|
||||
role: "user";
|
||||
content: string | ContentBlock[];
|
||||
name?: string;
|
||||
encryptedValue?: string;
|
||||
}
|
||||
|
||||
type ContentBlock = TextContentBlock | BinaryContentBlock;
|
||||
|
||||
interface TextContentBlock {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface BinaryContentBlock {
|
||||
type: "binary";
|
||||
mimeType: string; // 必须为 image/* 类型
|
||||
id?: string;
|
||||
url?: string; // 必须是有效的 signed URL
|
||||
data?: string; // 不允许使用 data 字段
|
||||
filename?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### AssistantMessage
|
||||
|
||||
```typescript
|
||||
interface AssistantMessage {
|
||||
id: string;
|
||||
role: "assistant";
|
||||
content?: string | null;
|
||||
name?: string | null;
|
||||
toolCalls?: ToolCall[];
|
||||
encryptedValue?: string | null;
|
||||
}
|
||||
|
||||
interface ToolCall {
|
||||
id: string;
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
encryptedValue?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
### SystemMessage
|
||||
|
||||
```typescript
|
||||
interface SystemMessage {
|
||||
id: string;
|
||||
role: "system";
|
||||
content: string;
|
||||
name?: string | null;
|
||||
encryptedValue?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
### ToolMessage
|
||||
|
||||
```typescript
|
||||
interface ToolMessage {
|
||||
id: string;
|
||||
role: "tool";
|
||||
content: string;
|
||||
toolCallId: string;
|
||||
error?: string | null;
|
||||
encryptedValue?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Other Message Types
|
||||
|
||||
- **DeveloperMessage**: `role: "developer"`
|
||||
- **ReasoningMessage**: `role: "reasoning"`
|
||||
- **ActivityMessage**: `role: "activity"` (for progress updates)
|
||||
|
||||
---
|
||||
|
||||
## Tool Schema
|
||||
|
||||
```typescript
|
||||
interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: object; // JSON Schema 格式
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get current weather for a location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "City name"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["celsius", "fahrenheit"]
|
||||
}
|
||||
},
|
||||
"required": ["location"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Processing
|
||||
|
||||
Backend 使用 `build_tools_prompt` 函数将 tools 转换为 prompt 格式:
|
||||
|
||||
```
|
||||
<!-- TOOLS_START -->
|
||||
- get_weather: Get current weather for a location
|
||||
- args_schema: {"type":"object","properties":{"location":{"type":"string","description":"City name"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}
|
||||
- searchDocuments: Search for documents
|
||||
- args_schema: {"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}
|
||||
Note: tool arguments must strictly match args_schema.
|
||||
<!-- TOOLS_END -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context Schema
|
||||
|
||||
```typescript
|
||||
interface Context {
|
||||
description: string;
|
||||
value: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
Backend 实现了以下验证规则:
|
||||
|
||||
| Rule | Error Message |
|
||||
|------|---------------|
|
||||
| payload ≤ 256KB | `RunAgentInput payload exceeds size limit` |
|
||||
| threadId 必须是 UUID | `threadId must be a valid UUID` |
|
||||
| runId 最大 128 字符 | `runId exceeds length limit` |
|
||||
| messages ≤ 200 条 | `RunAgentInput.messages exceeds limit` |
|
||||
| user text ≤ 10,000 字符 | `RunAgentInput user message text exceeds limit` |
|
||||
| **恰好 1 条 user message** | `RunAgentInput.messages must contain exactly one user message` |
|
||||
| user message 必须在第一条 | `RunAgentInput.messages[0].role must be user` |
|
||||
| binary 必须是 image/* | `binary content requires image mimeType` |
|
||||
| binary 必须有 url | `binary content requires url` |
|
||||
| binary 不允许使用 data | `binary content data is not allowed` |
|
||||
|
||||
---
|
||||
|
||||
## Request Example
|
||||
|
||||
### 纯文本请求
|
||||
|
||||
```json
|
||||
{
|
||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"runId": "run-001",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-001",
|
||||
"role": "user",
|
||||
"content": "帮我查一下北京今天的天气"
|
||||
}
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 多模态请求 (带图片)
|
||||
|
||||
```json
|
||||
{
|
||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"runId": "run-002",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-001",
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这张图片里的内容是什么?"
|
||||
},
|
||||
{
|
||||
"type": "binary",
|
||||
"mimeType": "image/png",
|
||||
"url": "https://storage.example.com/agent-inputs/user-123/image.png?signature=xxx"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 带 Tools 的请求
|
||||
|
||||
```json
|
||||
{
|
||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"runId": "run-003",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg-001",
|
||||
"role": "user",
|
||||
"content": "北京天气怎么样?"
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "获取指定城市的天气信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "城市名称"
|
||||
}
|
||||
},
|
||||
"required": ["city"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"context": [],
|
||||
"forwardedProps": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Response
|
||||
|
||||
成功响应返回 `TaskAcceptedResponse`:
|
||||
|
||||
```typescript
|
||||
interface TaskAcceptedResponse {
|
||||
taskId: string;
|
||||
threadId: string;
|
||||
runId: string;
|
||||
created: string; // ISO-8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## History API
|
||||
|
||||
`GET /api/v1/agent/history` 返回对话历史快照。
|
||||
|
||||
### Request
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `threadId` | string (query) | 可选,指定会话 ID,不指定则返回最新会话 |
|
||||
| `before` | string (query) | 可选,日期格式 `YYYY-MM-DD`,返回该日期之前的快照 |
|
||||
|
||||
### Response
|
||||
|
||||
返回 `HistorySnapshotResponse`:
|
||||
|
||||
```typescript
|
||||
interface HistorySnapshotResponse {
|
||||
scope: "history_day";
|
||||
threadId: string | null;
|
||||
day: string | null; // ISO date format "YYYY-MM-DD"
|
||||
hasMore: boolean;
|
||||
messages: HistoryMessage[];
|
||||
}
|
||||
```
|
||||
|
||||
### HistoryMessage
|
||||
|
||||
根据消息 role 不同,返回字段有所差异:
|
||||
|
||||
```typescript
|
||||
// role = "user"
|
||||
interface HistoryMessageUser {
|
||||
id: string;
|
||||
seq: number;
|
||||
role: "user";
|
||||
content: string;
|
||||
url: string | null; // 附件临时访问 URL
|
||||
timestamp: string; // ISO-8601 timestamp
|
||||
}
|
||||
|
||||
// role = "tool"
|
||||
interface HistoryMessageTool {
|
||||
id: string;
|
||||
seq: number;
|
||||
role: "tool";
|
||||
content: string;
|
||||
uiSchema: UiSchemaRenderer | null; // 由 tool_agent_output.ui_hints 编译
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// role = "assistant"
|
||||
interface HistoryMessageAssistant {
|
||||
id: string;
|
||||
seq: number;
|
||||
role: "assistant";
|
||||
content: string;
|
||||
uiSchema: UiSchemaRenderer | null; // 由 worker_agent_output.ui_hints 编译
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
type HistoryMessage = HistoryMessageUser | HistoryMessageTool | HistoryMessageAssistant;
|
||||
```
|
||||
|
||||
### UiSchemaRenderer
|
||||
|
||||
编译后的 UI 渲染结构,详见 [UiSchema Protocol](../ui/ui-schema.md):
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": "history_day",
|
||||
"threadId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"day": "2026-03-15",
|
||||
"hasMore": true,
|
||||
"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"},
|
||||
{"type": "text", "content": "您的会议日程创建成功", "role": "body"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestamp": "2026-03-15T10:00:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- `UserMessage.content` 支持 string 或 array 格式,前端优先使用 array 格式以支持多模态
|
||||
- binary content 的 url 必须是有效的 signed URL,由 `/api/v1/agent/attachments` 端点生成
|
||||
- backend 验证通过后,会将 binary url 转换为内部存储路径
|
||||
- tools 为空数组时,prompt 中不会包含工具说明
|
||||
@@ -0,0 +1,370 @@
|
||||
# Agent SSE Events
|
||||
|
||||
本文档记录 Agent Runtime 产生的所有 Server-Sent Events (SSE) 事件,用于前端实时展示。
|
||||
|
||||
## 事件流转架构
|
||||
|
||||
```
|
||||
pipeline.emit()
|
||||
↓
|
||||
AgentScopeEventPipeline.emit()
|
||||
├─→ store.persist() → 持久化到数据库
|
||||
└─→ bus.publish() → 发布到 Redis Stream
|
||||
↓
|
||||
前端通过 GET /runs/{thread_id}/events 读取
|
||||
```
|
||||
|
||||
## 事件统一格式
|
||||
|
||||
所有事件在 Redis 中传输时都包含以下字段:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: string, // 事件类型
|
||||
threadId: string, // 会话 ID
|
||||
runId: string, // 运行 ID
|
||||
data: object // 事件数据
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Orchestrator 生命周期事件
|
||||
|
||||
### run.started
|
||||
|
||||
Agent 开始运行时触发。
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "run.started",
|
||||
threadId: "xxx",
|
||||
runId: "yyy",
|
||||
data: {}
|
||||
}
|
||||
```
|
||||
|
||||
### run.finished
|
||||
|
||||
Agent 成功完成时触发。
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "run.finished",
|
||||
threadId: "xxx",
|
||||
runId: "yyy",
|
||||
data: {}
|
||||
}
|
||||
```
|
||||
|
||||
### run.error
|
||||
|
||||
Agent 运行出错时触发。
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "run.error",
|
||||
threadId: "xxx",
|
||||
runId: "yyy",
|
||||
data: {
|
||||
message: "runtime execution failed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Step 阶段事件
|
||||
|
||||
### step.start
|
||||
|
||||
阶段开始时触发。
|
||||
|
||||
```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));
|
||||
});
|
||||
```
|
||||
@@ -1,130 +0,0 @@
|
||||
# Inbox Messages Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
Inbox messages are notifications sent to users for various events like friend requests and calendar invites.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Message Types
|
||||
|
||||
```typescript
|
||||
type InboxMessageType = 'friend_request' | 'calendar' | 'system' | 'group';
|
||||
```
|
||||
|
||||
```typescript
|
||||
type InboxMessageStatus = 'pending' | 'accepted' | 'rejected' | 'dismissed';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Schema
|
||||
|
||||
### Calendar Invite Content
|
||||
|
||||
```typescript
|
||||
interface CalendarInviteContent {
|
||||
type: 'invite';
|
||||
permission: number; // 1=view, 4=edit, 8=invite
|
||||
action: 'pending';
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar Update Content
|
||||
|
||||
```typescript
|
||||
interface CalendarUpdateContent {
|
||||
type: 'update';
|
||||
title: string;
|
||||
action: 'updated';
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar Delete Content
|
||||
|
||||
```typescript
|
||||
interface CalendarDeleteContent {
|
||||
type: 'delete';
|
||||
title: string;
|
||||
action: 'deleted';
|
||||
}
|
||||
```
|
||||
|
||||
### Friendship Request Content
|
||||
|
||||
```typescript
|
||||
interface FriendshipContent {
|
||||
type: 'request';
|
||||
message?: string; // Optional friend request message
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Union Type
|
||||
|
||||
```typescript
|
||||
type InboxMessageContent =
|
||||
| CalendarInviteContent
|
||||
| CalendarUpdateContent
|
||||
| CalendarDeleteContent
|
||||
| FriendshipContent;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| content | jsonb | Structured content based on message_type |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Friend Request
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "request",
|
||||
"message": "Hi, let's be friends!"
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar Invite
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "invite",
|
||||
"permission": 4,
|
||||
"action": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar Update
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "update",
|
||||
"title": "Team Meeting",
|
||||
"action": "updated"
|
||||
}
|
||||
```
|
||||
|
||||
### Calendar Delete
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "delete",
|
||||
"title": "Team Meeting",
|
||||
"action": "deleted"
|
||||
}
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
# Invite Codes Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
Invite codes for referral system with reward configuration.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Reward Configuration
|
||||
|
||||
```typescript
|
||||
interface InviteCodeRewardConfig {
|
||||
// Reserved for future use
|
||||
// Currently unused, allowing custom extensions
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| reward_config | jsonb | Reward configuration for invite codes |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Empty Config
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
### Future Usage Example
|
||||
|
||||
```json
|
||||
{
|
||||
"reward_type": "credits",
|
||||
"reward_amount": 100,
|
||||
"currency": "USD",
|
||||
"max_rewards_per_user": 5
|
||||
}
|
||||
```
|
||||
@@ -1,66 +0,0 @@
|
||||
# Memories Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
User memories stored in the system with flexible content structure.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Memory Content
|
||||
|
||||
```typescript
|
||||
interface MemoryContent {
|
||||
// Reserved for future use
|
||||
// Currently unused, allowing custom extensions
|
||||
[key: string]: any;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| content | jsonb | Memory content with flexible structure |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Empty Content
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
### Future Usage Example
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Remember that user prefers morning meetings",
|
||||
"category": "preference",
|
||||
"importance": "high",
|
||||
"tags": ["meetings", "morning", "preference"]
|
||||
}
|
||||
```
|
||||
|
||||
### Agent Memory Example
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": "User discussed vacation plans for March",
|
||||
"entities": {
|
||||
"dates": ["2026-03-15", "2026-03-20"],
|
||||
"locations": ["Tokyo", "Osaka"]
|
||||
},
|
||||
"sentiment": "positive"
|
||||
}
|
||||
```
|
||||
@@ -1,88 +0,0 @@
|
||||
# Profiles Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
User profile settings with privacy and notification preferences.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Preference Settings
|
||||
|
||||
### Privacy
|
||||
|
||||
```typescript
|
||||
interface PrivacySettings {
|
||||
show_email?: boolean;
|
||||
show_online_status?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Notification
|
||||
|
||||
```typescript
|
||||
interface NotificationSettings {
|
||||
friend_request?: boolean;
|
||||
calendar_invite?: boolean;
|
||||
calendar_update?: boolean;
|
||||
calendar_delete?: boolean;
|
||||
system_message?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Profile Settings V1
|
||||
|
||||
```typescript
|
||||
interface ProfileSettingsV1 {
|
||||
privacy?: PrivacySettings;
|
||||
notification?: NotificationSettings;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| settings | jsonb | User preferences including privacy and notification settings |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Default Settings
|
||||
|
||||
```json
|
||||
{
|
||||
"privacy": {
|
||||
"show_email": false,
|
||||
"show_online_status": true
|
||||
},
|
||||
"notification": {
|
||||
"friend_request": true,
|
||||
"calendar_invite": true,
|
||||
"calendar_update": true,
|
||||
"calendar_delete": true,
|
||||
"system_message": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Settings
|
||||
|
||||
```json
|
||||
{
|
||||
"notification": {
|
||||
"friend_request": false
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,13 +0,0 @@
|
||||
# Auth Routes Protocol Notes
|
||||
|
||||
## POST `/api/v1/auth/verifications`
|
||||
|
||||
- `invite_code` is optional.
|
||||
- Recommended format is fixed `4` chars and pattern `^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$`.
|
||||
- Backend normalizes invite codes to uppercase and validates in service logic.
|
||||
- Invalid invite code values are ignored (treated as empty), and signup verification email flow still continues.
|
||||
|
||||
## Verification Token Input Convention
|
||||
|
||||
- Verification token for signup/recovery uses fixed `6` digits.
|
||||
- Client UI should use fixed-length segmented input to reduce mistyped values.
|
||||
@@ -1,239 +0,0 @@
|
||||
# Agent Runs Events and History Route Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth for agent runs event streaming and history snapshot routes.
|
||||
|
||||
## Overview
|
||||
|
||||
Defines the transport format for:
|
||||
|
||||
- `POST /api/v1/agent/runs`
|
||||
- `GET /api/v1/agent/runs/{thread_id}/events`
|
||||
- `GET /api/v1/agent/history`
|
||||
- `GET /api/v1/agent/attachments/signed-url`
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Draft (pending full backend/frontend alignment)
|
||||
|
||||
---
|
||||
|
||||
## Route Semantics
|
||||
|
||||
### `GET /api/v1/agent/history`
|
||||
|
||||
- Unified history endpoint.
|
||||
- Query params:
|
||||
- `threadId` (optional): target thread id.
|
||||
- `before` (optional, `YYYY-MM-DD`): paginate by day.
|
||||
- Behavior:
|
||||
- With `threadId`: returns that thread's day snapshot.
|
||||
- Without `threadId`: returns latest available thread snapshot for current user.
|
||||
|
||||
### `GET /api/v1/agent/attachments/signed-url`
|
||||
|
||||
- Generate temporary signed URL for attachment rendering.
|
||||
- Query params:
|
||||
- `bucket` (required)
|
||||
- `path` (required)
|
||||
- Scope rule:
|
||||
- `bucket` must match current storage bucket.
|
||||
- `path` must be within current user prefix `agent-inputs/{user_id}/`.
|
||||
|
||||
---
|
||||
|
||||
## SSE Envelope (`/events`)
|
||||
|
||||
`GET /api/v1/agent/runs/{thread_id}/events` uses `text/event-stream`.
|
||||
|
||||
Each SSE frame format:
|
||||
|
||||
```text
|
||||
id: <stream-id>
|
||||
event: <EVENT_TYPE>
|
||||
data: <JSON payload>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Type Set
|
||||
|
||||
- `RUN_STARTED`
|
||||
- `STEP_STARTED`
|
||||
- `STEP_FINISHED`
|
||||
- `TEXT_MESSAGE_START`
|
||||
- `TEXT_MESSAGE_CONTENT`
|
||||
- `TEXT_MESSAGE_END`
|
||||
- `TOOL_CALL_RESULT`
|
||||
- `RUN_FINISHED`
|
||||
- `RUN_ERROR`
|
||||
|
||||
---
|
||||
|
||||
## Common Event Fields
|
||||
|
||||
```typescript
|
||||
interface EventBase {
|
||||
type: string;
|
||||
threadId: string;
|
||||
runId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Payload Schemas
|
||||
|
||||
### Run Lifecycle
|
||||
|
||||
```typescript
|
||||
interface RunStartedEvent extends EventBase {
|
||||
type: "RUN_STARTED";
|
||||
runId: string;
|
||||
}
|
||||
|
||||
interface RunFinishedEvent extends EventBase {
|
||||
type: "RUN_FINISHED";
|
||||
runId: string;
|
||||
}
|
||||
|
||||
interface RunErrorEvent extends EventBase {
|
||||
type: "RUN_ERROR";
|
||||
runId: string;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Step Lifecycle
|
||||
|
||||
```typescript
|
||||
interface StepStartedEvent extends EventBase {
|
||||
type: "STEP_STARTED";
|
||||
runId: string;
|
||||
stepName: string;
|
||||
}
|
||||
|
||||
interface StepFinishedEvent extends EventBase {
|
||||
type: "STEP_FINISHED";
|
||||
runId: string;
|
||||
stepName: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Text Streaming
|
||||
|
||||
```typescript
|
||||
interface TextMessageStartEvent extends EventBase {
|
||||
type: "TEXT_MESSAGE_START";
|
||||
runId: string;
|
||||
messageId: string;
|
||||
role: "assistant" | "system" | "user" | "tool";
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
interface TextMessageContentEvent extends EventBase {
|
||||
type: "TEXT_MESSAGE_CONTENT";
|
||||
runId: string;
|
||||
messageId: string;
|
||||
delta: string; // incremental text chunk
|
||||
}
|
||||
|
||||
interface TextMessageEndEvent extends EventBase {
|
||||
type: "TEXT_MESSAGE_END";
|
||||
runId: string;
|
||||
messageId: string;
|
||||
workerAgentOutput: WorkerAgentOutput;
|
||||
// stage/model are intentionally excluded from this event
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Result
|
||||
|
||||
```typescript
|
||||
interface ToolCallResultEvent extends EventBase {
|
||||
type: "TOOL_CALL_RESULT";
|
||||
messageId: string;
|
||||
toolCallId: string;
|
||||
toolAgentOutput: ToolAgentOutput; // required
|
||||
}
|
||||
```
|
||||
|
||||
### Worker/Tool Payloads
|
||||
|
||||
```typescript
|
||||
interface WorkerAgentOutput {
|
||||
status: "success" | "partial_success" | "failed";
|
||||
answer: string;
|
||||
key_points?: string[];
|
||||
result_type?: string;
|
||||
suggested_actions?: string[];
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
ui_hints?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ToolAgentOutput {
|
||||
tool_name: string;
|
||||
tool_call_id: string;
|
||||
tool_call_args?: Record<string, unknown>;
|
||||
status: "success" | "partial" | "failure";
|
||||
result_summary: string;
|
||||
ui_hints?: Record<string, unknown>;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
retryable?: boolean;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## History Response Schema
|
||||
|
||||
`GET /api/v1/agent/history` returns `STATE_SNAPSHOT` payload.
|
||||
|
||||
```typescript
|
||||
interface AgentHistoryResponse {
|
||||
type: "STATE_SNAPSHOT";
|
||||
threadId?: string;
|
||||
snapshot: {
|
||||
scope: "history_day";
|
||||
threadId: string | null;
|
||||
day: string | null; // YYYY-MM-DD
|
||||
hasMore: boolean;
|
||||
messages: SnapshotMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
interface SnapshotMessage {
|
||||
id: string;
|
||||
seq: number;
|
||||
role: "user" | "assistant" | "system" | "tool";
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
timestamp: string; // ISO-8601
|
||||
}
|
||||
|
||||
interface AttachmentSignedUrlResponse {
|
||||
bucket: string;
|
||||
path: string;
|
||||
url: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
- For `TOOL_CALL_RESULT`, clients should treat `toolAgentOutput` as canonical payload.
|
||||
- `TEXT_MESSAGE_CONTENT.delta` is defined as incremental text chunk. Implementations should emit multiple chunks for real streaming UX.
|
||||
- `TEXT_MESSAGE_END` must not include `stage` or `model` in this protocol version.
|
||||
- History snapshot `messages[]` strictly follows `backend/src/schemas/messages/chat_message.py` `AgentChatMessage` schema.
|
||||
- Attachment URL rendering is decoupled from history; client should call `/api/v1/agent/attachments/signed-url` using metadata fields.
|
||||
@@ -1,106 +0,0 @@
|
||||
# Schedule Items Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
Schedule items represent calendar events with metadata, attachments, and sharing capabilities.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
```typescript
|
||||
type ScheduleItemStatus = 'active' | 'completed' | 'canceled' | 'archived';
|
||||
```
|
||||
|
||||
## Source Type
|
||||
|
||||
```typescript
|
||||
type ScheduleItemSourceType = 'manual' | 'imported' | 'agent_generated';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metadata Schema
|
||||
|
||||
### Attachment Type
|
||||
|
||||
```typescript
|
||||
type AttachmentType = 'document' | 'reminder';
|
||||
```
|
||||
|
||||
### Attachment
|
||||
|
||||
```typescript
|
||||
interface ScheduleItemMetadataAttachment {
|
||||
name: string;
|
||||
type: AttachmentType;
|
||||
visible_to: string[]; // UUIDs
|
||||
url?: string;
|
||||
note?: string;
|
||||
content?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Metadata
|
||||
|
||||
```typescript
|
||||
interface ScheduleItemMetadata {
|
||||
color?: string; // "#RRGGBB" format
|
||||
location?: string;
|
||||
notes?: string;
|
||||
attachments?: ScheduleItemMetadataAttachment[];
|
||||
reminder_minutes?: number; // 0-10080 (0 to 7 days)
|
||||
version: 1;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| metadata | jsonb | Structured metadata including color, location, notes, attachments, reminders |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Basic Metadata
|
||||
|
||||
```json
|
||||
{
|
||||
"color": "#3B82F6",
|
||||
"location": "Conference Room A",
|
||||
"notes": "Bring presentation slides",
|
||||
"reminder_minutes": 15,
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
### With Attachment
|
||||
|
||||
```json
|
||||
{
|
||||
"color": "#10B981",
|
||||
"location": "https://meet.example.com/abc123",
|
||||
"notes": "Weekly sync meeting",
|
||||
"attachments": [
|
||||
{
|
||||
"name": "agenda.pdf",
|
||||
"type": "document",
|
||||
"url": "https://storage.example.com/agenda.pdf",
|
||||
"visible_to": ["uuid1", "uuid2"]
|
||||
}
|
||||
],
|
||||
"reminder_minutes": 30,
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
@@ -1,66 +0,0 @@
|
||||
# System Agents Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
System agent configuration for LLM parameters.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Active
|
||||
|
||||
---
|
||||
|
||||
## LLM Configuration
|
||||
|
||||
```typescript
|
||||
interface SystemAgentLLMConfig {
|
||||
temperature?: number; // 0.0 - 2.0
|
||||
max_tokens?: number; // >= 1
|
||||
timeout_seconds?: number; // > 0, <= 300, default 30
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Field
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| config | jsonb | LLM configuration including temperature, max_tokens, timeout |
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Default Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 0.7,
|
||||
"max_tokens": null,
|
||||
"timeout_seconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
### Creative Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 1.2,
|
||||
"max_tokens": 2000,
|
||||
"timeout_seconds": 60
|
||||
}
|
||||
```
|
||||
|
||||
### Precise Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"temperature": 0.1,
|
||||
"max_tokens": 500,
|
||||
"timeout_seconds": 30
|
||||
}
|
||||
```
|
||||
@@ -1,466 +0,0 @@
|
||||
# UI Schema Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
A generic UI schema for rendering tool/agent execution results. Designed for AI Agent / Tool ecosystem with extensibility.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `1.0`
|
||||
- **Status**: Frozen (no new node types)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UiSchemaDocument (root) │
|
||||
│ - version / schemaType / status / docId │
|
||||
│ - meta (protocol-level metadata) │
|
||||
│ - nodes (array of UiNode) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Field Layers: │
|
||||
│ 1. Public fields (all renderers must handle) │
|
||||
│ id / type / title / description / icon / status / │
|
||||
│ timestamp / actions │
|
||||
│ 2. meta (protocol-level, not rendered) │
|
||||
│ requestId / toolId / traceId / userId │
|
||||
│ 3. extensions (tool私有扩展, renderer透传) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Types
|
||||
|
||||
### SchemaType
|
||||
|
||||
```typescript
|
||||
type SchemaType = 'tool_result' | 'agent_response' | 'notification';
|
||||
```
|
||||
|
||||
### UiStatus
|
||||
|
||||
```typescript
|
||||
type UiStatus = 'info' | 'success' | 'warning' | 'error' | 'pending';
|
||||
```
|
||||
|
||||
### IconSource
|
||||
|
||||
```typescript
|
||||
type IconSource = 'icon' | 'emoji' | 'url';
|
||||
```
|
||||
|
||||
### ActionType
|
||||
|
||||
```typescript
|
||||
type ActionType = 'navigation' | 'url' | 'event' | 'tool' | 'copy' | 'payload';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Structure
|
||||
|
||||
```typescript
|
||||
interface UiSchemaDocument {
|
||||
// Protocol identifier
|
||||
version: string; // "1.0"
|
||||
schemaType: SchemaType; // tool_result | agent_response | notification
|
||||
|
||||
// Document metadata
|
||||
docId?: string; // For local refresh / diff / analytics
|
||||
timestamp?: string; // ISO 8601
|
||||
locale?: string; // "zh-CN"
|
||||
|
||||
// Unified status
|
||||
status: UiStatus;
|
||||
|
||||
// Render control
|
||||
renderer?: {
|
||||
renderer?: string; // Dedicated renderer name
|
||||
theme?: 'default' | 'dark' | 'light';
|
||||
};
|
||||
|
||||
// Protocol-level metadata (not rendered)
|
||||
meta?: {
|
||||
requestId?: string;
|
||||
toolId?: string;
|
||||
traceId?: string;
|
||||
userId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
// Root nodes
|
||||
nodes: UiNode[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node Types (v1 Whitelist)
|
||||
|
||||
```
|
||||
✅ Supported in v1:
|
||||
- card 卡片
|
||||
- list 列表
|
||||
- table 表格
|
||||
- text 文本/Markdown
|
||||
- kv 键值对
|
||||
- operation 操作结果
|
||||
- error 错误提示
|
||||
- container 容器
|
||||
|
||||
❌ Not supported in v1 (reserved for v2):
|
||||
- chart / metric / image / video / tabs / accordion / form
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Node Fields
|
||||
|
||||
All nodes share these fields:
|
||||
|
||||
```typescript
|
||||
interface UiBaseNode {
|
||||
id?: string; // For local refresh / action targeting
|
||||
type: string; // Node type identifier
|
||||
}
|
||||
|
||||
interface UiTitledNode extends UiBaseNode {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: UiIcon;
|
||||
status?: UiStatus;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
interface UiActionableNode extends UiTitledNode {
|
||||
actions?: UiAction[];
|
||||
}
|
||||
|
||||
interface UiExtendableNode extends UiActionableNode {
|
||||
extensions?: Record<string, any>; // Tool私有扩展, 通用renderer透传
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node Definitions
|
||||
|
||||
### 1. Card Node
|
||||
|
||||
```typescript
|
||||
interface UiCardNode extends UiExtendableNode {
|
||||
type: 'card';
|
||||
children?: UiNode[]; // Nested nodes
|
||||
footer?: UiTextNode;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. List Node
|
||||
|
||||
```typescript
|
||||
interface UiListNode extends UiExtendableNode {
|
||||
type: 'list';
|
||||
items: ListItem[];
|
||||
pagination?: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
};
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
interface ListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
icon?: UiIcon;
|
||||
badge?: { label: string; variant?: 'default' | 'success' | 'warning' | 'error' | 'info' };
|
||||
metadata?: Record<string, any>;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Table Node
|
||||
|
||||
```typescript
|
||||
interface UiTableNode extends UiExtendableNode {
|
||||
type: 'table';
|
||||
columns: TableColumn[];
|
||||
rows: TableRow[];
|
||||
pagination?: Pagination;
|
||||
}
|
||||
|
||||
interface TableColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
width?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
interface TableRow {
|
||||
id: string;
|
||||
cells: Record<string, any>;
|
||||
metadata?: Record<string, any>;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Text Node
|
||||
|
||||
```typescript
|
||||
interface UiTextNode extends UiBaseNode {
|
||||
type: 'text';
|
||||
content: string;
|
||||
format?: 'plain' | 'markdown';
|
||||
icon?: UiIcon;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Key-Value Node
|
||||
|
||||
```typescript
|
||||
interface UiKvNode extends UiExtendableNode {
|
||||
type: 'kv';
|
||||
pairs: KeyValuePair[];
|
||||
layout?: 'vertical' | 'horizontal' | 'grid';
|
||||
}
|
||||
|
||||
interface KeyValuePair {
|
||||
key: string;
|
||||
label?: string;
|
||||
value: string | number | boolean;
|
||||
copyable?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Operation Node
|
||||
|
||||
```typescript
|
||||
interface UiOperationNode extends UiExtendableNode {
|
||||
type: 'operation';
|
||||
operation: 'create' | 'update' | 'delete' | 'execute';
|
||||
result: 'success' | 'failure' | 'partial';
|
||||
message?: string;
|
||||
affectedCount?: number;
|
||||
details?: UiNode;
|
||||
rollback?: UiAction;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Error Node
|
||||
|
||||
```typescript
|
||||
interface UiErrorNode extends UiBaseNode {
|
||||
type: 'error';
|
||||
title?: string;
|
||||
icon?: UiIcon;
|
||||
errorCode: string;
|
||||
message: string;
|
||||
details?: string;
|
||||
stack?: string;
|
||||
retryable: boolean;
|
||||
suggestions?: string[];
|
||||
retry?: UiAction;
|
||||
support?: UiAction;
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Container Node
|
||||
|
||||
```typescript
|
||||
interface UiContainerNode extends UiBaseNode {
|
||||
type: 'container';
|
||||
direction: 'vertical' | 'horizontal';
|
||||
gap?: number;
|
||||
children: UiNode[];
|
||||
actions?: UiAction[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Action Structure
|
||||
|
||||
```typescript
|
||||
interface UiAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: UiIcon;
|
||||
style?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
disabled?: boolean;
|
||||
action: ActionSpec;
|
||||
confirm?: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ActionSpec =
|
||||
| { 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 };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Structure
|
||||
|
||||
```typescript
|
||||
interface UiIcon {
|
||||
source: IconSource; // 'icon' | 'emoji' | 'url'
|
||||
value: string; // icon name / emoji / URL
|
||||
color?: string; // "#FF0000"
|
||||
size?: number; // pixels
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Rules
|
||||
|
||||
### operation vs error
|
||||
|
||||
| Scenario | Node Type |
|
||||
|----------|-----------|
|
||||
| Tool execution failed, system exception | `UiErrorNode` |
|
||||
| Business operation result (CRUD) | `UiOperationNode` |
|
||||
| Network error / permission denied | `UiErrorNode` |
|
||||
|
||||
### extensions Usage Constraints
|
||||
|
||||
1. ❌ NO: Any rendering-related style / text / layout
|
||||
2. ❌ NO: Any fields other renderers need to read
|
||||
3. ✅ YES: Business identifiers (eventId, orderId)
|
||||
4. ✅ YES: Dedicated renderer private config
|
||||
5. ✅ YES: Analytics / logging context data
|
||||
6. ✅ YES: Data that generic renderer doesn't care about
|
||||
|
||||
---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Example 1: Success Card
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"docId": "doc_evt_001",
|
||||
"timestamp": "2026-03-12T10:30:00Z",
|
||||
"status": "success",
|
||||
"renderer": { "renderer": "calendar" },
|
||||
"meta": { "requestId": "req_abc", "toolId": "calendar.create_event" },
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_card_1",
|
||||
"type": "card",
|
||||
"title": "日程已创建",
|
||||
"description": "会议日程创建成功",
|
||||
"icon": { "source": "icon", "value": "event_available" },
|
||||
"extensions": { "eventId": "evt_abc123", "color": "#3B82F6" },
|
||||
"children": [
|
||||
{
|
||||
"type": "kv",
|
||||
"pairs": [
|
||||
{ "key": "title", "label": "主题", "value": "Q1 规划会议" },
|
||||
{ "key": "time", "label": "时间", "value": "2026-03-15 14:00" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "action_view",
|
||||
"label": "查看详情",
|
||||
"style": "primary",
|
||||
"action": { "type": "navigation", "path": "/calendar/evt_abc123" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: List Result
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"status": "success",
|
||||
"meta": { "toolId": "search.documents" },
|
||||
"nodes": [
|
||||
{ "type": "text", "content": "找到 **3** 个相关文档" },
|
||||
{
|
||||
"id": "node_list_1",
|
||||
"type": "list",
|
||||
"items": [
|
||||
{
|
||||
"id": "item_1",
|
||||
"title": "API 设计规范",
|
||||
"subtitle": "v2.1",
|
||||
"icon": { "source": "emoji", "value": "📄" },
|
||||
"actions": [
|
||||
{ "id": "a1", "label": "查看", "action": { "type": "url", "url": "/docs/api" } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": { "page": 1, "pageSize": 10, "total": 3, "hasMore": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Error Result
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"schemaType": "tool_result",
|
||||
"status": "error",
|
||||
"meta": { "toolId": "user.delete" },
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node_error_1",
|
||||
"type": "error",
|
||||
"title": "删除用户失败",
|
||||
"icon": { "source": "icon", "value": "error_outline" },
|
||||
"errorCode": "PERMISSION_DENIED",
|
||||
"message": "您没有权限执行此操作",
|
||||
"details": "需要管理员权限",
|
||||
"retryable": true,
|
||||
"suggestions": ["联系管理员", "联系技术支持"],
|
||||
"retry": {
|
||||
"id": "action_retry",
|
||||
"label": "重试",
|
||||
"style": "primary",
|
||||
"action": { "type": "tool", "toolId": "user.delete", "params": { "userId": "u1" } }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Evolution (v2)
|
||||
|
||||
Reserved for future:
|
||||
|
||||
- Nested containers: `tabs`, `accordion`, `carousel`
|
||||
- Data visualization: `chart`, `metric`, `progress`
|
||||
- Rich media: `image`, `video`, `audio`, `file`
|
||||
- Internationalization: `i18nKey` field
|
||||
- Version migration: `deprecated` flag
|
||||
- Offline support: `offline` flag
|
||||
@@ -0,0 +1,432 @@
|
||||
# 前后端数据流通指南
|
||||
|
||||
本文档描述前端如何与后端 Agent 系统交互,以及如何渲染后端返回的 UI 数据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 整体交互流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 前端 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────┐ 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.1 前端上传附件流程
|
||||
|
||||
```javascript
|
||||
// 方式一:先上传附件,获取签名 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(如果有)
|
||||
const imageUrl = 'https://storage.example.com/...';
|
||||
```
|
||||
|
||||
### 2.2 构造 RunAgentInput
|
||||
|
||||
```javascript
|
||||
const runInput = {
|
||||
threadId: threadId,
|
||||
runId: `run-${Date.now()}`,
|
||||
state: {},
|
||||
messages: [
|
||||
{
|
||||
id: `msg-${Date.now()}`,
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: '这张图片里有什么?' },
|
||||
{
|
||||
type: 'binary',
|
||||
mimeType: 'image/png',
|
||||
url: attachment.url // 签名 URL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
tools: [], // 或传入工具定义
|
||||
context: [],
|
||||
forwardedProps: {}
|
||||
};
|
||||
```
|
||||
|
||||
### 2.3 发送请求
|
||||
|
||||
```javascript
|
||||
const response = await fetch('/api/v1/agent/runs', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(runInput)
|
||||
});
|
||||
// 返回 202 Accepted
|
||||
const { taskId, threadId, runId, created } = await response.json();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 事件流渲染
|
||||
|
||||
### 3.1 订阅事件
|
||||
|
||||
```javascript
|
||||
class AgentEventHandler {
|
||||
constructor(threadId) {
|
||||
this.eventSource = new EventSource(`/api/v1/agent/runs/${threadId}/events`);
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
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 事件处理与渲染
|
||||
|
||||
#### text.delta - 文本增量
|
||||
|
||||
```javascript
|
||||
onTextDelta(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
const { delta, messageId } = data.data;
|
||||
// 追加到对应消息的文本内容
|
||||
appendTextToMessage(messageId, delta);
|
||||
}
|
||||
```
|
||||
|
||||
#### tool.result - 工具结果
|
||||
|
||||
```javascript
|
||||
onToolResult(event) {
|
||||
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 对话历史渲染
|
||||
|
||||
### 4.1 获取历史
|
||||
|
||||
```javascript
|
||||
const response = await fetch(
|
||||
`/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 渲染组件
|
||||
@@ -0,0 +1,747 @@
|
||||
# UI Schema Protocol
|
||||
|
||||
> **NOTE**: This document is the single source of truth. All implementations must follow this specification.
|
||||
|
||||
## Overview
|
||||
|
||||
A generic UI schema for rendering tool/agent execution results. Designed for AI Agent / Tool ecosystem with extensibility.
|
||||
|
||||
**Design Philosophy**: Keep only "primitive components + layout containers". Frontend only needs to recursively render the root layout tree.
|
||||
|
||||
## Version
|
||||
|
||||
- **Current**: `2.0`
|
||||
- **Status**: Active
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ UiSchemaRenderer (root) │
|
||||
│ - version / locale / status / theme │
|
||||
│ - meta (protocol-level metadata) │
|
||||
│ - root (UiLayoutNode - stack or grid) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Rendering Flow: │
|
||||
│ 1. Backend returns UiSchemaRenderer with root layout │
|
||||
│ 2. Frontend recursively renders root layout tree │
|
||||
│ 3. Layout nodes (stack/grid) contain children │
|
||||
│ 4. Primitive nodes (text/icon/button/etc) are leaves │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Types
|
||||
|
||||
### UiStatus
|
||||
|
||||
```typescript
|
||||
type UiStatus = 'info' | 'success' | 'warning' | 'error' | 'pending';
|
||||
```
|
||||
|
||||
### IconSource
|
||||
|
||||
```typescript
|
||||
type IconSource = 'icon' | 'emoji' | 'url';
|
||||
```
|
||||
|
||||
### TextFormat
|
||||
|
||||
```typescript
|
||||
type TextFormat = 'plain' | 'markdown';
|
||||
```
|
||||
|
||||
### TextRole
|
||||
|
||||
```typescript
|
||||
type TextRole = 'title' | 'subtitle' | 'body' | 'caption' | 'code';
|
||||
```
|
||||
|
||||
### ButtonStyle
|
||||
|
||||
```typescript
|
||||
type ButtonStyle = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
```
|
||||
|
||||
### LayoutDirection
|
||||
|
||||
```typescript
|
||||
type LayoutDirection = 'vertical' | 'horizontal';
|
||||
```
|
||||
|
||||
### LayoutAppearance
|
||||
|
||||
```typescript
|
||||
type LayoutAppearance = 'plain' | 'card' | 'section';
|
||||
```
|
||||
|
||||
### LayoutAlign
|
||||
|
||||
```typescript
|
||||
type LayoutAlign = 'start' | 'center' | 'end' | 'stretch';
|
||||
```
|
||||
|
||||
### LayoutJustify
|
||||
|
||||
```typescript
|
||||
type LayoutJustify = 'start' | 'center' | 'end' | 'space-between';
|
||||
```
|
||||
|
||||
### RendererTheme
|
||||
|
||||
```typescript
|
||||
type RendererTheme = 'default' | 'light' | 'dark';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root Structure
|
||||
|
||||
```typescript
|
||||
interface UiSchemaRenderer {
|
||||
version: string; // "2.0"
|
||||
locale: string; // "zh-CN"
|
||||
status: UiStatus;
|
||||
theme: RendererTheme;
|
||||
|
||||
// Protocol-level metadata (not rendered)
|
||||
meta?: {
|
||||
requestId?: string;
|
||||
toolId?: string;
|
||||
traceId?: string;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
// Root layout node
|
||||
root: UiLayoutNode;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node Types
|
||||
|
||||
### Primitive Components
|
||||
|
||||
#### 1. Text Node
|
||||
|
||||
```typescript
|
||||
interface UiTextNode extends UiBaseNode {
|
||||
type: 'text';
|
||||
content: string;
|
||||
format: TextFormat; // 'plain' | 'markdown'
|
||||
role: TextRole; // 'title' | 'subtitle' | 'body' | 'caption' | 'code'
|
||||
status?: UiStatus;
|
||||
maxLines?: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Icon Node
|
||||
|
||||
```typescript
|
||||
interface UiIconNode extends UiBaseNode {
|
||||
type: 'icon';
|
||||
source: IconSource; // 'icon' | 'emoji' | 'url'
|
||||
value: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Badge Node
|
||||
|
||||
```typescript
|
||||
interface UiBadgeNode extends UiBaseNode {
|
||||
type: 'badge';
|
||||
label: string;
|
||||
status: UiStatus;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Button Node
|
||||
|
||||
```typescript
|
||||
interface UiButtonNode extends UiBaseNode {
|
||||
type: 'button';
|
||||
label: string;
|
||||
style: ButtonStyle;
|
||||
disabled?: boolean;
|
||||
icon?: UiIconSpec;
|
||||
action: UiActionPayload;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Key-Value Node
|
||||
|
||||
```typescript
|
||||
interface UiKvNode extends UiBaseNode {
|
||||
type: 'kv';
|
||||
items: UiKvItem[];
|
||||
columns?: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
interface UiKvItem {
|
||||
key: string;
|
||||
label?: string;
|
||||
value: any;
|
||||
copyable?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Divider Node
|
||||
|
||||
```typescript
|
||||
interface UiDividerNode extends UiBaseNode {
|
||||
type: 'divider';
|
||||
inset?: number;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Layout Containers
|
||||
|
||||
#### 7. Stack Node
|
||||
|
||||
```typescript
|
||||
interface UiStackNode extends UiBaseNode {
|
||||
type: 'stack';
|
||||
direction: LayoutDirection; // 'vertical' | 'horizontal'
|
||||
gap?: number;
|
||||
appearance: LayoutAppearance; // 'plain' | 'card' | 'section'
|
||||
status?: UiStatus;
|
||||
align?: LayoutAlign;
|
||||
justify?: LayoutJustify;
|
||||
wrap?: boolean;
|
||||
children: UiNode[];
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 8. Grid Node
|
||||
|
||||
```typescript
|
||||
interface UiGridNode extends UiBaseNode {
|
||||
type: 'grid';
|
||||
columns: number;
|
||||
gap?: number;
|
||||
appearance: LayoutAppearance;
|
||||
status?: UiStatus;
|
||||
children: UiNode[];
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Base Node
|
||||
|
||||
```typescript
|
||||
interface UiBaseNode {
|
||||
id?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Node Union
|
||||
|
||||
```typescript
|
||||
type UiNode =
|
||||
| UiTextNode
|
||||
| UiIconNode
|
||||
| UiBadgeNode
|
||||
| UiButtonNode
|
||||
| UiKvNode
|
||||
| UiDividerNode
|
||||
| UiStackNode
|
||||
| UiGridNode;
|
||||
|
||||
type UiLayoutNode = UiStackNode | UiGridNode;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Action Payloads
|
||||
|
||||
```typescript
|
||||
type UiActionPayload =
|
||||
| NavigateAction
|
||||
| UrlAction
|
||||
| EventAction
|
||||
| ToolAction
|
||||
| CopyAction
|
||||
| PayloadAction;
|
||||
|
||||
// Navigation action
|
||||
interface NavigateAction {
|
||||
type: 'navigation';
|
||||
path: string;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
// URL action
|
||||
interface UrlAction {
|
||||
type: 'url';
|
||||
url: string;
|
||||
target?: '_self' | '_blank';
|
||||
}
|
||||
|
||||
// Event action
|
||||
interface EventAction {
|
||||
type: 'event';
|
||||
event: string;
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
// Tool action
|
||||
interface ToolAction {
|
||||
type: 'tool';
|
||||
toolId: string;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
// Copy action
|
||||
interface CopyAction {
|
||||
type: 'copy';
|
||||
content: string;
|
||||
successMessage?: string;
|
||||
}
|
||||
|
||||
// Payload action
|
||||
interface PayloadAction {
|
||||
type: 'payload';
|
||||
payload: Record<string, any>;
|
||||
submitTo?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Icon Specification
|
||||
|
||||
```typescript
|
||||
interface UiIconSpec {
|
||||
source: IconSource;
|
||||
value: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns---
|
||||
|
||||
## JSON Examples
|
||||
|
||||
### Example 1: Simple Text
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "success",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 12,
|
||||
"appearance": "plain",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"content": "操作成功",
|
||||
"role": "title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "您的事项已创建完成",
|
||||
"role": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Card with Actions
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "success",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 16,
|
||||
"appearance": "card",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"content": "日程已创建",
|
||||
"role": "title"
|
||||
},
|
||||
{
|
||||
"type": "kv",
|
||||
"items": [
|
||||
{ "key": "title", "label": "主题", "value": "Q1 规划会议" },
|
||||
{ "key": "time", "label": "时间", "value": "2026-03-15 14:00" }
|
||||
],
|
||||
"columns": 2
|
||||
},
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "horizontal",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "button",
|
||||
"label": "查看详情",
|
||||
"style": "primary",
|
||||
"action": { "type": "navigation", "path": "/calendar/evt_abc123" }
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"label": "删除",
|
||||
"style": "danger",
|
||||
"action": { "type": "tool", "toolId": "calendar.delete", "params": { "eventId": "evt_abc123" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Error Status Panel
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "error",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 12,
|
||||
"appearance": "card",
|
||||
"status": "error",
|
||||
"children": [
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "horizontal",
|
||||
"gap": 8,
|
||||
"align": "center",
|
||||
"justify": "space-between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"content": "删除失败",
|
||||
"role": "title"
|
||||
},
|
||||
{
|
||||
"type": "badge",
|
||||
"label": "ERROR",
|
||||
"status": "error"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"content": "您没有权限执行此操作",
|
||||
"role": "body",
|
||||
"status": "error"
|
||||
},
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "horizontal",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "button",
|
||||
"label": "重试",
|
||||
"style": "primary",
|
||||
"action": { "type": "tool", "toolId": "user.delete", "params": { "userId": "u1" } }
|
||||
},
|
||||
{
|
||||
"type": "button",
|
||||
"label": "联系管理员",
|
||||
"style": "secondary",
|
||||
"action": { "type": "url", "url": "mailto:admin@example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Grid Layout
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "info",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "grid",
|
||||
"columns": 3,
|
||||
"gap": 16,
|
||||
"appearance": "plain",
|
||||
"children": [
|
||||
{
|
||||
"type": "card",
|
||||
"children": [
|
||||
{ "type": "text", "content": "今日订单", "role": "title" },
|
||||
{ "type": "text", "content": "128", "role": "subtitle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"children": [
|
||||
{ "type": "text", "content": "待处理", "role": "title" },
|
||||
{ "type": "text", "content": "24", "role": "subtitle", "status": "warning" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"children": [
|
||||
{ "type": "text", "content": "总收入", "role": "title" },
|
||||
{ "type": "text", "content": "¥8,640", "role": "subtitle", "status": "success" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 5: Section Layout
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "success",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 24,
|
||||
"appearance": "plain",
|
||||
"children": [
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 12,
|
||||
"appearance": "section",
|
||||
"children": [
|
||||
{ "type": "text", "content": "基本信息", "role": "title" },
|
||||
{ "type": "kv", "items": [
|
||||
{ "key": "name", "label": "姓名", "value": "张三" },
|
||||
{ "key": "email", "label": "邮箱", "value": "zhangsan@example.com", "copyable": true }
|
||||
]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "vertical",
|
||||
"gap": 12,
|
||||
"appearance": "section",
|
||||
"children": [
|
||||
{ "type": "text", "content": "账户设置", "role": "title" },
|
||||
{ "type": "button", "label": "修改密码", "style": "secondary", "action": { "type": "navigation", "path": "/settings/password" } },
|
||||
{ "type": "button", "label": "退出登录", "style": "ghost", "action": { "type": "event", "event": "logout" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## UiHints (Descriptive UI)
|
||||
|
||||
### Overview
|
||||
|
||||
UiHints is a **descriptive** UI representation designed for AI agents to express UI intent with minimal token cost. It describes **what to show**, not **how to render**.
|
||||
|
||||
The `ui_compiler` transforms UiHints into UiSchemaRenderer for frontend rendering.
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Descriptive not Rendered**: Express content intent, not visual instructions
|
||||
2. **Minimal Token Cost**: Simple structure, semantic field names
|
||||
3. **Composable**: Supports nested sections and mixed content
|
||||
4. **Compilable**: Mechanical transformation to UiSchemaRenderer
|
||||
5. **Lossless**: Main content fields in hints are preserved in renderer
|
||||
|
||||
### Intent Types
|
||||
|
||||
Intent is a **weak hint** - it only affects default layout style, not field presence.
|
||||
|
||||
| Intent | Default Layout |
|
||||
|--------|----------------|
|
||||
| `message` | plain |
|
||||
| `data` | card |
|
||||
| `list` | plain |
|
||||
| `status` | card |
|
||||
| `form` | section |
|
||||
| `mixed` | card |
|
||||
|
||||
### UiHints Payload
|
||||
|
||||
```typescript
|
||||
interface UiHintsPayload {
|
||||
version: string; // "2.1"
|
||||
intent: UiHintIntent; // Primary display intent (weak hint)
|
||||
status: UiStatus; // Overall status
|
||||
|
||||
title?: string; // Top-level title
|
||||
description?: string; // Top-level description
|
||||
body?: string; // Top-level main body text
|
||||
bodyFormat?: "plain" | "markdown"; // Body text format
|
||||
|
||||
items?: UiHintKvItem[]; // Top-level key-value items
|
||||
listItems?: UiHintListItem[]; // Top-level list items
|
||||
sections?: UiHintSection[]; // Grouped sections
|
||||
actions?: UiHintAction[]; // Top-level actions
|
||||
icon?: UiHintIcon; // Top-level icon
|
||||
meta?: Record<string, any>; // Extra meta
|
||||
}
|
||||
|
||||
interface UiHintSection {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: UiHintIcon;
|
||||
content?: string;
|
||||
contentFormat?: "plain" | "markdown";
|
||||
items?: UiHintKvItem[];
|
||||
listItems?: UiHintListItem[];
|
||||
actions?: UiHintAction[];
|
||||
}
|
||||
|
||||
interface UiHintListItem {
|
||||
id?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
icon?: UiHintIcon;
|
||||
status?: UiHintStatus;
|
||||
actions?: UiHintAction[];
|
||||
}
|
||||
|
||||
interface UiHintKvItem {
|
||||
key: string;
|
||||
label?: string;
|
||||
value?: any;
|
||||
copyable?: boolean;
|
||||
}
|
||||
|
||||
interface UiHintAction {
|
||||
label: string;
|
||||
style?: "primary" | "secondary" | "ghost" | "danger";
|
||||
disabled?: boolean;
|
||||
action: UiHintActionTarget;
|
||||
}
|
||||
|
||||
type UiHintActionTarget =
|
||||
| { 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 };
|
||||
|
||||
interface UiHintIcon {
|
||||
source: "icon" | "emoji" | "url";
|
||||
value: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Compilation Flow
|
||||
|
||||
```
|
||||
Agent Output (UiHints 2.1)
|
||||
│
|
||||
▼
|
||||
ui_compiler
|
||||
│
|
||||
▼
|
||||
UiSchemaRenderer (for frontend rendering)
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
**Input (UiHints)**:
|
||||
```json
|
||||
{
|
||||
"intent": "status",
|
||||
"status": "success",
|
||||
"title": "日程已创建",
|
||||
"body": "本次创建已成功完成。",
|
||||
"items": [
|
||||
{"key": "title", "label": "主题", "value": "Q1 规划会议"},
|
||||
{"key": "time", "label": "时间", "value": "2026-03-15 14:00"}
|
||||
],
|
||||
"actions": [
|
||||
{"label": "查看详情", "style": "primary", "action": {"type": "navigation", "path": "/calendar/evt_123"}},
|
||||
{"label": "删除", "style": "danger", "action": {"type": "tool", "toolId": "calendar.delete", "params": {"eventId": "evt_123"}}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Output (UiSchemaRenderer)**:
|
||||
```json
|
||||
{
|
||||
"version": "2.0",
|
||||
"locale": "zh-CN",
|
||||
"status": "success",
|
||||
"theme": "default",
|
||||
"root": {
|
||||
"type": "stack",
|
||||
"appearance": "card",
|
||||
"status": "success",
|
||||
"children": [
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "horizontal",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{"type": "text", "content": "日程已创建", "role": "title"},
|
||||
{"type": "badge", "label": "SUCCESS", "status": "success"}
|
||||
],
|
||||
"justify": "space-between",
|
||||
"align": "center"
|
||||
},
|
||||
{"type": "text", "content": "本次创建已成功完成。", "role": "body"},
|
||||
{"type": "kv", "items": [...]},
|
||||
{
|
||||
"type": "stack",
|
||||
"direction": "horizontal",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{"type": "button", "label": "查看详情", "style": "primary", ...},
|
||||
{"type": "button", "label": "删除", "style": "danger", ...}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Implementation
|
||||
|
||||
- `schemas.agent.ui_hints.UiHintsPayload` - Descriptive UI model (v2.1)
|
||||
- `core.agentscope.runtime.ui_compiler.compile(hints)` - Compile to UiSchemaRenderer
|
||||
Reference in New Issue
Block a user