import 'dart:convert'; import 'package:http/http.dart' as http; import '../utils/logger.dart'; import 'rag_service.dart'; import '../models/content_chunk.dart'; /// Service for RAG-enhanced AI communication using Ollama API class RAGAIService { static const String _baseUrl = 'http://89.114.196.110:11434/api/chat'; static const String _model = 'qwen3-coder:30b'; static const int _timeoutSeconds = 60; static const int _maxTokens = 4000; /// Generate AI response with RAG context static Future generateRAGResponse({ required String userQuery, required String context, required TutorMode mode, required List sources, }) async { try { Logger.info('Generating RAG response with ${sources.length} sources'); // 1. Build the prompt with context final prompt = _buildRAGPrompt(userQuery, context, mode); // 2. Call Ollama API final response = await _callOllamaAPI(prompt); // 3. Process response and create RAGResponse final ragResponse = _createRAGResponse( query: userQuery, aiResponse: response, mode: mode, sources: sources, ); Logger.info('RAG response generated successfully'); return ragResponse; } catch (e) { Logger.error('Error generating RAG response: $e'); return _createErrorResponse(userQuery, mode, e.toString()); } } /// Build RAG-enhanced prompt for Ollama static String _buildRAGPrompt( String userQuery, String context, TutorMode mode, ) { final promptBuilder = StringBuffer(); // System prompt with role and instructions promptBuilder.writeln( 'Você é um assistente educacional especializado da Escola Profissional de Vila do Conde.', ); promptBuilder.writeln( 'Sua função é ajudar os alunos usando APENAS o conteúdo fornecido abaixo.', ); promptBuilder.writeln( 'NÃO use conhecimento externo. Baseie todas as respostas exclusivamente no material educacional.', ); promptBuilder.writeln('Seja claro, paciente e educativo.\n'); // Add context promptBuilder.writeln('=== CONTEÚDO EDUCACIONAL DISPONÍVEL ==='); promptBuilder.writeln(context); promptBuilder.writeln('\n=== FIM DO CONTEÚDO ===\n'); // Mode-specific instructions promptBuilder.writeln('=== MODO DE TUTORIA ==='); switch (mode) { case TutorMode.explanation: promptBuilder.writeln('MODO: EXPLICAÇÃO DETALHADA'); promptBuilder.writeln( 'Forneça explicações claras e detalhadas baseadas exclusivamente no conteúdo.', ); promptBuilder.writeln( 'Use exemplos do material e estruture a resposta de forma lógica.', ); promptBuilder.writeln( 'Se o conteúdo não tiver informação suficiente, indique isso claramente.', ); break; case TutorMode.tutor: promptBuilder.writeln('MODO: TUTORIA SOCRÁTICA'); promptBuilder.writeln( 'Use o método socrático - faça perguntas que guiem o aluno.', ); promptBuilder.writeln('Baseie-se apenas no conteúdo fornecido.'); promptBuilder.writeln('Incentive o pensamento crítico e a descoberta.'); break; case TutorMode.exploration: promptBuilder.writeln('MODO: EXPLORAÇÃO E DESCOBERTA'); promptBuilder.writeln( 'Ajude o aluno a explorar o conceito através de descoberta.', ); promptBuilder.writeln( 'Conecte ideias relacionadas presentes no conteúdo.', ); promptBuilder.writeln( 'Sugira investigações baseadas no material disponível.', ); break; } // User query promptBuilder.writeln('\n=== PERGUNTA DO ALUNO ==='); promptBuilder.writeln(userQuery); promptBuilder.writeln('\n=== RESPOSTA ==='); return promptBuilder.toString(); } /// Call Ollama API static Future _callOllamaAPI(String prompt) async { try { Logger.info('Calling Ollama API with model: $_model'); final url = Uri.parse(_baseUrl); final requestBody = { 'model': _model, 'messages': [ {'role': 'user', 'content': prompt}, ], 'stream': false, 'options': {'temperature': 0.7, 'top_p': 0.9, 'max_tokens': _maxTokens}, }; final response = await http .post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ) .timeout(Duration(seconds: _timeoutSeconds)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final message = responseData['message']; final content = message?['content'] ?? ''; Logger.info('Ollama API response received'); return content.trim(); } else { throw Exception('API Error: ${response.statusCode} - ${response.body}'); } } catch (e) { Logger.error('Error calling Ollama API: $e'); throw Exception('Failed to call AI service: $e'); } } /// Create RAGResponse from AI response static RAGResponse _createRAGResponse({ required String query, required String aiResponse, required TutorMode mode, required List sources, }) { 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(); // Calculate confidence based on sources and response quality final confidence = _calculateResponseConfidence(aiResponse, sources); // Extract related concepts from sources final relatedConcepts = _extractRelatedConcepts(sources); return RAGResponse( answer: aiResponse, sources: citations, confidence: confidence, mode: mode, relatedConcepts: relatedConcepts, metadata: { 'model': _model, 'queryLength': query.length, 'responseLength': aiResponse.length, 'sourceCount': sources.length, 'processingTime': DateTime.now().millisecondsSinceEpoch, 'temperature': 0.7, }, ); } catch (e) { Logger.error('Error creating RAG response: $e'); throw Exception('Failed to create RAG response: $e'); } } /// Get excerpt from text static String _getExcerpt(String text, {int maxLength = 200}) { if (text.length <= maxLength) return text; return text.substring(0, maxLength - 3) + '...'; } /// 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; } /// Calculate response confidence static double _calculateResponseConfidence( String response, List sources, ) { double confidence = 0.0; // Base confidence on number of sources confidence += (sources.length / 5.0) * 0.4; // Max 0.4 for sources // Boost confidence if response mentions concepts from sources final sourceConcepts = sources.map((s) => s.concept.toLowerCase()).toSet(); final responseLower = response.toLowerCase(); int conceptMatches = 0; for (final concept in sourceConcepts) { if (responseLower.contains(concept)) { conceptMatches++; } } confidence += (conceptMatches / sourceConcepts.length) * 0.3; // Max 0.3 for concept matching // Boost confidence if response is substantial if (response.length > 100) { confidence += 0.2; } // Boost confidence if response cites sources if (responseLower.contains('fonte') || responseLower.contains('conteúdo') || responseLower.contains('material')) { confidence += 0.1; } return confidence.clamp(0.0, 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 error response static RAGResponse _createErrorResponse( String query, TutorMode mode, String error, ) { return RAGResponse( answer: 'Desculpe, ocorreu um erro ao processar sua pergunta: $error. Por favor, tente novamente mais tarde.', sources: [], confidence: 0.0, mode: mode, relatedConcepts: [], metadata: {'error': error, 'model': _model}, ); } /// Simple chat without RAG (for fallback) static Future simpleChat(String message) async { try { Logger.info('Simple chat call'); final url = Uri.parse(_baseUrl); final requestBody = { 'model': _model, 'messages': [ { 'role': 'user', 'content': 'Responda de forma curta e direta: $message', }, ], 'stream': false, 'options': {'temperature': 0.7, 'max_tokens': 500}, }; final response = await http .post( url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(requestBody), ) .timeout(Duration(seconds: _timeoutSeconds)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final message = responseData['message']; final content = message?['content'] ?? ''; return content.trim(); } else { throw Exception('API Error: ${response.statusCode}'); } } catch (e) { Logger.error('Error in simple chat: $e'); return 'Desculpe, não consegui processar sua mensagem no momento.'; } } /// Check if Ollama service is available static Future isServiceAvailable() async { try { Logger.info('Checking Ollama service availability'); final url = Uri.parse('http://89.114.196.110:11434/api/tags'); final response = await http.get(url).timeout(Duration(seconds: 10)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final models = responseData['models'] as List? ?? []; final hasModel = models.any( (model) => (model['name'] as String? ?? '').contains('qwen3-coder'), ); Logger.info('Ollama service available, model found: $hasModel'); return hasModel; } else { Logger.warning( 'Ollama service returned status: ${response.statusCode}', ); return false; } } catch (e) { Logger.error('Ollama service not available: $e'); return false; } } /// Get model information static Future?> getModelInfo() async { try { final url = Uri.parse('http://89.114.196.110:11434/api/tags'); final response = await http.get(url).timeout(Duration(seconds: 10)); if (response.statusCode == 200) { final responseData = jsonDecode(response.body); final models = responseData['models'] as List? ?? []; for (final model in models) { if ((model['name'] as String? ?? '').contains('qwen3-coder')) { return model as Map; } } } return null; } catch (e) { Logger.error('Error getting model info: $e'); return null; } } /// Test the service with a simple query static Future testService() async { try { final testQuery = 'Olá, você está funcionando?'; final response = await simpleChat(testQuery); return 'Service test successful: $response'; } catch (e) { return 'Service test failed: $e'; } } }