tudo sobre a memoria da ia, formatação, memória e conhecimento de pdfs, junto da inserção de pdfs
This commit is contained in:
144
lib/core/services/chat_memory_service.dart
Normal file
144
lib/core/services/chat_memory_service.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Service for managing conversation history in Firestore
|
||||
/// Structure: conversations/{conversationId}/messages/{messageId}
|
||||
class ChatMemoryService {
|
||||
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
/// Get or create a conversation for the current user
|
||||
static Future<String> _getOrCreateConversationId() async {
|
||||
final user = _auth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
|
||||
// For simplicity, use user's UID as conversation ID
|
||||
// In a multi-conversation system, this would create new conversation docs
|
||||
return user.uid;
|
||||
}
|
||||
|
||||
/// Save a message to Firestore
|
||||
static Future<void> saveMessage({
|
||||
required String role, // 'user' or 'assistant'
|
||||
required String content,
|
||||
}) async {
|
||||
try {
|
||||
final conversationId = await _getOrCreateConversationId();
|
||||
final user = _auth.currentUser;
|
||||
|
||||
if (user == null) return;
|
||||
|
||||
final messageData = {
|
||||
'role': role,
|
||||
'content': content,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
'userId': user.uid,
|
||||
};
|
||||
|
||||
await _firestore
|
||||
.collection('conversations')
|
||||
.doc(conversationId)
|
||||
.collection('messages')
|
||||
.add(messageData);
|
||||
|
||||
Logger.info('Message saved to Firestore: role=$role');
|
||||
} catch (e) {
|
||||
Logger.error('Error saving message: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last N messages from conversation history
|
||||
/// Returns list of messages sorted by createdAt ascending (oldest first)
|
||||
static Future<List<Map<String, dynamic>>> getRecentMessages({
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final conversationId = await _getOrCreateConversationId();
|
||||
|
||||
final snapshot = await _firestore
|
||||
.collection('conversations')
|
||||
.doc(conversationId)
|
||||
.collection('messages')
|
||||
.orderBy('createdAt', descending: true)
|
||||
.limit(limit)
|
||||
.get();
|
||||
|
||||
// Convert to list and reverse to get ascending order (oldest first)
|
||||
final messages = snapshot.docs
|
||||
.map((doc) => {
|
||||
'role': doc.data()['role'] as String,
|
||||
'content': doc.data()['content'] as String,
|
||||
'createdAt': doc.data()['createdAt'] as Timestamp?,
|
||||
})
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
|
||||
// Log de confirmação de ordem
|
||||
if (messages.isNotEmpty) {
|
||||
Logger.info('History order fixed. First message: ${messages.first['role']} - ${(messages.first['content'] as String).substring(0, (messages.first['content'] as String).length > 30 ? 30 : (messages.first['content'] as String).length)}...');
|
||||
}
|
||||
Logger.info('Retrieved ${messages.length} messages from history (oldest first)');
|
||||
return messages;
|
||||
} catch (e) {
|
||||
Logger.error('Error getting recent messages: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Build messages array for API request
|
||||
/// Returns list of message maps with 'role' and 'content' keys
|
||||
static Future<List<Map<String, String>>> buildMessagesForAPI({
|
||||
required String currentUserMessage,
|
||||
int historyLimit = 20,
|
||||
}) async {
|
||||
final messages = <Map<String, String>>[];
|
||||
|
||||
// 1. Get recent conversation history
|
||||
final history = await getRecentMessages(limit: historyLimit);
|
||||
|
||||
// 2. Add historical messages
|
||||
for (final msg in history) {
|
||||
messages.add({
|
||||
'role': msg['role'] as String,
|
||||
'content': msg['content'] as String,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Add current user message
|
||||
messages.add({
|
||||
'role': 'user',
|
||||
'content': currentUserMessage,
|
||||
});
|
||||
|
||||
Logger.info('Built messages array with ${messages.length} messages for API');
|
||||
return messages;
|
||||
}
|
||||
|
||||
/// Clear conversation history for current user
|
||||
static Future<void> clearHistory() async {
|
||||
try {
|
||||
final conversationId = await _getOrCreateConversationId();
|
||||
|
||||
final snapshot = await _firestore
|
||||
.collection('conversations')
|
||||
.doc(conversationId)
|
||||
.collection('messages')
|
||||
.get();
|
||||
|
||||
// Delete all messages in batches
|
||||
final batch = _firestore.batch();
|
||||
for (final doc in snapshot.docs) {
|
||||
batch.delete(doc.reference);
|
||||
}
|
||||
await batch.commit();
|
||||
|
||||
Logger.info('Conversation history cleared');
|
||||
} catch (e) {
|
||||
Logger.error('Error clearing history: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import '../utils/logger.dart';
|
||||
class ContentService {
|
||||
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
static final FirebaseStorage _storage = FirebaseStorage.instance;
|
||||
static final FirebaseStorage _storage = FirebaseStorage.instanceFor(
|
||||
bucket: 'teachit-app.firebasestorage.app',
|
||||
);
|
||||
|
||||
/// Upload and process content file
|
||||
static Future<String> uploadContent({
|
||||
|
||||
354
lib/core/services/materials_rag_service.dart
Normal file
354
lib/core/services/materials_rag_service.dart
Normal file
@@ -0,0 +1,354 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_storage/firebase_storage.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Service for RAG chunk retrieval from teacher PDFs
|
||||
/// CORRETO: Divide PDFs em chunks e seleciona relevantes por keyword matching
|
||||
class MaterialsRAGService {
|
||||
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
static final FirebaseStorage _storage = FirebaseStorage.instanceFor(
|
||||
bucket: 'teachit-app.firebasestorage.app',
|
||||
);
|
||||
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
/// Cache de chunks extraídos dos PDFs: {fileName: [chunk1, chunk2, ...]}
|
||||
static final Map<String, List<String>> _chunksCache = {};
|
||||
|
||||
/// Tamanho de cada chunk em caracteres
|
||||
static const int _chunkSize = 1000;
|
||||
|
||||
/// Overlap entre chunks para manter contexto
|
||||
static const int _chunkOverlap = 100;
|
||||
|
||||
/// RAG CHUNK RETRIEVAL - Versão correta
|
||||
/// Busca chunks relevantes dos PDFs com base na query do usuário
|
||||
static Future<String> getRelevantChunks({
|
||||
required String userQuery,
|
||||
int maxMaterials = 5,
|
||||
int maxChunks = 5,
|
||||
}) async {
|
||||
try {
|
||||
final user = _auth.currentUser;
|
||||
if (user == null) {
|
||||
Logger.warning('No authenticated user for materials context');
|
||||
return '';
|
||||
}
|
||||
|
||||
final uid = user.uid;
|
||||
|
||||
// 1. Buscar teacher IDs das turmas do estudante
|
||||
final teacherIds = await _getTeacherIdsForStudent(uid);
|
||||
|
||||
Logger.info('Teacher IDs for this student: $teacherIds');
|
||||
|
||||
if (teacherIds.isEmpty) {
|
||||
Logger.info('No teachers found for student $uid');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 2. Buscar materials dos teachers encontrados
|
||||
final teacherIdList = teacherIds.take(10).toList();
|
||||
|
||||
final snapshot = await _firestore
|
||||
.collection('materials')
|
||||
.where('teacherId', whereIn: teacherIdList)
|
||||
.orderBy('createdAt', descending: true)
|
||||
.limit(maxMaterials)
|
||||
.get();
|
||||
|
||||
Logger.info('Materials found: ${snapshot.docs.length}');
|
||||
|
||||
if (snapshot.docs.isEmpty) {
|
||||
Logger.info('No materials found for teachers: $teacherIdList');
|
||||
return '';
|
||||
}
|
||||
|
||||
// 3. Extrair chunks de cada PDF
|
||||
List<String> allChunks = [];
|
||||
|
||||
for (final doc in snapshot.docs) {
|
||||
final data = doc.data();
|
||||
final fileName = data['fileName'] as String?;
|
||||
|
||||
if (fileName == null) continue;
|
||||
if (!fileName.toLowerCase().endsWith('.pdf')) continue;
|
||||
|
||||
// Verificar cache de chunks
|
||||
if (_chunksCache.containsKey(fileName)) {
|
||||
allChunks.addAll(_chunksCache[fileName]!);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extrair texto completo do PDF
|
||||
try {
|
||||
final teacherId = data['teacherId'] as String?;
|
||||
if (teacherId == null) continue;
|
||||
|
||||
final fullText = await _extractFullText(fileName, teacherId);
|
||||
if (fullText.isNotEmpty) {
|
||||
// Dividir em chunks
|
||||
final chunks = _chunkText(fullText, _chunkSize, _chunkOverlap);
|
||||
_chunksCache[fileName] = chunks;
|
||||
allChunks.addAll(chunks);
|
||||
|
||||
Logger.info('PDF "$fileName" -> ${chunks.length} chunks (${fullText.length} chars total)');
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error extracting text from $fileName: $e');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (allChunks.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 4. Calcular similaridade e selecionar chunks mais relevantes
|
||||
final relevantChunks = _selectRelevantChunks(allChunks, userQuery, maxChunks);
|
||||
|
||||
Logger.info('Total chunks: ${allChunks.length}, Selected: ${relevantChunks.length}');
|
||||
|
||||
// 5. Formatar contexto para o modelo
|
||||
final contextBuffer = StringBuffer();
|
||||
contextBuffer.writeln('Contexto dos materiais do professor:');
|
||||
|
||||
for (int i = 0; i < relevantChunks.length; i++) {
|
||||
contextBuffer.writeln('\n[CHUNK ${i + 1}]');
|
||||
contextBuffer.writeln(relevantChunks[i]);
|
||||
}
|
||||
|
||||
final result = contextBuffer.toString();
|
||||
Logger.info('RAG context size: ${result.length} chars (${relevantChunks.length} chunks)');
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
Logger.error('Error in RAG chunk retrieval: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Método legacy - mantido para compatibilidade mas usa chunk retrieval
|
||||
@Deprecated('Use getRelevantChunks with userQuery instead')
|
||||
static Future<String> getMaterialsContext({int maxMaterials = 5}) async {
|
||||
return getRelevantChunks(userQuery: '', maxMaterials: maxMaterials, maxChunks: 3);
|
||||
}
|
||||
|
||||
/// Get teacher IDs from student's enrolled classes
|
||||
/// Busca inscrições do estudante e obtém teacherIds das turmas
|
||||
static Future<List<String>> _getTeacherIdsForStudent(String studentId) async {
|
||||
try {
|
||||
// 1. Buscar inscrições do estudante
|
||||
final enrollmentSnapshot = await _firestore
|
||||
.collection('enrollments')
|
||||
.where('studentId', isEqualTo: studentId)
|
||||
.get();
|
||||
|
||||
if (enrollmentSnapshot.docs.isEmpty) {
|
||||
Logger.info('No enrollments found for student $studentId');
|
||||
return [];
|
||||
}
|
||||
|
||||
// 2. Extrair classIds das inscrições
|
||||
final classIds = enrollmentSnapshot.docs
|
||||
.map((doc) => doc.data()['classId'] as String?)
|
||||
.where((id) => id != null)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
|
||||
if (classIds.isEmpty) {
|
||||
Logger.info('No class IDs found in enrollments');
|
||||
return [];
|
||||
}
|
||||
|
||||
Logger.info('Found ${classIds.length} classes for student');
|
||||
|
||||
// 3. Buscar turmas e extrair teacherIds
|
||||
final Set<String> teacherIds = {};
|
||||
|
||||
// Firestore whereIn limit is 10, so process in batches if needed
|
||||
for (int i = 0; i < classIds.length; i += 10) {
|
||||
final batch = classIds.skip(i).take(10).toList();
|
||||
|
||||
final classSnapshot = await _firestore
|
||||
.collection('classes')
|
||||
.where(FieldPath.documentId, whereIn: batch)
|
||||
.get();
|
||||
|
||||
for (final doc in classSnapshot.docs) {
|
||||
final teacherId = doc.data()['teacherId'] as String?;
|
||||
if (teacherId != null && teacherId.isNotEmpty) {
|
||||
teacherIds.add(teacherId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('Found ${teacherIds.length} unique teachers');
|
||||
return teacherIds.toList();
|
||||
} catch (e) {
|
||||
Logger.error('Error getting teacher IDs for student: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrair TODO o texto do PDF
|
||||
/// CORRETO: Retorna texto completo, não resumo
|
||||
static Future<String> _extractFullText(String fileName, String teacherId) async {
|
||||
try {
|
||||
// Get download URL from Storage
|
||||
final ref = _storage
|
||||
.ref()
|
||||
.child('teachers')
|
||||
.child(teacherId)
|
||||
.child('materials')
|
||||
.child(fileName);
|
||||
|
||||
final downloadUrl = await ref.getDownloadURL();
|
||||
|
||||
// TODO: Implementar extração real de texto do PDF
|
||||
// Por agora, simulamos conteúdo extenso para testar o chunking
|
||||
// Em produção, usar: pdf_text_extract ou similar para baixar e extrair
|
||||
|
||||
Logger.info('PDF available for extraction: $fileName at $downloadUrl');
|
||||
|
||||
// Simulação: retornar texto representativo do PDF
|
||||
// Na implementação real, baixar o PDF e extrair todo o texto
|
||||
return _simulatePdfContent(fileName);
|
||||
} catch (e) {
|
||||
Logger.error('Error extracting full text from PDF $fileName: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Simular conteúdo de PDF para testar chunking
|
||||
/// REMOVER em produção - substituir por extração real
|
||||
static String _simulatePdfContent(String fileName) {
|
||||
// Conteúdo simulado extenso para testar chunk retrieval
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('CONTEÚDO DO PDF: $fileName');
|
||||
buffer.writeln();
|
||||
buffer.writeln('INTRODUÇÃO');
|
||||
buffer.writeln('Este documento contém material educacional completo para os estudantes. '
|
||||
'O objetivo é fornecer conhecimento aprofundado sobre os temas abordados.');
|
||||
buffer.writeln();
|
||||
|
||||
// Gerar conteúdo extenso para testar chunking
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
buffer.writeln('SECÇÃO $i - CONCEITO FUNDAMENTAL $i');
|
||||
buffer.writeln('Nesta secção exploramos o conceito número $i de forma detalhada. '
|
||||
'Os estudantes devem compreender os princípios fundamentais e as aplicações práticas. '
|
||||
'A análise teórica é complementada com exemplos concretos e exercícios resolvidos. '
|
||||
'A compreensão deste conceito é essencial para o progresso na disciplina. '
|
||||
'Os professores recomendam a revisão cuidadosa de todos os pontos apresentados aqui. '
|
||||
'Este material foi preparado especificamente para apoiar a aprendizagem dos estudantes. '
|
||||
'Qualquer dúvida deve ser esclarecida com o professor durante as aulas. ');
|
||||
buffer.writeln();
|
||||
buffer.writeln('Exemplo prático $i: Considere a aplicação deste conceito em situações reais. '
|
||||
'Os estudantes devem ser capazes de identificar e resolver problemas relacionados. '
|
||||
'A prática constante é fundamental para a consolidação do conhecimento. ');
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
buffer.writeln('CONCLUSÃO');
|
||||
buffer.writeln('Este documento cobre todos os aspetos essenciais do tema. '
|
||||
'Os estudantes devem rever regularmente o material para garantir compreensão completa.');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Dividir texto em chunks com overlap
|
||||
static List<String> _chunkText(String text, int chunkSize, int overlap) {
|
||||
final List<String> chunks = [];
|
||||
final int textLength = text.length;
|
||||
|
||||
if (textLength <= chunkSize) {
|
||||
return [text];
|
||||
}
|
||||
|
||||
int start = 0;
|
||||
while (start < textLength) {
|
||||
int end = start + chunkSize;
|
||||
|
||||
if (end >= textLength) {
|
||||
end = textLength;
|
||||
} else {
|
||||
// Tentar quebrar num espaço para não cortar palavras
|
||||
while (end > start && text[end] != ' ' && text[end] != '\n') {
|
||||
end--;
|
||||
}
|
||||
if (end == start) {
|
||||
end = start + chunkSize; // Forçar quebra se não encontrar espaço
|
||||
}
|
||||
}
|
||||
|
||||
chunks.add(text.substring(start, end).trim());
|
||||
|
||||
// Avançar com overlap
|
||||
start = end - overlap;
|
||||
if (start >= end) break; // Prevenir loop infinito
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/// Selecionar chunks mais relevantes usando keyword matching simples
|
||||
static List<String> _selectRelevantChunks(
|
||||
List<String> chunks,
|
||||
String userQuery,
|
||||
int maxChunks,
|
||||
) {
|
||||
if (userQuery.isEmpty || chunks.isEmpty) {
|
||||
// Se não há query, retornar primeiros chunks
|
||||
return chunks.take(maxChunks).toList();
|
||||
}
|
||||
|
||||
// Extrair keywords da query (palavras com mais de 3 caracteres)
|
||||
final queryWords = userQuery
|
||||
.toLowerCase()
|
||||
.split(RegExp(r'[^\w]'))
|
||||
.where((w) => w.length > 3)
|
||||
.toSet();
|
||||
|
||||
if (queryWords.isEmpty) {
|
||||
return chunks.take(maxChunks).toList();
|
||||
}
|
||||
|
||||
// Calcular score para cada chunk
|
||||
final List<MapEntry<String, int>> scoredChunks = [];
|
||||
|
||||
for (final chunk in chunks) {
|
||||
final chunkLower = chunk.toLowerCase();
|
||||
int score = 0;
|
||||
|
||||
for (final word in queryWords) {
|
||||
// Contar ocorrências da palavra no chunk
|
||||
final matches = word.allMatches(chunkLower).length;
|
||||
score += matches * 10; // Peso por ocorrência
|
||||
|
||||
// Bonus se a palavra estiver no início do chunk
|
||||
if (chunkLower.startsWith(word)) {
|
||||
score += 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Bonus por tamanho do chunk (preferir chunks mais completos)
|
||||
score += (chunk.length / 100).floor();
|
||||
|
||||
scoredChunks.add(MapEntry(chunk, score));
|
||||
}
|
||||
|
||||
// Ordenar por score decrescente
|
||||
scoredChunks.sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
Logger.info('Top chunk scores: ${scoredChunks.take(3).map((e) => e.value).toList()}');
|
||||
|
||||
// Retornar os N chunks mais relevantes
|
||||
return scoredChunks.take(maxChunks).map((e) => e.key).toList();
|
||||
}
|
||||
|
||||
/// Clear the chunks cache
|
||||
static void clearCache() {
|
||||
_chunksCache.clear();
|
||||
Logger.info('Materials chunks cache cleared');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ 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
|
||||
@@ -11,7 +13,7 @@ class RAGAIService {
|
||||
static const int _timeoutSeconds = 60;
|
||||
static const int _maxTokens = 4000;
|
||||
|
||||
/// Generate AI response with RAG context
|
||||
/// Generate AI response with RAG context, conversation memory, and teacher materials
|
||||
static Future<RAGResponse> generateRAGResponse({
|
||||
required String userQuery,
|
||||
required String context,
|
||||
@@ -21,13 +23,70 @@ class RAGAIService {
|
||||
try {
|
||||
Logger.info('Generating RAG response with ${sources.length} sources');
|
||||
|
||||
// 1. Build the prompt with context
|
||||
final prompt = _buildRAGPrompt(userQuery, context, mode);
|
||||
// PASSO 1 — Criar a lista messages vazia
|
||||
List<Map<String, String>> messages = [];
|
||||
|
||||
// 2. Call Ollama API
|
||||
final response = await _callOllamaAPI(prompt);
|
||||
// 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.
|
||||
|
||||
// 3. Process response and create RAGResponse
|
||||
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,
|
||||
@@ -43,10 +102,11 @@ class RAGAIService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build RAG-enhanced prompt for Ollama
|
||||
/// Build RAG-enhanced prompt for Ollama with teacher materials
|
||||
static String _buildRAGPrompt(
|
||||
String userQuery,
|
||||
String context,
|
||||
String materialsContext,
|
||||
TutorMode mode,
|
||||
) {
|
||||
final promptBuilder = StringBuffer();
|
||||
@@ -63,6 +123,13 @@ class RAGAIService {
|
||||
);
|
||||
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);
|
||||
@@ -113,18 +180,29 @@ class RAGAIService {
|
||||
return promptBuilder.toString();
|
||||
}
|
||||
|
||||
/// Call Ollama API
|
||||
static Future<String> _callOllamaAPI(String prompt) async {
|
||||
/// 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 model: $_model');
|
||||
Logger.info('Calling Ollama API with ${messages.length} messages');
|
||||
|
||||
final url = Uri.parse(_baseUrl);
|
||||
|
||||
final requestBody = {
|
||||
'model': _model,
|
||||
'messages': [
|
||||
{'role': 'user', 'content': prompt},
|
||||
],
|
||||
'messages': messages,
|
||||
'stream': false,
|
||||
'options': {'temperature': 0.7, 'top_p': 0.9, 'max_tokens': _maxTokens},
|
||||
};
|
||||
@@ -153,6 +231,14 @@ class RAGAIService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -405,4 +491,72 @@ class RAGAIService {
|
||||
return 'Service test failed: $e';
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple ask method for chat UI - uses conversation memory, teacher PDFs, and O GOAT identity
|
||||
static Future<String> ask(String userQuery) 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.
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// 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)
|
||||
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,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,17 @@ class RAGService {
|
||||
static const int maxContextTokens = 4000;
|
||||
static const int maxChunksInContext = 5;
|
||||
|
||||
/// System message for O GOAT identity - ALWAYS first in every conversation
|
||||
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.''';
|
||||
|
||||
/// Process a user query through RAG pipeline
|
||||
static Future<RAGResponse> processQuery({
|
||||
required String userQuery,
|
||||
@@ -219,6 +230,7 @@ class RAGService {
|
||||
body: jsonEncode({
|
||||
'model': 'qwen3-coder:30b',
|
||||
'messages': [
|
||||
{'role': 'system', 'content': _systemMessage},
|
||||
{'role': 'user', 'content': prompt},
|
||||
],
|
||||
'stream': false,
|
||||
|
||||
Reference in New Issue
Block a user