647 lines
22 KiB
Dart
647 lines
22 KiB
Dart
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../utils/logger.dart';
|
|
import 'rag_service.dart';
|
|
import 'chat_memory_service.dart';
|
|
import 'materials_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, conversation memory, and teacher materials
|
|
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');
|
|
|
|
// PASSO 1 — Criar a lista messages vazia
|
|
List<Map<String, String>> messages = [];
|
|
|
|
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
|
messages.add({
|
|
'role': 'system',
|
|
'content': '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
|
|
|
Nunca referes o nome do modelo.
|
|
Nunca dizes que és Qwen ou OpenAI.
|
|
Respondes sempre como o GOAT.
|
|
|
|
Tens personalidade confiante, motivadora e orgulhosa.
|
|
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
|
Usas formatação Markdown clara e organizada.''',
|
|
});
|
|
|
|
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore
|
|
final conversationHistory = await ChatMemoryService.getRecentMessages(limit: 20);
|
|
for (final msg in conversationHistory) {
|
|
messages.add({
|
|
'role': msg['role'] as String,
|
|
'content': msg['content'] as String,
|
|
});
|
|
}
|
|
|
|
// PASSO 4 — BUSCAR PDFs DO PROFESSOR NO Firebase Storage (RAG CHUNK RETRIEVAL)
|
|
final pdfContext = await MaterialsRAGService.getRelevantChunks(
|
|
userQuery: userQuery,
|
|
maxMaterials: 5,
|
|
maxChunks: 5,
|
|
);
|
|
if (pdfContext.isNotEmpty) {
|
|
messages.add({
|
|
'role': 'system',
|
|
'content': pdfContext, // Já vem formatado com [CHUNK 1], [CHUNK 2], etc.
|
|
});
|
|
}
|
|
|
|
// PASSO 5 — SÓ AGORA adicionar a pergunta do user
|
|
messages.add({
|
|
'role': 'user',
|
|
'content': userQuery,
|
|
});
|
|
|
|
// Log do tamanho do array para verificação
|
|
Logger.info('Built messages array with ${messages.length} messages for API');
|
|
|
|
// Save user message to Firestore (after building the messages array)
|
|
await ChatMemoryService.saveMessage(
|
|
role: 'user',
|
|
content: userQuery,
|
|
);
|
|
|
|
// Call Ollama API with complete messages array
|
|
final response = await _callOllamaAPIWithMessages(messages);
|
|
|
|
// Save AI response to memory
|
|
await ChatMemoryService.saveMessage(
|
|
role: 'assistant',
|
|
content: response,
|
|
);
|
|
|
|
// 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 with teacher materials
|
|
static String _buildRAGPrompt(
|
|
String userQuery,
|
|
String context,
|
|
String materialsContext,
|
|
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 teacher materials (PDFs) if available
|
|
if (materialsContext.isNotEmpty) {
|
|
promptBuilder.writeln('=== MATERIAL DO PROFESSOR ===');
|
|
promptBuilder.writeln(materialsContext);
|
|
promptBuilder.writeln('\n=== FIM DO MATERIAL DO PROFESSOR ===\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();
|
|
}
|
|
|
|
/// System message for O GOAT identity (for legacy calls)
|
|
static const String _systemMessage = '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
|
|
|
Nunca referes o nome do modelo.
|
|
Nunca dizes que és Qwen ou OpenAI.
|
|
Respondes sempre como o GOAT.
|
|
|
|
Tens personalidade confiante, motivadora e orgulhosa.
|
|
Ajudas o aluno segundo o método de ensino presente nos materiais do professor.
|
|
Usas formatação clara e organizada.''';
|
|
|
|
/// Call Ollama API with complete messages array
|
|
static Future<String> _callOllamaAPIWithMessages(
|
|
List<Map<String, String>> messages,
|
|
) async {
|
|
try {
|
|
Logger.info('Calling Ollama API with ${messages.length} messages');
|
|
|
|
final url = Uri.parse(_baseUrl);
|
|
|
|
final requestBody = {
|
|
'model': _model,
|
|
'messages': messages,
|
|
'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');
|
|
}
|
|
}
|
|
|
|
/// Legacy: Call Ollama API with single prompt (for backward compatibility)
|
|
static Future<String> _callOllamaAPI(String prompt) async {
|
|
return _callOllamaAPIWithMessages([
|
|
{'role': 'system', 'content': _systemMessage},
|
|
{'role': 'user', 'content': prompt},
|
|
]);
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
}
|
|
|
|
/// Gerar quiz a partir de um prompt com contexto PDF embutido — sem histórico de conversa
|
|
static Future<String> generateQuiz(String prompt) async {
|
|
final messages = <Map<String, String>>[
|
|
{
|
|
'role': 'system',
|
|
'content': 'És um assistente educativo especializado em criar quizzes pedagógicos. '
|
|
'Cria sempre perguntas claras, baseadas exclusivamente no contexto fornecido.',
|
|
},
|
|
{
|
|
'role': 'user',
|
|
'content': prompt,
|
|
},
|
|
];
|
|
return await _callOllamaAPIWithMessages(messages);
|
|
}
|
|
|
|
/// 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';
|
|
}
|
|
}
|
|
|
|
/// Cache do último contexto PDF enviado ao modelo — reutilizado em follow-ups
|
|
static String _lastPdfContext = '';
|
|
|
|
/// Limpar contexto cacheado — chamar ao mudar de material
|
|
static void clearLastContext() {
|
|
_lastPdfContext = '';
|
|
Logger.info('Last PDF context cleared');
|
|
}
|
|
|
|
/// Detecta se a query é um follow-up (pergunta curta/vaga sem keywords de conteúdo)
|
|
static bool _isFollowUp(String query) {
|
|
final q = query.trim().toLowerCase();
|
|
// Menos de 6 palavras E começa com pronome/advérbio de follow-up
|
|
final words = q.split(RegExp(r'\s+'));
|
|
if (words.length > 8) return false;
|
|
const followUpStarters = [
|
|
'e ', 'e o', 'e a', 'e os', 'e as', 'mas ', 'então ',
|
|
'explica', 'explique', 'explica melhor', 'melhor', 'mais detalhes',
|
|
'podes', 'pode ', 'consegues', 'e se ', 'e quando',
|
|
'dá um exemplo', 'da um exemplo', 'um exemplo', 'exemplo',
|
|
'como assim', 'o que significa', 'porquê', 'porque isso',
|
|
'e o ponto', 'e a regra', 'continua', 'continua',
|
|
'o que mais', 'mais algum', 'e depois', 'e agora',
|
|
];
|
|
return followUpStarters.any((s) => q.startsWith(s) || q == s.trim());
|
|
}
|
|
|
|
/// Simple ask method for chat UI - uses conversation memory, teacher PDFs, and O GOAT identity
|
|
/// [selectedMaterialIds] — se fornecido, limita o RAG apenas aos materiais escolhidos pelo aluno
|
|
static Future<String> ask(
|
|
String userQuery, {
|
|
List<String>? selectedMaterialIds,
|
|
}) async {
|
|
Logger.info('USING RAG AI SERVICE');
|
|
|
|
// PASSO 1 — Criar a lista messages vazia
|
|
List<Map<String, String>> messages = [];
|
|
|
|
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO GOAT (SEMPRE PRIMEIRO)
|
|
messages.add({
|
|
'role': 'system',
|
|
'content': '''Tu és "O GOAT", o Assistente IA oficial do Teach it.
|
|
|
|
Nunca referes o nome do modelo.
|
|
Nunca dizes que és Qwen ou OpenAI.
|
|
Respondes sempre como o GOAT.
|
|
|
|
Tens personalidade confiante, motivadora e orgulhosa.
|
|
Usas formatação Markdown clara e organizada.
|
|
|
|
REGRAS CRÍTICAS SOBRE O CONTEXTO:
|
|
- Quando te for fornecido contexto de materiais (entre [MATERIAL: ...]), responde EXCLUSIVAMENTE com base nesse conteúdo.
|
|
- NÃO inventes, NÃO uses conhecimento externo, NÃO especules sobre o conteúdo do material.
|
|
- Se a resposta não estiver no contexto fornecido, diz claramente: "Não encontrei essa informação no material disponível."
|
|
- Cita sempre de onde tiraste a informação (ex: "Segundo o material...").''',
|
|
});
|
|
|
|
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 4 para poupar heap)
|
|
final conversationHistory = await ChatMemoryService.getRecentMessages(limit: 4);
|
|
for (final msg in conversationHistory) {
|
|
messages.add({
|
|
'role': msg['role'] as String,
|
|
'content': msg['content'] as String,
|
|
});
|
|
}
|
|
|
|
// Log de confirmação de ordem do histórico
|
|
if (messages.length > 1) {
|
|
Logger.info('History order fixed. First message: ${messages[1]}');
|
|
}
|
|
|
|
// PASSO 4 — BUSCAR PDFs DO PROFESSOR NO Firebase Storage (RAG CHUNK RETRIEVAL)
|
|
// Detectar follow-up e reutilizar contexto anterior se disponível
|
|
String pdfContext;
|
|
if (_isFollowUp(userQuery) && _lastPdfContext.isNotEmpty) {
|
|
pdfContext = _lastPdfContext;
|
|
Logger.info('Follow-up detected — reusing last PDF context (${pdfContext.length} chars)');
|
|
} else {
|
|
pdfContext = await MaterialsRAGService.getRelevantChunks(
|
|
userQuery: userQuery,
|
|
maxMaterials: 5,
|
|
maxChunks: 5,
|
|
selectedMaterialIds: selectedMaterialIds,
|
|
);
|
|
if (pdfContext.isNotEmpty) {
|
|
_lastPdfContext = pdfContext;
|
|
Logger.info('PDF context sent to model (${pdfContext.length} chars): ${pdfContext.length > 300 ? pdfContext.substring(0, 300) : pdfContext}');
|
|
}
|
|
}
|
|
if (pdfContext.isEmpty && selectedMaterialIds != null && selectedMaterialIds.isNotEmpty) {
|
|
// Contexto vazio com materiais seleccionados — retornar resposta local imediatamente
|
|
const noContextReply =
|
|
'Neste momento não tenho acesso ao conteúdo do ficheiro selecionado. '
|
|
'Tenta novamente ou faz uma pergunta geral — estou aqui para ajudar! 💪';
|
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
|
await ChatMemoryService.saveMessage(role: 'assistant', content: noContextReply);
|
|
return noContextReply;
|
|
}
|
|
if (pdfContext.isEmpty && (selectedMaterialIds == null || selectedMaterialIds.isEmpty)) {
|
|
// Sem material seleccionado — pedir ao utilizador para seleccionar um
|
|
const noMaterialReply =
|
|
'Para responder a perguntas sobre conteúdo, preciso que selecciones um material primeiro. '
|
|
'📚 Usa o botão de materiais para escolher um PDF e depois faz a tua pergunta!';
|
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
|
await ChatMemoryService.saveMessage(role: 'assistant', content: noMaterialReply);
|
|
return noMaterialReply;
|
|
}
|
|
|
|
// PASSO 5 — adicionar a pergunta do user (com contexto embutido se disponível)
|
|
final userContent = pdfContext.isNotEmpty
|
|
? '''Usa APENAS o seguinte contexto para responder. Não uses conhecimento externo.
|
|
Se a resposta não estiver no contexto, diz: "Não encontrei essa informação no material disponível."
|
|
|
|
$pdfContext
|
|
|
|
Pergunta: $userQuery'''
|
|
: userQuery;
|
|
messages.add({
|
|
'role': 'user',
|
|
'content': userContent,
|
|
});
|
|
|
|
Logger.info('USING RAG AI SERVICE - Built messages array with ${messages.length} messages');
|
|
|
|
// Save user message to Firestore
|
|
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
|
|
|
|
// Call API
|
|
final response = await _callOllamaAPIWithMessages(messages);
|
|
|
|
// Save AI response to memory
|
|
await ChatMemoryService.saveMessage(role: 'assistant', content: response);
|
|
|
|
return response;
|
|
}
|
|
}
|