IA e pequenas coisas a funcionar
This commit is contained in:
408
lib/core/services/rag_ai_service.dart
Normal file
408
lib/core/services/rag_ai_service.dart
Normal file
@@ -0,0 +1,408 @@
|
||||
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<RAGResponse> generateRAGResponse({
|
||||
required String userQuery,
|
||||
required String context,
|
||||
required TutorMode mode,
|
||||
required List<ContentChunk> 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<String> _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<ContentChunk> 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<ContentChunk> 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<String> _extractRelatedConcepts(List<ContentChunk> sources) {
|
||||
final concepts = <String>{};
|
||||
|
||||
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<String> 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<bool> 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<Map<String, dynamic>?> 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<String, dynamic>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
Logger.error('Error getting model info: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Test the service with a simple query
|
||||
static Future<String> 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user