feat: AG-UI 协议对齐与路由导航功能
- 前端: 添加 SSE 流式支持、stateSnapshot 事件、路由导航工具 - 前端: 实现工具调用审批流程,支持 pending 状态展示 - 后端: Agent 状态管理与会话持久化相关重构 - 文档: 新增 agent-agui-full-alignance 设计文档 - 测试: 补充相关单元测试和集成测试
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:social_app/core/api/i_api_client.dart';
|
||||
import 'package:social_app/core/di/injection.dart';
|
||||
|
||||
import '../../data/models/ag_ui_event.dart';
|
||||
import '../../data/models/chat_list_item.dart';
|
||||
@@ -53,8 +55,9 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
final AgUiService _service;
|
||||
final Map<String, String> _toolCallArgsBuffer = {};
|
||||
|
||||
ChatBloc({AgUiService? service})
|
||||
: _service = service ?? AgUiService(),
|
||||
ChatBloc({AgUiService? service, IApiClient? apiClient})
|
||||
: _service =
|
||||
service ?? AgUiService(apiClient: apiClient ?? sl<IApiClient>()),
|
||||
super(const ChatState()) {
|
||||
_service.onEvent = _handleEvent;
|
||||
}
|
||||
@@ -84,6 +87,8 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
_handleToolCallResult(event as ToolCallResultEvent);
|
||||
case AgUiEventType.toolCallError:
|
||||
_handleToolCallError(event as ToolCallErrorEvent);
|
||||
case AgUiEventType.stateSnapshot:
|
||||
_handleStateSnapshot(event as StateSnapshotEvent);
|
||||
case AgUiEventType.messagesSnapshot:
|
||||
_handleMessagesSnapshot(event as MessagesSnapshotEvent);
|
||||
case AgUiEventType.unknown:
|
||||
@@ -157,9 +162,12 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
_toolCallArgsBuffer.remove(endEvent.toolCallId);
|
||||
final updatedItems = state.items.map((item) {
|
||||
if (item.id == endEvent.toolCallId && item is ToolCallItem) {
|
||||
final nextStatus = item.toolName == 'navigate_to_route'
|
||||
? ToolCallStatus.pending
|
||||
: ToolCallStatus.executing;
|
||||
return item.copyWith(
|
||||
args: parsedArgs,
|
||||
status: ToolCallStatus.executing,
|
||||
status: nextStatus,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
@@ -174,10 +182,15 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
final uiCard = resultEvent.ui;
|
||||
if (uiCard == null) {
|
||||
emit(state.copyWith(items: filteredItems));
|
||||
return;
|
||||
}
|
||||
final resultItem = ToolResultItem(
|
||||
id: resultEvent.messageId,
|
||||
callId: resultEvent.toolCallId,
|
||||
uiCard: resultEvent.ui ?? UiCard(cardType: 'empty', data: {}),
|
||||
uiCard: uiCard,
|
||||
timestamp: DateTime.now(),
|
||||
sender: MessageSender.ai,
|
||||
);
|
||||
@@ -224,6 +237,26 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleStateSnapshot(StateSnapshotEvent stateSnapshotEvent) {
|
||||
final snapshot = stateSnapshotEvent.snapshot;
|
||||
if (snapshot['scope'] != 'history_day') {
|
||||
return;
|
||||
}
|
||||
final rawMessages = snapshot['messages'];
|
||||
if (rawMessages is! List<dynamic>) {
|
||||
_handleMessagesSnapshot(MessagesSnapshotEvent(messages: const []));
|
||||
return;
|
||||
}
|
||||
final parsed = <SnapshotMessage>[];
|
||||
for (final raw in rawMessages) {
|
||||
if (raw is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
parsed.add(SnapshotMessage.fromJson(raw));
|
||||
}
|
||||
_handleMessagesSnapshot(MessagesSnapshotEvent(messages: parsed));
|
||||
}
|
||||
|
||||
List<ChatListItem> _convertSnapshotMessages(List<SnapshotMessage> messages) {
|
||||
return messages.map((msg) {
|
||||
final timestamp = msg.timestamp ?? DateTime.now();
|
||||
@@ -298,6 +331,44 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
await _service.loadHistory(beforeDate: state.oldestLoadedDate);
|
||||
}
|
||||
|
||||
Future<void> approveToolCall(String toolCallId) async {
|
||||
ToolCallItem? target;
|
||||
for (final item in state.items) {
|
||||
if (item is ToolCallItem && item.callId == toolCallId) {
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
final updatedItems = state.items.map((item) {
|
||||
if (item is ToolCallItem && item.callId == toolCallId) {
|
||||
return item.copyWith(status: ToolCallStatus.executing, errorMessage: null);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
emit(state.copyWith(items: updatedItems, isLoading: true, error: null));
|
||||
try {
|
||||
await _service.approveToolCall(
|
||||
toolCallId: target.callId,
|
||||
toolName: target.toolName,
|
||||
args: target.args,
|
||||
);
|
||||
} catch (error) {
|
||||
final failedItems = state.items.map((item) {
|
||||
if (item is ToolCallItem && item.callId == toolCallId) {
|
||||
return item.copyWith(
|
||||
status: ToolCallStatus.error,
|
||||
errorMessage: error.toString(),
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}).toList();
|
||||
emit(state.copyWith(items: failedItems, isLoading: false, error: error.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
emit(state.copyWith(error: null));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user