Files
social-app/docs/plans/2026-02-28-ag-ui-chat-design.md
T
qzl c3192a2431 feat(chat): add ChatBubble widget and mock data for home screen
- 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
2026-02-28 14:47:33 +08:00

13 KiB
Raw Blame History

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 Schemav1

{
  "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 初始版本