Show agent tool steps in chat responses
This commit is contained in:
@@ -12,10 +12,80 @@ import '../../features/chat/presentation/cubit/chat_state.dart';
|
||||
const _uuid = Uuid();
|
||||
|
||||
class AgentRunResult {
|
||||
const AgentRunResult({required this.answer, this.proposal});
|
||||
const AgentRunResult({
|
||||
required this.answer,
|
||||
this.proposal,
|
||||
this.steps = const [],
|
||||
});
|
||||
|
||||
final String answer;
|
||||
final FileChangeProposal? proposal;
|
||||
final List<AgentToolStep> steps;
|
||||
|
||||
factory AgentRunResult.fromApiData(Map<String, dynamic> data) {
|
||||
final steps = (data['steps'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(AgentToolStep.fromJson)
|
||||
.toList(growable: false);
|
||||
final tool = data['tool'] as String?;
|
||||
final skill = data['skill'] as String?;
|
||||
final files = (data['files'] as List<dynamic>? ?? const []).cast<String>();
|
||||
final details = <String>[
|
||||
if (steps.isNotEmpty)
|
||||
'خطوات الوكيل:\n${steps.indexed.map((entry) => '${entry.$1 + 1}. ${entry.$2.label} — ${entry.$2.statusLabel}.').join('\n')}'
|
||||
else if (tool != null)
|
||||
'الأداة المستخدمة: ${AgentToolStep.labelFor(tool)}.',
|
||||
if (skill != null)
|
||||
'المهارة المستخدمة: ${switch (skill) {
|
||||
'code_explain' => 'شرح الكود',
|
||||
'code_review' => 'مراجعة الكود',
|
||||
'test_plan' => 'خطة اختبارات',
|
||||
_ => skill,
|
||||
}}.',
|
||||
if (files.isNotEmpty) 'الملفات المقروءة: ${files.join('، ')}.',
|
||||
];
|
||||
final proposalData = data['proposal'];
|
||||
final proposal =
|
||||
proposalData is Map<String, dynamic>
|
||||
? FileChangeProposal.fromJson(proposalData)
|
||||
: null;
|
||||
final result = data['result']?.toString() ?? '';
|
||||
return AgentRunResult(
|
||||
answer: details.isEmpty ? result : '$result\n\n${details.join('\n')}',
|
||||
proposal: proposal,
|
||||
steps: steps,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AgentToolStep {
|
||||
const AgentToolStep({required this.tool, required this.status});
|
||||
|
||||
final String tool;
|
||||
final String status;
|
||||
|
||||
factory AgentToolStep.fromJson(Map<String, dynamic> json) => AgentToolStep(
|
||||
tool: json['tool']?.toString() ?? 'unknown',
|
||||
status: json['status']?.toString() ?? 'unknown',
|
||||
);
|
||||
|
||||
String get label => labelFor(tool);
|
||||
String get statusLabel => switch (status) {
|
||||
'completed' => 'اكتملت',
|
||||
'failed' => 'فشلت',
|
||||
'running' => 'قيد التنفيذ',
|
||||
'pending' => 'بانتظار التنفيذ',
|
||||
_ => status,
|
||||
};
|
||||
|
||||
static String labelFor(String tool) => switch (tool) {
|
||||
'calculator' => 'الآلة الحاسبة المحلية',
|
||||
'search_workspace' => 'البحث في مساحة العمل',
|
||||
'search_knowledge' => 'البحث في فهرس المعرفة',
|
||||
'propose_file_change' => 'اقتراح تعديل ملف',
|
||||
'local-llm' => 'النموذج المحلي',
|
||||
_ => tool,
|
||||
};
|
||||
}
|
||||
|
||||
class ApiRepository {
|
||||
@@ -488,37 +558,7 @@ class ApiRepository {
|
||||
if (streamError != null) throw Exception(streamError);
|
||||
final data = resultData;
|
||||
if (data == null) throw Exception('انقطع اتصال البث قبل اكتمال الوكيل.');
|
||||
final result = data['result']?.toString() ?? '';
|
||||
final files =
|
||||
(data['files'] as List<dynamic>? ?? const []).cast<String>();
|
||||
final tool = data['tool'] as String?;
|
||||
final skill = data['skill'] as String?;
|
||||
final toolLabels = {
|
||||
'calculator': 'الآلة الحاسبة المحلية',
|
||||
'search_workspace': 'البحث في مساحة العمل (قراءة فقط)',
|
||||
'local-llm': 'النموذج المحلي دون أداة',
|
||||
};
|
||||
final usedTool = tool == null ? null : toolLabels[tool] ?? tool;
|
||||
final details = <String>[
|
||||
if (usedTool != null) 'الأداة المستخدمة: $usedTool.',
|
||||
if (skill != null)
|
||||
'المهارة المستخدمة: ${switch (skill) {
|
||||
'code_explain' => 'شرح الكود',
|
||||
'code_review' => 'مراجعة الكود',
|
||||
'test_plan' => 'خطة اختبارات',
|
||||
_ => skill,
|
||||
}}.',
|
||||
if (files.isNotEmpty) 'الملفات المقروءة: ${files.join('، ')}.',
|
||||
];
|
||||
final proposalData = data['proposal'];
|
||||
final proposal =
|
||||
proposalData is Map<String, dynamic>
|
||||
? FileChangeProposal.fromJson(proposalData)
|
||||
: null;
|
||||
return AgentRunResult(
|
||||
answer: details.isEmpty ? result : '$result\n\n${details.join('\n')}',
|
||||
proposal: proposal,
|
||||
);
|
||||
return AgentRunResult.fromApiData(data);
|
||||
} finally {
|
||||
if (identical(_activeRequestClient, client)) _activeRequestClient = null;
|
||||
client.close();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_app/core/network/api_repository.dart';
|
||||
|
||||
void main() {
|
||||
group('AgentRunResult.fromApiData', () {
|
||||
test('preserves and labels every completed tool step', () {
|
||||
final result = AgentRunResult.fromApiData({
|
||||
'result': 'الإجابة النهائية.',
|
||||
'tool': 'search_knowledge',
|
||||
'steps': [
|
||||
{'tool': 'search_workspace', 'status': 'completed'},
|
||||
{'tool': 'search_knowledge', 'status': 'completed'},
|
||||
],
|
||||
'files': ['lib/main.dart'],
|
||||
});
|
||||
|
||||
expect(result.steps, hasLength(2));
|
||||
expect(result.steps.first.label, 'البحث في مساحة العمل');
|
||||
expect(result.steps.last.label, 'البحث في فهرس المعرفة');
|
||||
expect(result.steps.last.statusLabel, 'اكتملت');
|
||||
expect(result.answer, contains('1. البحث في مساحة العمل — اكتملت.'));
|
||||
expect(result.answer, contains('2. البحث في فهرس المعرفة — اكتملت.'));
|
||||
expect(result.answer, contains('الملفات المقروءة: lib/main.dart.'));
|
||||
});
|
||||
|
||||
test('uses the legacy tool field when the API returns no steps', () {
|
||||
final result = AgentRunResult.fromApiData({
|
||||
'result': '391',
|
||||
'tool': 'calculator',
|
||||
});
|
||||
|
||||
expect(result.steps, isEmpty);
|
||||
expect(result.answer, '391\n\nالأداة المستخدمة: الآلة الحاسبة المحلية.');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user