Add LLM digest setup and share timestamp context
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/core/llm/llm_diagnostics_log.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
});
|
||||
|
||||
test('records and exports LLM diagnostics', () async {
|
||||
const LlmDiagnosticsLog log = LlmDiagnosticsLog();
|
||||
|
||||
await log.record(
|
||||
'provider_error',
|
||||
'Google AI request failed with 429.',
|
||||
details: '{"statusCode":429,"responseBody":"quota"}',
|
||||
);
|
||||
|
||||
final List<LlmDiagnosticsEvent> events = await log.read();
|
||||
|
||||
expect(events, hasLength(1));
|
||||
expect(events.single.stage, 'provider_error');
|
||||
expect(events.single.details, contains('statusCode'));
|
||||
expect(log.exportText(events), contains('Google AI request failed'));
|
||||
});
|
||||
|
||||
test('trims very large diagnostics before storing', () async {
|
||||
const LlmDiagnosticsLog log = LlmDiagnosticsLog();
|
||||
|
||||
await log.record(
|
||||
'provider_error',
|
||||
'large',
|
||||
details: List<String>.filled(9000, 'x').join(),
|
||||
);
|
||||
|
||||
final List<LlmDiagnosticsEvent> events = await log.read();
|
||||
|
||||
expect(events.single.details, endsWith('[truncated]'));
|
||||
expect(events.single.details!.length, lessThan(9000));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:relationship_saver/core/llm/ollama/ollama_tool_chat_client.dart';
|
||||
import 'package:relationship_saver/core/llm/ollama/ollama_web_search_client.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
|
||||
import '../../helpers/queue_http_client_adapter.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'runs Ollama tool calls and feeds search results back to model',
|
||||
() async {
|
||||
final List<Map<String, dynamic>> requestBodies = <Map<String, dynamic>>[];
|
||||
final QueueHttpClientAdapter adapter = QueueHttpClientAdapter(
|
||||
<AdapterHandler>[
|
||||
(
|
||||
RequestOptions options,
|
||||
Stream<Uint8List>? requestStream,
|
||||
Future<void>? cancelFuture,
|
||||
) async {
|
||||
requestBodies.add(
|
||||
jsonDecode(options.data as String) as Map<String, dynamic>,
|
||||
);
|
||||
return jsonResponse(200, <String, dynamic>{
|
||||
'message': <String, dynamic>{
|
||||
'role': 'assistant',
|
||||
'content': '',
|
||||
'tool_calls': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'function': <String, dynamic>{
|
||||
'name': 'search_web',
|
||||
'arguments': <String, dynamic>{
|
||||
'query': 'Zurich jazz events May 2026',
|
||||
'maxResults': 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
(
|
||||
RequestOptions options,
|
||||
Stream<Uint8List>? requestStream,
|
||||
Future<void>? cancelFuture,
|
||||
) async {
|
||||
requestBodies.add(
|
||||
jsonDecode(options.data as String) as Map<String, dynamic>,
|
||||
);
|
||||
return jsonResponse(200, <String, dynamic>{
|
||||
'message': <String, dynamic>{
|
||||
'role': 'assistant',
|
||||
'content': '''
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"personToken": "person_001",
|
||||
"kind": "eventIdea",
|
||||
"title": "Jazz night",
|
||||
"details": "A grounded event suggestion.",
|
||||
"confidence": 0.8
|
||||
}
|
||||
]
|
||||
}
|
||||
''',
|
||||
},
|
||||
});
|
||||
},
|
||||
],
|
||||
);
|
||||
final Dio dio = Dio();
|
||||
dio.httpClientAdapter = adapter;
|
||||
final OllamaToolChatClient client = OllamaToolChatClient(
|
||||
dio: dio,
|
||||
webSearchClient: const FakeWebSearchClient(),
|
||||
);
|
||||
|
||||
final String response = await client.complete(
|
||||
baseUrl: 'http://macbook.local:11434',
|
||||
model: 'qwen3:8b',
|
||||
systemPrompt: 'Return digest JSON.',
|
||||
userPrompt: '{"people":[]}',
|
||||
);
|
||||
|
||||
expect(response, contains(AiSuggestionKind.eventIdea.name));
|
||||
expect(adapter.fetchCount, 2);
|
||||
expect(requestBodies.first['tools'], isA<List<dynamic>>());
|
||||
final List<dynamic> secondMessages =
|
||||
requestBodies.last['messages'] as List<dynamic>;
|
||||
final Map<String, dynamic> toolMessage = secondMessages
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.singleWhere(
|
||||
(Map<String, dynamic> message) => message['role'] == 'tool',
|
||||
);
|
||||
expect(toolMessage['tool_name'], 'search_web');
|
||||
expect(toolMessage['content'], contains('moods.ch'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class FakeWebSearchClient implements OllamaWebSearchClient {
|
||||
const FakeWebSearchClient();
|
||||
|
||||
@override
|
||||
Future<String> fetchPageText(String url) async {
|
||||
return 'Fetched text for $url';
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OllamaWebSearchResult>> search(
|
||||
String query, {
|
||||
int maxResults = 5,
|
||||
}) async {
|
||||
return const <OllamaWebSearchResult>[
|
||||
OllamaWebSearchResult(
|
||||
title: 'Moods Jazz Club',
|
||||
url: 'https://www.moods.ch/',
|
||||
snippet: 'Upcoming jazz concerts in Zurich.',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user