Skip to content

Commit 2b3319f

Browse files
feat(ai): implement semantic search, RAG pipeline, and citation extraction
1 parent 5f40856 commit 2b3319f

10 files changed

Lines changed: 955 additions & 16 deletions

lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import 'screens/settings_screen.dart';
88
import 'screens/splash_screen.dart';
99
import 'services/objectbox_store.dart';
1010
import 'services/biomarker_dictionary.dart';
11+
import 'services/embedding_service.dart';
1112
import 'services/gemma_service.dart';
13+
import 'services/vector_store_service.dart';
1214

1315
/// Global references — initialized in SplashScreen before navigation.
1416
late ObjectBoxStore objectbox;
1517
late BiomarkerDictionary biomarkerDictionary;
1618
late GemmaService gemmaService;
19+
late EmbeddingService embeddingService;
20+
late VectorStoreService vectorStoreService;
1721

1822
Future<void> main() async {
1923
WidgetsFlutterBinding.ensureInitialized();

lib/screens/chat_screen.dart

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import 'dart:async';
22

33
import 'package:flutter/material.dart';
44

5+
import 'package:flutter_gemma/flutter_gemma.dart';
6+
57
import '../main.dart';
68
import '../models/models.dart';
79
import '../services/chat_context_builder.dart';
10+
import '../services/citation_extractor.dart';
811
import '../widgets/chat_message_bubble.dart';
912

1013
/// The AI Chat screen — transforms from download/load prompt into a
@@ -28,7 +31,7 @@ class _ChatScreenState extends State<ChatScreen> {
2831
final _messages = <ChatMessage>[];
2932
final _textController = TextEditingController();
3033
final _scrollController = ScrollController();
31-
final _contextBuilder = ChatContextBuilder();
34+
late final ChatContextBuilder _contextBuilder;
3235

3336
StreamSubscription<ModelInfo>? _statusSubscription;
3437
StreamSubscription<String>? _generationSubscription;
@@ -37,6 +40,7 @@ class _ChatScreenState extends State<ChatScreen> {
3740
@override
3841
void initState() {
3942
super.initState();
43+
_contextBuilder = ChatContextBuilder(vectorStore: vectorStoreService);
4044
_statusSubscription = gemmaService.modelStatusStream.listen((info) {
4145
if (mounted) {
4246
setState(() => _modelInfo = info);
@@ -55,7 +59,7 @@ class _ChatScreenState extends State<ChatScreen> {
5559

5660
// ─── Chat Logic ────────────────────────────────────────────────────
5761

58-
void _sendMessage([String? overrideText]) {
62+
Future<void> _sendMessage([String? overrideText]) async {
5963
final text = (overrideText ?? _textController.text).trim();
6064
if (text.isEmpty || !mounted) return;
6165
if (gemmaService.isGenerating) return;
@@ -69,8 +73,13 @@ class _ChatScreenState extends State<ChatScreen> {
6973
});
7074
_scrollToBottom();
7175

72-
// Build context from lab data
73-
final context = _contextBuilder.buildQueryContext(text);
76+
// Build context from lab data (async — may use semantic search)
77+
final context = await _contextBuilder.buildQueryContext(text);
78+
final retrievedDocs = List<RetrievalResult>.from(
79+
_contextBuilder.lastRetrievedDocs,
80+
);
81+
82+
if (!mounted) return;
7483

7584
// Add placeholder assistant message (streaming)
7685
final assistantMsg = ChatMessage.assistantStreaming();
@@ -101,13 +110,20 @@ class _ChatScreenState extends State<ChatScreen> {
101110
},
102111
onDone: () {
103112
if (mounted) {
113+
var finalContent = tokenBuffer.toString();
114+
// Append citation footer if we have retrieved docs
115+
if (finalContent.isNotEmpty && retrievedDocs.isNotEmpty) {
116+
finalContent = CitationExtractor.appendSourceFooter(
117+
finalContent,
118+
retrievedDocs,
119+
);
120+
}
104121
setState(() {
105122
_messages[assistantIndex] = _messages[assistantIndex].copyWith(
106123
isStreaming: false,
107-
// If we got no tokens, show a fallback message
108-
content: tokenBuffer.isEmpty
124+
content: finalContent.isEmpty
109125
? 'I wasn\'t able to generate a response. Please try again.'
110-
: null,
126+
: finalContent,
111127
);
112128
});
113129
_scrollToBottom();
@@ -261,6 +277,7 @@ class _ChatScreenState extends State<ChatScreen> {
261277
textController: _textController,
262278
scrollController: _scrollController,
263279
isGenerating: gemmaService.isGenerating,
280+
isSemanticSearchActive: _contextBuilder.isSemanticSearchActive,
264281
onSend: _sendMessage,
265282
onStop: _stopGeneration,
266283
onSuggestionTap: (text) {
@@ -552,6 +569,7 @@ class _ChatView extends StatelessWidget {
552569
final TextEditingController textController;
553570
final ScrollController scrollController;
554571
final bool isGenerating;
572+
final bool isSemanticSearchActive;
555573
final VoidCallback onSend;
556574
final VoidCallback onStop;
557575
final ValueChanged<String>? onSuggestionTap;
@@ -561,6 +579,7 @@ class _ChatView extends StatelessWidget {
561579
required this.textController,
562580
required this.scrollController,
563581
required this.isGenerating,
582+
this.isSemanticSearchActive = false,
564583
required this.onSend,
565584
required this.onStop,
566585
this.onSuggestionTap,
@@ -572,6 +591,39 @@ class _ChatView extends StatelessWidget {
572591

573592
return Column(
574593
children: [
594+
// Search mode indicator
595+
Container(
596+
width: double.infinity,
597+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
598+
color: theme.colorScheme.surfaceContainerHighest.withValues(
599+
alpha: 0.5,
600+
),
601+
child: Row(
602+
mainAxisAlignment: MainAxisAlignment.center,
603+
mainAxisSize: MainAxisSize.min,
604+
children: [
605+
Icon(
606+
isSemanticSearchActive ? Icons.auto_awesome : Icons.text_fields,
607+
size: 14,
608+
color: isSemanticSearchActive
609+
? theme.colorScheme.primary
610+
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
611+
),
612+
const SizedBox(width: 4),
613+
Text(
614+
isSemanticSearchActive
615+
? 'Semantic search active'
616+
: 'Keyword search (basic)',
617+
style: theme.textTheme.labelSmall?.copyWith(
618+
color: isSemanticSearchActive
619+
? theme.colorScheme.primary
620+
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
621+
),
622+
),
623+
],
624+
),
625+
),
626+
575627
// Message list
576628
Expanded(
577629
child: messages.isEmpty

lib/screens/reports_screen.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ class _ReportsScreenState extends State<ReportsScreen> {
141141
if (!mounted) return;
142142

143143
if (importResult.success) {
144+
// Index new results in VectorStore for semantic search
145+
if (importResult.report != null && vectorStoreService.isReady) {
146+
final newResults = objectbox.getResultsForReport(
147+
importResult.report!.id,
148+
);
149+
vectorStoreService.indexResults(newResults);
150+
}
151+
144152
final successMessage = importResult.warnings.isEmpty
145153
? 'Imported ${importResult.successfulMatches} biomarkers'
146154
: 'Imported ${importResult.successfulMatches} biomarkers with warnings';

0 commit comments

Comments
 (0)