Add LLM digest setup and share timestamp context

This commit is contained in:
Rijad Zuzo
2026-05-19 19:17:35 +02:00
parent 9d2da0c600
commit 5d80af375e
42 changed files with 2878 additions and 39 deletions
@@ -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.',
),
];
}
}