IA e pequenas coisas a funcionar

This commit is contained in:
2026-05-10 18:45:00 +01:00
parent 0f382e970b
commit 3475b57036
21 changed files with 4484 additions and 72 deletions

View 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';
}
}
}