From 1fd33c57a77b43fcfcf0cadc2b579e17743e1555 Mon Sep 17 00:00:00 2001 From: qzl Date: Sat, 28 Feb 2026 13:38:26 +0800 Subject: [PATCH] feat(chat): add AgUiService with mock event stream --- .../chat/data/services/ag_ui_service.dart | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 apps/lib/features/chat/data/services/ag_ui_service.dart diff --git a/apps/lib/features/chat/data/services/ag_ui_service.dart b/apps/lib/features/chat/data/services/ag_ui_service.dart new file mode 100644 index 0000000..a132d33 --- /dev/null +++ b/apps/lib/features/chat/data/services/ag_ui_service.dart @@ -0,0 +1,167 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:social_app/core/config/env.dart'; + +import '../ai/ai_decision_engine.dart'; +import '../models/ag_ui_event.dart'; +import '../models/tool_result.dart'; +import '../tools/tool_registry.dart'; + +typedef EventCallback = void Function(AgUiEvent event); + +class AgUiService { + EventCallback onEvent; + final AiDecisionEngine _decisionEngine; + + AgUiService({EventCallback? onEvent}) + : onEvent = onEvent ?? ((_) {}), + _decisionEngine = AiDecisionEngine(); + + Future sendMessage(String content) async { + if (Env.isMockApi) { + await _mockEventStream(content); + } else { + throw UnimplementedError('Real API not implemented'); + } + } + + Future _mockEventStream(String content) async { + final threadId = 'thread_${DateTime.now().millisecondsSinceEpoch}'; + final runId = 'run_${DateTime.now().millisecondsSinceEpoch}'; + + onEvent(RunStartedEvent(threadId: threadId, runId: runId)); + + final forceTrigger = _decisionEngine.tryForceTrigger(content); + if (forceTrigger != null) { + await _mockToolCallFlowWithArgs(forceTrigger.toolName, forceTrigger.args); + } else if (_decisionEngine.shouldTriggerToolCall(content)) { + await _mockToolCallFlow(content); + } + + final replies = _generateReplies(content); + if (replies.isNotEmpty) { + await _mockTextMessageStream(replies); + } + + onEvent(RunFinishedEvent(threadId: threadId, runId: runId)); + } + + Future _mockToolCallFlow(String content) async { + final args = _decisionEngine.getToolCallArgs(content); + if (args == null) return; + + await _mockToolCallFlowWithArgs('create_calendar_event', args); + } + + Future _mockToolCallFlowWithArgs( + String toolName, + Map args, + ) async { + final toolCallId = 'tc_${DateTime.now().millisecondsSinceEpoch}'; + + onEvent(ToolCallStartEvent(toolCallId: toolCallId, toolCallName: toolName)); + + onEvent(ToolCallArgsEvent(toolCallId: toolCallId, delta: jsonEncode(args))); + + onEvent(ToolCallEndEvent(toolCallId: toolCallId)); + + final validation = ToolRegistry.validateArgs(toolName, args); + if (!validation.ok) { + onEvent( + ToolCallErrorEvent( + toolCallId: toolCallId, + error: validation.error ?? 'Validation failed', + code: 'VALIDATION_ERROR', + ), + ); + return; + } + + try { + ToolRegistry.initialize(); + final result = await ToolRegistry.execute(toolName, args); + final ui = _buildUiCard(toolName, result); + final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}'; + + onEvent( + ToolCallResultEvent( + messageId: messageId, + toolCallId: toolCallId, + result: result, + ui: ui, + ), + ); + } catch (e) { + onEvent( + ToolCallErrorEvent( + toolCallId: toolCallId, + error: e.toString(), + code: 'EXECUTION_ERROR', + ), + ); + } + } + + UiCard? _buildUiCard(String toolName, Map result) { + if (toolName == 'create_calendar_event') { + return UiCard( + cardType: 'calendar', + data: CalendarCardData( + id: result['eventId'] ?? '', + title: result['title'] ?? '', + description: result['description'], + startAt: result['startAt'] ?? '', + endAt: result['endAt'], + timezone: result['timezone'], + location: result['location'], + color: result['color'], + sourceType: result['sourceType'], + ).toJson(), + actions: [ + CardAction( + type: 'link', + label: '查看详情', + target: '/calendar/${result['eventId']}', + ), + ], + ); + } + return null; + } + + List _generateReplies(String content) { + final intent = _decisionEngine.matchIntent(content); + + switch (intent) { + case Intent.createEvent: + return ['好的,我已经为您创建了日程安排。']; + case Intent.searchEvent: + return ['您今天有以下日程:\n- 10:00 团队会议\n- 14:00 产品评审']; + case Intent.unknown: + return ['我理解了您的问题,让我来帮您处理。']; + } + } + + Future _mockTextMessageStream(List replies) async { + for (final reply in replies) { + final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}'; + + onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant')); + + const chunkSize = 10; + for (var i = 0; i < reply.length; i += chunkSize) { + final end = (i + chunkSize < reply.length) + ? i + chunkSize + : reply.length; + final chunk = reply.substring(i, end); + + onEvent(TextMessageContentEvent(messageId: messageId, delta: chunk)); + + await Future.delayed(const Duration(milliseconds: 50)); + } + + onEvent(TextMessageEndEvent(messageId: messageId)); + } + } +}