import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import '../models/content_chunk.dart'; import '../utils/logger.dart'; import 'vector_service.dart'; /// Modes for AI tutoring enum TutorMode { explanation, // Detailed explanations tutor, // Socratic questioning exploration, // Discovery-based learning } /// Response from RAG system class RAGResponse { final String answer; final List sources; final double confidence; final TutorMode mode; final List relatedConcepts; final Map metadata; RAGResponse({ required this.answer, required this.sources, required this.confidence, required this.mode, required this.relatedConcepts, required this.metadata, }); } /// Source citation for RAG responses class SourceCitation { final String contentId; final String chunkId; final String title; final String concept; final String subject; final String excerpt; final double relevance; final int? pageNumber; SourceCitation({ required this.contentId, required this.chunkId, required this.title, required this.concept, required this.subject, required this.excerpt, required this.relevance, this.pageNumber, }); } /// Service for Retrieval-Augmented Generation pipeline class RAGService { static final FirebaseFirestore _firestore = FirebaseFirestore.instance; static const int maxContextTokens = 4000; static const int maxChunksInContext = 5; /// Process a user query through RAG pipeline static Future processQuery({ required String userQuery, required TutorMode mode, String? subject, String? concept, int? grade, double? minDifficulty, double? maxDifficulty, int maxSources = 5, }) async { try { Logger.info( 'Processing RAG query: "${userQuery.substring(0, 50)}..." in ${mode.name} mode', ); // 1. Generate embedding for user query final queryEmbedding = VectorService.generateEmbedding(userQuery); // 2. Retrieve relevant content chunks final relevantChunks = await VectorService.searchSimilar( queryEmbedding: queryEmbedding, subject: subject, concept: concept, grade: grade, minDifficulty: minDifficulty, maxDifficulty: maxDifficulty, k: maxSources + 2, // Get extra for filtering threshold: 0.3, ); if (relevantChunks.isEmpty) { Logger.warning('No relevant content found for query'); return _createNoContentResponse(userQuery, mode); } // 3. Build context window final context = _buildContextWindow(relevantChunks, userQuery, mode); // 4. Generate response (this will be handled by RAGAIService) final response = await _generateResponse( query: userQuery, context: context, mode: mode, sources: relevantChunks.take(maxSources).toList(), ); Logger.info( 'RAG response generated with confidence: ${response.confidence}', ); return response; } catch (e) { Logger.error('Error processing RAG query: $e'); return _createErrorResponse(userQuery, mode, e.toString()); } } /// Build context window from relevant chunks static String _buildContextWindow( List chunks, String userQuery, TutorMode mode, ) { try { final contextBuilder = StringBuffer(); // Add context header contextBuilder.writeln('=== CONTEÚDO EDUCACIONAL RELEVANTE ===\n'); // Sort chunks by relevance and take top chunks final sortedChunks = chunks.take(maxChunksInContext).toList(); for (int i = 0; i < sortedChunks.length; i++) { final chunk = sortedChunks[i]; contextBuilder.writeln('--- Fonte ${i + 1} ---'); contextBuilder.writeln('Disciplina: ${chunk.subject}'); contextBuilder.writeln('Conceito: ${chunk.concept}'); if (chunk.subConcept != null) { contextBuilder.writeln('Subconceito: ${chunk.subConcept}'); } contextBuilder.writeln('Unidade: ${chunk.unit}'); contextBuilder.writeln('Dificuldade: ${chunk.difficulty}'); if (chunk.pageNumber != null) { contextBuilder.writeln('Página: ${chunk.pageNumber}'); } contextBuilder.writeln('\nConteúdo:\n${chunk.text}\n'); } // Add mode-specific instructions contextBuilder.writeln('\n=== INSTRUÇÕES DE TUTORIA ==='); contextBuilder.writeln('Modo: ${_getModeInstructions(mode)}'); contextBuilder.writeln('Pergunta do Aluno: $userQuery\n'); final contextText = contextBuilder.toString(); // Check context length and truncate if necessary if (contextText.length > maxContextTokens) { Logger.warning('Context too long, truncating'); return contextText.substring(0, maxContextTokens - 100) + '...[truncated]'; } return contextText; } catch (e) { Logger.error('Error building context window: $e'); return 'Erro ao construir contexto'; } } /// Get mode-specific instructions static String _getModeInstructions(TutorMode mode) { switch (mode) { case TutorMode.explanation: return 'Forneça explicações detalhadas e claras baseadas apenas no conteúdo fornecido. Use exemplos do material e estruture a resposta de forma lógica.'; case TutorMode.tutor: return 'Use o método socrático - faça perguntas que guiem o aluno a descobrir a resposta. Baseie-se apenas no conteúdo fornecido e incentive o pensamento crítico.'; case TutorMode.exploration: return 'Ajude o aluno a explorar o conceito através de descoberta. Conecte com ideias relacionadas e sugira investigações baseadas no conteúdo.'; } } /// Generate response (placeholder - will be implemented in RAGAIService) static Future _generateResponse({ required String query, required String context, required TutorMode mode, required List sources, }) async { try { // Create source citations final citations = sources .map( (chunk) => SourceCitation( contentId: chunk.contentId, chunkId: chunk.id, title: chunk.sourceDocument, concept: chunk.concept, subject: chunk.subject, excerpt: _getExcerpt(chunk.text), relevance: _calculateRelevance(query, chunk.text), pageNumber: chunk.pageNumber, ), ) .toList(); // Call Ollama API final url = Uri.parse('http://89.114.196.110:11434/api/chat'); // Build prompt with context final prompt = _buildPrompt(query, context, mode); final response = await http .post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'model': 'qwen3-coder:30b', 'messages': [ {'role': 'user', 'content': prompt}, ], 'stream': false, }), ) .timeout(const Duration(seconds: 60)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final answer = responseData['message']['content'] as String; final confidence = _calculateConfidence(sources); final relatedConcepts = _extractRelatedConcepts(sources); return RAGResponse( answer: answer, sources: citations, confidence: confidence, mode: mode, relatedConcepts: relatedConcepts, metadata: { 'queryLength': query.length, 'contextLength': context.length, 'sourceCount': sources.length, 'processingTime': DateTime.now().millisecondsSinceEpoch, 'apiResponse': true, }, ); } else { Logger.error('Ollama API error: ${response.statusCode}'); // Fallback to mock response final answer = _generateMockAnswer(query, context, mode); final confidence = _calculateConfidence(sources); final relatedConcepts = _extractRelatedConcepts(sources); return RAGResponse( answer: answer, sources: citations, confidence: confidence, mode: mode, relatedConcepts: relatedConcepts, metadata: { 'queryLength': query.length, 'contextLength': context.length, 'sourceCount': sources.length, 'processingTime': DateTime.now().millisecondsSinceEpoch, 'apiResponse': false, 'apiError': response.statusCode, }, ); } } catch (e) { Logger.error('Error calling Ollama API: $e'); // Fallback to mock response final answer = _generateMockAnswer(query, context, mode); final confidence = _calculateConfidence(sources); final relatedConcepts = _extractRelatedConcepts(sources); return RAGResponse( answer: answer, sources: sources .map( (chunk) => SourceCitation( contentId: chunk.contentId, chunkId: chunk.id, title: chunk.sourceDocument, concept: chunk.concept, subject: chunk.subject, excerpt: _getExcerpt(chunk.text), relevance: _calculateRelevance(query, chunk.text), pageNumber: chunk.pageNumber, ), ) .toList(), confidence: confidence, mode: mode, relatedConcepts: relatedConcepts, metadata: { 'queryLength': query.length, 'contextLength': context.length, 'sourceCount': sources.length, 'processingTime': DateTime.now().millisecondsSinceEpoch, 'apiResponse': false, 'apiError': e.toString(), }, ); } } /// Get excerpt from text static String _getExcerpt(String text, {int maxLength = 200}) { if (text.length <= maxLength) return text; return text.substring(0, maxLength - 3) + '...'; } /// Build prompt for AI model with educational context static String _buildPrompt(String query, String context, TutorMode mode) { final modeInstructions = _getModeInstructions(mode); return '''$modeInstructions === CONTEÚDO EDUCACIONAL RELEVANTE === $context === PERGUNTA DO ALUNO === $query === INSTRUÇÕES === - Baseie sua resposta APENAS no conteúdo educacional fornecido acima - Se o conteúdo não for suficiente para responder, explique o que está disponível - Use linguagem clara e educacional - Adapte a resposta ao nível do aluno - Forneça exemplos quando possível - Seja conciso mas completo'''; } /// Calculate relevance score static double _calculateRelevance(String query, String text) { final queryWords = query.toLowerCase().split(' '); final textLower = text.toLowerCase(); int matches = 0; for (final word in queryWords) { if (word.length > 2 && textLower.contains(word)) { matches++; } } return matches / queryWords.length; } /// Generate mock answer (placeholder) static String _generateMockAnswer( String query, String context, TutorMode mode, ) { switch (mode) { case TutorMode.explanation: return 'Baseado no conteúdo fornecido, posso explicar que $query. Esta abordagem é fundamentada nos conceitos apresentados no material educacional. A resposta detalhada envolve os princípios teóricos e práticos descritos nas fontes relevantes.'; case TutorMode.tutor: return 'Para entender $query, vamos explorar juntos. O que você já sabe sobre este conceito? Baseado no material, podemos investigar os aspectos fundamentais passo a passo.'; case TutorMode.exploration: return 'Vamos descobrir mais sobre $query! O conteúdo sugere várias abordagens interessantes. Que aspecto gostaria de explorar primeiro? Podemos investigar as conexões entre os diferentes conceitos apresentados.'; } } /// Calculate confidence score static double _calculateConfidence(List sources) { if (sources.isEmpty) return 0.0; // Base confidence on number and quality of sources double confidence = min(sources.length * 0.2, 0.8); // Boost confidence if sources are highly relevant final avgRelevance = sources.map((s) => s.difficulty).reduce((a, b) => a + b) / sources.length; confidence += avgRelevance * 0.2; return min(confidence, 1.0); } /// Extract related concepts static List _extractRelatedConcepts(List sources) { final concepts = {}; for (final source in sources) { concepts.add(source.concept); if (source.subConcept != null) { concepts.add(source.subConcept!); } } return concepts.toList()..sort(); } /// Create response for no content found static RAGResponse _createNoContentResponse(String query, TutorMode mode) { return RAGResponse( answer: 'Desculpe, não encontrei conteúdo relevante para responder à sua pergunta sobre "$query". Tente reformular a pergunta ou verifique se o material sobre este tópico está disponível.', sources: [], confidence: 0.0, mode: mode, relatedConcepts: [], metadata: {'error': 'no_content_found'}, ); } /// Create error response static RAGResponse _createErrorResponse( String query, TutorMode mode, String error, ) { return RAGResponse( answer: 'Ocorreu um erro ao processar sua pergunta. Por favor, tente novamente mais tarde.', sources: [], confidence: 0.0, mode: mode, relatedConcepts: [], metadata: {'error': error}, ); } /// Get conversation history for context static Future>> getConversationHistory({ required String userId, int limit = 10, }) async { try { Logger.info('Getting conversation history for user: $userId'); final querySnapshot = await _firestore .collection('conversations') .where('userId', isEqualTo: userId) .orderBy('createdAt', descending: true) .limit(limit) .get(); return querySnapshot.docs .map((doc) => {'id': doc.id, ...doc.data() as Map}) .toList(); } catch (e) { Logger.error('Error getting conversation history: $e'); return []; } } /// Save conversation to Firestore static Future saveConversation({ required String userId, required String query, required RAGResponse response, }) async { try { Logger.info('Saving conversation for user: $userId'); final conversationData = { 'userId': userId, 'query': query, 'answer': response.answer, 'mode': response.mode.name, 'confidence': response.confidence, 'sources': response.sources .map( (s) => { 'contentId': s.contentId, 'chunkId': s.chunkId, 'title': s.title, 'concept': s.concept, 'subject': s.subject, 'excerpt': s.excerpt, 'relevance': s.relevance, 'pageNumber': s.pageNumber, }, ) .toList(), 'relatedConcepts': response.relatedConcepts, 'metadata': response.metadata, 'createdAt': FieldValue.serverTimestamp(), }; final docRef = await _firestore .collection('conversations') .add(conversationData); final conversationId = docRef.id; Logger.info('Conversation saved: $conversationId'); return conversationId; } catch (e) { Logger.error('Error saving conversation: $e'); throw Exception('Failed to save conversation: $e'); } } /// Get popular concepts static Future>> getPopularConcepts({ int limit = 20, }) async { try { Logger.info('Getting popular concepts'); final querySnapshot = await _firestore .collection('contentChunks') .where('isActive', isEqualTo: true) .limit(100) .get(); final conceptCounts = {}; for (final doc in querySnapshot.docs) { final data = doc.data(); final concept = data['concept'] as String? ?? 'Unknown'; conceptCounts[concept] = (conceptCounts[concept] ?? 0) + 1; } final sortedConcepts = conceptCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return sortedConcepts .take(limit) .map((entry) => {'concept': entry.key, 'count': entry.value}) .toList(); } catch (e) { Logger.error('Error getting popular concepts: $e'); return []; } } }