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
13 KiB
13 KiB
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 事件模型
// 基类
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)
{
"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)
// 工具定义
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 模型
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 发送消息
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 事件流(规则引擎)
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 事件解析与处理
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 渲染
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 日历卡片组件
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 存储结构
// 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 恢复逻辑
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 错误
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 事件流重连
// 断线重连时从 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 | 初始版本 |