c3192a2431
- Add ChatBubble reusable widget for chat messages - Add HomeMockData for chat list mock data - Add HomeScreen widget tests - Add AG-UI chat design and implementation plan docs - Add friendship design docs - Ignore backend/logs directory
568 lines
13 KiB
Markdown
568 lines
13 KiB
Markdown
# AG-UI 聊天功能设计文档
|
||
|
||
## 1. 概述
|
||
|
||
本文档描述如何使用 AG-UI 协议实现 AI 聊天功能,包括:
|
||
- 消息的发送与接收(通过 AG-UI 事件流)
|
||
- AI 工具调用(Tool Call)机制
|
||
- 日历卡片作为 Tool Result 渲染
|
||
- 前端工具注册与执行
|
||
- 本地持久化
|
||
|
||
## 2. 架构设计
|
||
|
||
### 2.1 整体流程
|
||
|
||
```
|
||
用户输入消息
|
||
↓
|
||
AgUiService.sendMessage()
|
||
↓
|
||
[Mock Mode] 规则引擎决策 → 事件流模拟
|
||
[Real Mode] POST /api/chat → SSE 监听
|
||
↓
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ AG-UI Event Stream (按序处理) │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT* → TEXT_MESSAGE_END │
|
||
│ TOOL_CALL_START → TOOL_CALL_ARGS* → TOOL_CALL_END │
|
||
│ TOOL_CALL_RESULT │
|
||
│ RUN_STARTED → ... → RUN_FINISHED │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
↓
|
||
ChatListItem 渲染
|
||
```
|
||
|
||
### 2.2 核心组件
|
||
|
||
| 组件 | 职责 |
|
||
|------|------|
|
||
| `AgUiEvent` | AG-UI 事件数据模型 |
|
||
| `AgUiService` | 事件流处理:发送消息、解析事件 |
|
||
| `ToolRegistry` | 前端工具注册表:定义工具 + handler |
|
||
| `AiDecisionEngine` | Mock 模式:规则引擎决定是否调用工具 |
|
||
| `UiSchemaParser` | 解析 tool result 中的 UI Schema |
|
||
| `UiSchemaRenderer` | 根据 schema 渲染对应组件 |
|
||
| `ChatHistoryRepository` | 本地持久化:IndexedDB/localStorage |
|
||
|
||
### 2.3 状态管理
|
||
|
||
```
|
||
ChatState {
|
||
messages: ChatListItem[] // 渲染列表
|
||
pendingToolCalls: Map<call_id, ToolCallState>
|
||
isLoading: bool
|
||
runId: string | null
|
||
}
|
||
```
|
||
|
||
## 3. 数据模型
|
||
|
||
### 3.1 AG-UI 事件模型
|
||
|
||
```dart
|
||
// 基类
|
||
abstract class AgUiEvent {
|
||
final String type;
|
||
final String? timestamp;
|
||
}
|
||
|
||
// 生命周期事件
|
||
class RunStartedEvent extends AgUiEvent {
|
||
final String threadId;
|
||
final String runId;
|
||
final String? parentRunId;
|
||
}
|
||
|
||
class RunFinishedEvent extends AgUiEvent {
|
||
final String threadId;
|
||
final String runId;
|
||
final dynamic result;
|
||
}
|
||
|
||
// 文本消息事件
|
||
class TextMessageStartEvent extends AgUiEvent {
|
||
final String messageId;
|
||
final String role; // "user" | "assistant" | "system"
|
||
}
|
||
|
||
class TextMessageContentEvent extends AgUiEvent {
|
||
final String messageId;
|
||
final String delta;
|
||
}
|
||
|
||
class TextMessageEndEvent extends AgUiEvent {
|
||
final String messageId;
|
||
}
|
||
|
||
// 工具调用事件
|
||
class ToolCallStartEvent extends AgUiEvent {
|
||
final String toolCallId;
|
||
final String toolCallName;
|
||
final String? parentMessageId;
|
||
}
|
||
|
||
class ToolCallArgsEvent extends AgUiEvent {
|
||
final String toolCallId;
|
||
final String delta; // JSON fragment
|
||
}
|
||
|
||
class ToolCallEndEvent extends AgUiEvent {
|
||
final String toolCallId;
|
||
}
|
||
|
||
class ToolCallResultEvent extends AgUiEvent {
|
||
final String messageId;
|
||
final String toolCallId;
|
||
final ToolResult result; // 给 AI 的原始结果
|
||
final UiCard? ui; // 给 UI 的渲染数据
|
||
}
|
||
|
||
class ToolCallErrorEvent extends AgUiEvent {
|
||
final String toolCallId;
|
||
final String error;
|
||
final String? code;
|
||
}
|
||
```
|
||
|
||
### 3.2 Tool Result Schema(v1)
|
||
|
||
```json
|
||
{
|
||
"type": "tool_result",
|
||
"version": "v1",
|
||
"call_id": "call_abc123",
|
||
"tool_name": "create_calendar_event",
|
||
"result": {
|
||
"eventId": "evt_xxx",
|
||
"ok": true,
|
||
"message": "日程已创建"
|
||
},
|
||
"ui": {
|
||
"type": "card",
|
||
"cardType": "calendar_card.v1",
|
||
"data": {
|
||
"id": "evt_xxx",
|
||
"title": "产品评审会议",
|
||
"description": "讨论Q2路线图",
|
||
"startAt": "2026-03-01T10:00:00+08:00",
|
||
"endAt": "2026-03-01T11:00:00+08:00",
|
||
"timezone": "Asia/Shanghai",
|
||
"location": "会议室A",
|
||
"color": "#4F46E5",
|
||
"sourceType": "agentGenerated"
|
||
},
|
||
"actions": [
|
||
{"type": "open", "label": "打开", "target": "calendar/evt_xxx"},
|
||
{"type": "edit", "label": "编辑", "action": "edit_event"},
|
||
{"type": "delete", "label": "删除", "action": "delete_event"}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 工具定义(前端 Tool Registry)
|
||
|
||
```dart
|
||
// 工具定义
|
||
class ToolDefinition {
|
||
final String name;
|
||
final String description;
|
||
final Map<String, dynamic> parameters;
|
||
final ToolHandler handler;
|
||
}
|
||
|
||
// create_calendar_event 工具
|
||
{
|
||
"name": "create_calendar_event",
|
||
"description": "创建一个日历事件或待办事项",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"title": {
|
||
"type": "string",
|
||
"description": "事件标题",
|
||
"minLength": 1,
|
||
"maxLength": 100
|
||
},
|
||
"description": {
|
||
"type": "string",
|
||
"description": "事件描述"
|
||
},
|
||
"startAt": {
|
||
"type": "string",
|
||
"format": "date-time",
|
||
"description": "开始时间 (ISO8601)"
|
||
},
|
||
"endAt": {
|
||
"type": "string",
|
||
"format": "date-time",
|
||
"description": "结束时间 (ISO8601)"
|
||
},
|
||
"timezone": {
|
||
"type": "string",
|
||
"default": "Asia/Shanghai"
|
||
},
|
||
"location": {
|
||
"type": "string"
|
||
},
|
||
"notes": {
|
||
"type": "string"
|
||
}
|
||
},
|
||
"required": ["title", "startAt"]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.4 ChatListItem 模型
|
||
|
||
```dart
|
||
enum ChatItemType {
|
||
message, // 纯文本消息
|
||
toolCall, // 工具调用中
|
||
toolResult, // 工具结果卡片
|
||
schedule // 日历事件(兼容旧数据)
|
||
}
|
||
|
||
abstract class ChatListItem {
|
||
String get id;
|
||
DateTime get timestamp;
|
||
ChatItemType get type;
|
||
MessageSender get sender;
|
||
}
|
||
|
||
class TextMessageItem extends ChatListItem {
|
||
final String id;
|
||
final String content;
|
||
final DateTime timestamp;
|
||
final MessageSender sender;
|
||
final bool isStreaming; // 是否正在流式输出
|
||
}
|
||
|
||
class ToolCallItem extends ChatListItem {
|
||
final String id;
|
||
final String callId;
|
||
final String toolName;
|
||
final Map<String, dynamic> args; // 解析后的参数
|
||
final ToolCallStatus status; // pending | executing | completed | error
|
||
final ToolResult? result;
|
||
final UiCard? uiCard;
|
||
}
|
||
|
||
class CalendarCardItem extends ChatListItem {
|
||
final String id;
|
||
final String callId; // 关联的 tool call
|
||
final CalendarCardData data;
|
||
final List<CardAction> actions;
|
||
}
|
||
```
|
||
|
||
## 4. 核心流程
|
||
|
||
### 4.1 发送消息
|
||
|
||
```dart
|
||
Future<void> sendMessage(String content) async {
|
||
// 1. 添加用户消息到列表
|
||
final userMessage = TextMessageItem(
|
||
id: generateId(),
|
||
content: content,
|
||
timestamp: DateTime.now(),
|
||
sender: MessageSender.user,
|
||
);
|
||
_chatItems.add(userMessage);
|
||
|
||
// 2. 发起请求
|
||
if (Env.isMockApi) {
|
||
await _mockEventStream(content);
|
||
} else {
|
||
await _realEventStream(content);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 Mock 事件流(规则引擎)
|
||
|
||
```dart
|
||
class AiDecisionEngine {
|
||
// 意图关键词映射
|
||
static final Map<Intent, List<Pattern>> _intentPatterns = {
|
||
Intent.createEvent: [
|
||
RegExp(r'提醒|开会|预约|日程|安排'),
|
||
RegExp(r'明天|今天|后天|下周'),
|
||
RegExp(r'\d{1,2}点|\d{1,2}:\d{2}'),
|
||
],
|
||
Intent.searchEvent: [
|
||
RegExp(r'查看|有什么|今天.*日程|明天.*安排'),
|
||
],
|
||
};
|
||
|
||
Intent? matchIntent(String text) {
|
||
for (final entry in _intentPatterns.entries) {
|
||
for (final pattern in entry.value) {
|
||
if (pattern.hasMatch(text)) {
|
||
return entry.key;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 支持强制触发:#tool:create_calendar_event {"title": "test"}
|
||
bool tryForceTrigger(String text) {...}
|
||
}
|
||
```
|
||
|
||
### 4.3 事件解析与处理
|
||
|
||
```dart
|
||
Future<void> _processEvent(AgUiEvent event) async {
|
||
switch (event.type) {
|
||
case 'TEXT_MESSAGE_START':
|
||
_handleTextMessageStart(event);
|
||
break;
|
||
case 'TEXT_MESSAGE_CONTENT':
|
||
_handleTextMessageContent(event);
|
||
break;
|
||
case 'TEXT_MESSAGE_END':
|
||
_handleTextMessageEnd(event);
|
||
break;
|
||
case 'TOOL_CALL_START':
|
||
_handleToolCallStart(event);
|
||
break;
|
||
case 'TOOL_CALL_ARGS':
|
||
_handleToolCallArgs(event);
|
||
break;
|
||
case 'TOOL_CALL_END':
|
||
await _handleToolCallEnd(event);
|
||
break;
|
||
case 'TOOL_CALL_RESULT':
|
||
_handleToolCallResult(event);
|
||
break;
|
||
case 'TOOL_CALL_ERROR':
|
||
_handleToolCallError(event);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void _handleToolCallStart(ToolCallStartEvent event) {
|
||
// 创建 pending 状态的 tool call item
|
||
final item = ToolCallItem(
|
||
id: event.toolCallId,
|
||
callId: event.toolCallId,
|
||
toolName: event.toolCallName,
|
||
args: {},
|
||
status: ToolCallStatus.pending,
|
||
);
|
||
_chatItems.add(item);
|
||
}
|
||
|
||
Future<void> _handleToolCallEnd(ToolCallEndEvent event) async {
|
||
// 1. 找到对应的 pending tool call
|
||
final toolCall = _findPendingToolCall(event.toolCallId);
|
||
if (toolCall == null) return;
|
||
|
||
// 2. 校验参数 JSON Schema
|
||
final validation = validateToolArgs(toolCall.toolName, toolCall.args);
|
||
if (!validation.ok) {
|
||
_emitToolCallError(event.toolCallId, validation.error);
|
||
return;
|
||
}
|
||
|
||
// 3. 执行工具 handler
|
||
toolCall.status = ToolCallStatus.executing;
|
||
final result = await ToolRegistry.execute(
|
||
toolCall.toolName,
|
||
toolCall.args,
|
||
);
|
||
|
||
// 4. 构建 tool result(包含 result + ui)
|
||
final toolResult = ToolResult(
|
||
eventId: result['eventId'],
|
||
ok: result['ok'] ?? true,
|
||
message: result['message'],
|
||
);
|
||
|
||
final uiCard = _buildUiCard(toolCall.toolName, result);
|
||
|
||
// 5. 发送 TOOL_CALL_RESULT 事件
|
||
_emitToolCallResult(event.toolCallId, toolResult, uiCard);
|
||
}
|
||
```
|
||
|
||
### 4.4 UI Schema 渲染
|
||
|
||
```dart
|
||
class UiSchemaRenderer {
|
||
static final Map<String, Widget Function(UiCard)> _renderers = {
|
||
'calendar_card.v1': (card) => CalendarCardWidget(
|
||
data: CalendarCardData.fromJson(card.data),
|
||
actions: card.actions,
|
||
),
|
||
};
|
||
|
||
static Widget render(UiCard card) {
|
||
final renderer = _renderers[card.cardType];
|
||
if (renderer != null) {
|
||
return renderer(card);
|
||
}
|
||
// Unknown card type fallback
|
||
return _renderUnknownCard(card);
|
||
}
|
||
|
||
static Widget _renderUnknownCard(UiCard card) {
|
||
return GenericCardWidget(
|
||
rawJson: jsonEncode(card.toJson()),
|
||
cardType: card.cardType,
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.5 日历卡片组件
|
||
|
||
```dart
|
||
class CalendarCardWidget extends StatelessWidget {
|
||
final CalendarCardData data;
|
||
final List<CardAction> actions;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final color = ColorExt.parse(data.color ?? '#4F46E5');
|
||
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
boxShadow: [...],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 颜色条
|
||
Container(
|
||
height: 4,
|
||
color: color,
|
||
),
|
||
// 内容
|
||
Padding(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(data.title, style: ...),
|
||
if (data.description != null) ...,
|
||
_buildTimeRow(),
|
||
if (data.location != null) ...,
|
||
],
|
||
),
|
||
),
|
||
// Actions
|
||
if (actions.isNotEmpty) _buildActions(actions),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 持久化设计
|
||
|
||
### 5.1 存储结构
|
||
|
||
```dart
|
||
// localStorage / IndexedDB
|
||
{
|
||
"chat_sessions": {
|
||
"current_thread_id": {
|
||
"messages": [...], // ChatListItem JSON
|
||
"lastRunId": "run_xxx",
|
||
"updatedAt": "2026-02-28T12:00:00Z"
|
||
}
|
||
},
|
||
"calendar_events": {
|
||
"evt_xxx": {...} // 独立存储的日历事件
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 恢复逻辑
|
||
|
||
```dart
|
||
Future<void> restoreSession() async {
|
||
final session = await ChatHistoryRepository.load('current_thread_id');
|
||
if (session != null) {
|
||
_chatItems.clear();
|
||
_chatItems.addAll(session.messages);
|
||
_runId = session.lastRunId;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 错误处理
|
||
|
||
### 6.1 Tool Call 错误
|
||
|
||
```dart
|
||
void _emitToolCallError(String callId, String error) {
|
||
// 1. 更新 item 状态
|
||
final item = _findToolCallItem(callId);
|
||
item?.status = ToolCallStatus.error;
|
||
item?.errorMessage = error;
|
||
|
||
// 2. 渲染错误卡片
|
||
final errorCard = UiCard(
|
||
cardType: 'error_card.v1',
|
||
data: {'message': error},
|
||
);
|
||
|
||
// 3. 触发 UI 更新
|
||
notifyListeners();
|
||
}
|
||
```
|
||
|
||
### 6.2 事件流重连
|
||
|
||
```dart
|
||
// 断线重连时从 snapshot 恢复
|
||
Future<void> reconnect() async {
|
||
final snapshot = await _fetchMessagesSnapshot();
|
||
_chatItems.clear();
|
||
_chatItems.addAll(snapshot.messages);
|
||
|
||
// 重新订阅事件流
|
||
_subscribeToEvents();
|
||
}
|
||
```
|
||
|
||
## 7. 实施计划
|
||
|
||
### Phase 1: 基础框架
|
||
- [ ] 定义 AG-UI 事件模型
|
||
- [ ] 实现 AgUiService 基础结构
|
||
- [ ] 实现 ToolRegistry
|
||
|
||
### Phase 2: Mock 实现
|
||
- [ ] 实现 AiDecisionEngine 规则引擎
|
||
- [ ] 实现 Mock 事件流
|
||
- [ ] 集成现有 HomeScreen
|
||
|
||
### Phase 3: UI 渲染
|
||
- [ ] 实现 UiSchemaParser
|
||
- [ ] 实现 CalendarCardWidget
|
||
- [ ] 实现 ToolPending / ToolError 状态卡片
|
||
|
||
### Phase 4: 持久化
|
||
- [ ] 实现 ChatHistoryRepository
|
||
- [ ] 实现会话恢复
|
||
|
||
### Phase 5: 真实后端对接
|
||
- [ ] 实现 SSE 客户端
|
||
- [ ] 实现事件流解析器
|
||
|
||
## 8. 版本历史
|
||
|
||
| 版本 | 日期 | 变更 |
|
||
|------|------|------|
|
||
| v1.0 | 2026-02-28 | 初始版本 |
|