Files
social-app/apps/lib/features/chat/data/services/ag_ui_service.dart
T

221 lines
6.6 KiB
Dart
Raw Normal View History

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';
import 'mock_history_service.dart';
/// Mock ID 前缀常量
const _threadIdPrefix = 'thread_';
const _runIdPrefix = 'run_';
const _toolCallIdPrefix = 'tc_';
const _messageIdPrefix = 'msg_';
/// 流式输出延迟 (毫秒)
const _streamChunkDelayMs = 50;
/// 文本块大小
const _textChunkSize = 10;
typedef EventCallback = void Function(AgUiEvent event);
class AgUiService {
EventCallback onEvent;
final AiDecisionEngine _decisionEngine;
final MockHistoryService _historyService;
AgUiService({EventCallback? onEvent})
: onEvent = onEvent ?? ((_) {}),
_decisionEngine = AiDecisionEngine(),
_historyService = MockHistoryService();
Future<void> sendMessage(String content) async {
if (Env.isMockApi) {
await _mockEventStream(content);
} else {
throw UnimplementedError('Real API not implemented');
}
}
Future<void> loadHistory({DateTime? beforeDate}) async {
if (Env.isMockApi) {
await _mockLoadHistory(beforeDate: beforeDate);
}
}
bool hasEarlierHistory(DateTime fromDate) {
return _historyService.hasEarlierHistory(fromDate);
}
Future<void> _mockLoadHistory({DateTime? beforeDate}) async {
final threadId = '$_threadIdPrefix${DateTime.now().millisecondsSinceEpoch}';
final runId = '$_runIdPrefix${DateTime.now().millisecondsSinceEpoch}';
onEvent(RunStartedEvent(threadId: threadId, runId: runId));
await Future.delayed(const Duration(milliseconds: 10));
// Determine target date, end early if no earlier history
final DateTime targetDate;
if (beforeDate != null) {
final prevDate = _historyService.getPreviousDay(beforeDate);
if (prevDate == null) {
onEvent(RunFinishedEvent(threadId: threadId, runId: runId));
return;
}
targetDate = prevDate;
} else {
targetDate = _historyService.getLatestHistoryDate() ?? DateTime.now();
}
final messages = _historyService.getHistoryForDay(targetDate);
onEvent(MessagesSnapshotEvent(messages: messages));
await Future.delayed(const Duration(milliseconds: 10));
onEvent(RunFinishedEvent(threadId: threadId, runId: runId));
}
Future<void> _mockEventStream(String content) async {
final threadId = '$_threadIdPrefix${DateTime.now().millisecondsSinceEpoch}';
final runId = '$_runIdPrefix${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<void> _mockToolCallFlow(String content) async {
final args = _decisionEngine.getToolCallArgs(content);
if (args == null) return;
await _mockToolCallFlowWithArgs('create_calendar_event', args);
}
Future<void> _mockToolCallFlowWithArgs(
String toolName,
Map<String, dynamic> args,
) async {
final toolCallId =
'$_toolCallIdPrefix${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 =
'$_messageIdPrefix${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<String, dynamic> 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<String> _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<void> _mockTextMessageStream(List<String> replies) async {
for (final reply in replies) {
final messageId =
'$_messageIdPrefix${DateTime.now().millisecondsSinceEpoch}';
onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant'));
for (var i = 0; i < reply.length; i += _textChunkSize) {
final end = (i + _textChunkSize < reply.length)
? i + _textChunkSize
: reply.length;
final chunk = reply.substring(i, end);
onEvent(TextMessageContentEvent(messageId: messageId, delta: chunk));
await Future.delayed(const Duration(milliseconds: _streamChunkDelayMs));
}
onEvent(TextMessageEndEvent(messageId: messageId));
}
}
}