2026-02-28 13:38:26 +08:00
|
|
|
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';
|
2026-03-02 11:17:20 +08:00
|
|
|
import 'mock_history_service.dart';
|
2026-02-28 13:38:26 +08:00
|
|
|
|
2026-02-28 14:41:21 +08:00
|
|
|
/// Mock ID 前缀常量
|
|
|
|
|
const _threadIdPrefix = 'thread_';
|
|
|
|
|
const _runIdPrefix = 'run_';
|
|
|
|
|
const _toolCallIdPrefix = 'tc_';
|
|
|
|
|
const _messageIdPrefix = 'msg_';
|
|
|
|
|
|
|
|
|
|
/// 流式输出延迟 (毫秒)
|
|
|
|
|
const _streamChunkDelayMs = 50;
|
|
|
|
|
|
|
|
|
|
/// 文本块大小
|
|
|
|
|
const _textChunkSize = 10;
|
|
|
|
|
|
2026-02-28 13:38:26 +08:00
|
|
|
typedef EventCallback = void Function(AgUiEvent event);
|
|
|
|
|
|
|
|
|
|
class AgUiService {
|
|
|
|
|
EventCallback onEvent;
|
|
|
|
|
final AiDecisionEngine _decisionEngine;
|
2026-03-02 11:17:20 +08:00
|
|
|
final MockHistoryService _historyService;
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
AgUiService({EventCallback? onEvent})
|
|
|
|
|
: onEvent = onEvent ?? ((_) {}),
|
2026-03-02 11:17:20 +08:00
|
|
|
_decisionEngine = AiDecisionEngine(),
|
|
|
|
|
_historyService = MockHistoryService();
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
Future<void> sendMessage(String content) async {
|
|
|
|
|
if (Env.isMockApi) {
|
|
|
|
|
await _mockEventStream(content);
|
|
|
|
|
} else {
|
|
|
|
|
throw UnimplementedError('Real API not implemented');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 11:17:20 +08:00
|
|
|
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));
|
2026-03-02 15:05:10 +08:00
|
|
|
await Future.delayed(const Duration(milliseconds: 10));
|
2026-03-02 11:17:20 +08:00
|
|
|
|
2026-03-02 15:05:10 +08:00
|
|
|
// Determine target date, end early if no earlier history
|
|
|
|
|
final DateTime targetDate;
|
2026-03-02 11:17:20 +08:00
|
|
|
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));
|
2026-03-02 15:05:10 +08:00
|
|
|
await Future.delayed(const Duration(milliseconds: 10));
|
2026-03-02 11:17:20 +08:00
|
|
|
onEvent(RunFinishedEvent(threadId: threadId, runId: runId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 13:38:26 +08:00
|
|
|
Future<void> _mockEventStream(String content) async {
|
2026-02-28 14:41:21 +08:00
|
|
|
final threadId = '$_threadIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
|
final runId = '$_runIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
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 {
|
2026-02-28 14:41:21 +08:00
|
|
|
final toolCallId =
|
|
|
|
|
'$_toolCallIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
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);
|
2026-02-28 14:41:21 +08:00
|
|
|
final messageId =
|
|
|
|
|
'$_messageIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
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) {
|
2026-02-28 14:41:21 +08:00
|
|
|
final messageId =
|
|
|
|
|
'$_messageIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
2026-02-28 13:38:26 +08:00
|
|
|
|
|
|
|
|
onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant'));
|
|
|
|
|
|
2026-02-28 14:41:21 +08:00
|
|
|
for (var i = 0; i < reply.length; i += _textChunkSize) {
|
|
|
|
|
final end = (i + _textChunkSize < reply.length)
|
|
|
|
|
? i + _textChunkSize
|
2026-02-28 13:38:26 +08:00
|
|
|
: reply.length;
|
|
|
|
|
final chunk = reply.substring(i, end);
|
|
|
|
|
|
|
|
|
|
onEvent(TextMessageContentEvent(messageId: messageId, delta: chunk));
|
|
|
|
|
|
2026-02-28 14:41:21 +08:00
|
|
|
await Future.delayed(const Duration(milliseconds: _streamChunkDelayMs));
|
2026-02-28 13:38:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onEvent(TextMessageEndEvent(messageId: messageId));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|