Melhorias no comportamento do chat com IA;

Adição do histórico de conversas com IA.
This commit is contained in:
2026-05-21 23:20:18 +01:00
parent 5bda59f7af
commit 7ee262f4c7
5 changed files with 825 additions and 162 deletions

View File

@@ -860,11 +860,153 @@ ans é o índice (0-3) da opção correcta.''';
return followUpStarters.any((s) => q.startsWith(s) || q == s.trim());
}
/// Build dynamic system prompt based on selected materials and discipline
static String _buildSystemPrompt({
List<String>? selectedMaterialNames,
String? disciplineName,
bool isMathematics = false,
}) {
final buffer = StringBuffer();
buffer.writeln(
r'''Tu és "Vico", o Assistente IA oficial do Learn It — uma plataforma educativa portuguesa.''',
);
buffer.writeln();
buffer.writeln('Nunca referes o nome do modelo de linguagem.');
buffer.writeln('Nunca dizes que és Qwen, OpenAI ou qualquer outro modelo.');
buffer.writeln('Respondes sempre como o Vico.');
buffer.writeln();
buffer.writeln('Tens personalidade simpática, confiante e motivadora.');
buffer.writeln(
'Podes responder normalmente a saudações, agradecimentos e conversa casual — sê natural e amigável.',
);
buffer.writeln();
// Material context section
if (disciplineName != null && disciplineName.isNotEmpty) {
buffer.writeln('DISCIPLINA ATUAL: $disciplineName');
}
if (selectedMaterialNames != null && selectedMaterialNames.isNotEmpty) {
buffer.writeln(
'MATERIAIS SELECIONADOS: ${selectedMaterialNames.join(", ")}',
);
}
if (disciplineName != null ||
(selectedMaterialNames != null && selectedMaterialNames.isNotEmpty)) {
buffer.writeln();
}
// LaTeX prohibition (always)
buffer.writeln(
'IMPORTANTE: NUNCA uses LaTeX ou símbolos como \$ ou \$\$ para fórmulas matemáticas.',
);
buffer.writeln(
'Usa apenas texto normal e caracteres Unicode para símbolos matemáticos (ex: x², ³, ¹⁄², π, √).',
);
buffer.writeln();
// Discipline-specific rules
if (isMathematics) {
buffer.writeln('REGRAS CRÍTICAS PARA MATEMÁTICA:');
buffer.writeln(
'- O material fornecido serve como REFERÊNCIA de matéria, fórmulas e métodos.',
);
buffer.writeln(
'- Podes CRIAR exercícios NOVOS que sigam a mesma lógica, fórmulas e métodos do material.',
);
buffer.writeln(
'- NÃO copies exercícios diretamente do material — cria variações com valores diferentes.',
);
buffer.writeln(
'- Se o aluno pedir exercícios, cria exercícios novos baseados nos CONCEITOS e FÓRMULAS do material.',
);
buffer.writeln(
'- Se a resposta não estiver no contexto, usa o teu conhecimento matemático geral para ajudar, mas prioriza os métodos do material.',
);
buffer.writeln(
'- SEMPRE fornece o passo a passo completo com o resultado final.',
);
} else {
buffer.writeln('REGRAS CRÍTICAS PARA PERGUNTAS EDUCATIVAS:');
buffer.writeln(
'- Quando te for fornecido contexto de materiais do professor (indicado com [MATERIAL: ...]), responde EXCLUSIVAMENTE com base nesse conteúdo.',
);
buffer.writeln(
'- NÃO inventes factos educativos, NÃO uses conhecimento externo sobre matérias escolares.',
);
buffer.writeln(
'- Se a resposta educativa não estiver no contexto fornecido, diz claramente: "Não encontrei essa informação no material disponível."',
);
}
buffer.writeln(
'- Para conversa casual e saudações não precisas de contexto — responde livremente com a tua personalidade.',
);
buffer.writeln();
// Material handling rules (always)
buffer.writeln('IMPORTANTE - COMO TRATAR MATERIAIS SELECIONADOS:');
buffer.writeln(
'- Quando o aluno selecionar materiais (PDFs, fichas, exames), ASSUME que o aluno quer ajuda com esses materiais.',
);
buffer.writeln(
'- NUNCA perguntes "que ficha pretendo resolver" ou "o que pretendo resolver".',
);
buffer.writeln(
'- NUNCA perguntes "em que posso ajudar" quando materiais estão selecionados.',
);
buffer.writeln(
'- ASSUME automaticamente que o aluno quer explicação, resolução ou ajuda com os materiais selecionados.',
);
if (selectedMaterialNames != null && selectedMaterialNames.isNotEmpty) {
buffer.writeln(
'- Se a pergunta do aluno for vaga (ex: "ajuda"), oferece ajuda específica sobre os materiais selecionados.',
);
}
buffer.writeln();
// Greeting rules (always)
buffer.writeln('IMPORTANTE - NÃO REPITAS SAUDAÇÕES:');
buffer.writeln(
'- NUNCA começes uma resposta com "Olá", "Olá de novo", "Oi", "Bom dia", etc., a menos que seja a PRIMEIRA mensagem da conversa.',
);
buffer.writeln(
'- Nas respostas subsequentes, vai DIRETO ao assunto sem saudar novamente.',
);
buffer.writeln(
'- Se já saudaste o utilizador uma vez na conversa, NUNCA o faças de novo.',
);
buffer.writeln();
// Complete response rules (always)
buffer.writeln('IMPORTANTE - RESPOSTAS COMPLETAS:');
buffer.writeln('- NUNCA termines respostas com dois pontos (:).');
buffer.writeln(
'- NUNCA deixes respostas incompletas como "A função é: " ou "Calculamos o denominador: ".',
);
buffer.writeln(
'- SEMPRE completa as frases e fornece a resposta completa.',
);
buffer.writeln(
'- Se precisares de explicar um cálculo, explica-o completamente com o resultado final.',
);
buffer.writeln(
'- Se precisares de definir algo, fornece a definição completa.',
);
return buffer.toString();
}
/// Simple ask method for chat UI - uses conversation memory, teacher PDFs, and Vico identity
/// [selectedMaterialIds] — se fornecido, limita o RAG apenas aos materiais escolhidos pelo aluno
/// [selectedMaterialNames] — nomes dos materiais selecionados (para contextualizar a IA)
/// [disciplineName] — nome da disciplina (ex: "Matemática A", "Português")
/// [isMathematics] — true se for matemática ou disciplina quantitativa
static Future<String> ask(
String userQuery, {
List<String>? selectedMaterialIds,
List<String>? selectedMaterialNames,
String? disciplineName,
bool isMathematics = false,
}) async {
Logger.info('USING RAG AI SERVICE');
@@ -872,50 +1014,21 @@ ans é o índice (0-3) da opção correcta.''';
List<Map<String, String>> messages = [];
// PASSO 2 — ADICIONAR SYSTEM MESSAGE DO VICO (SEMPRE PRIMEIRO)
messages.add({
'role': 'system',
'content':
r'''Tu és "Vico", o Assistente IA oficial do Learn It — uma plataforma educativa portuguesa.
final systemPrompt = _buildSystemPrompt(
selectedMaterialNames: selectedMaterialNames,
disciplineName: disciplineName,
isMathematics: isMathematics,
);
messages.add({'role': 'system', 'content': systemPrompt});
Nunca referes o nome do modelo de linguagem.
Nunca dizes que és Qwen, OpenAI ou qualquer outro modelo.
Respondes sempre como o Vico.
Tens personalidade simpática, confiante e motivadora.
Podes responder normalmente a saudações, agradecimentos e conversa casual — sê natural e amigável.
IMPORTANTE: NUNCA uses LaTeX ou símbolos como $ ou $$ para fórmulas matemáticas.
Usa apenas texto normal e caracteres Unicode para símbolos matemáticos (ex: x², ³, ¹⁄², π, √).
REGRAS CRÍTICAS PARA PERGUNTAS EDUCATIVAS:
- Quando te for fornecido contexto de materiais do professor (indicado com [MATERIAL: ...]), responde EXCLUSIVAMENTE com base nesse conteúdo.
- NÃO inventes factos educativos, NÃO uses conhecimento externo sobre matérias escolares.
- Se a resposta educativa não estiver no contexto fornecido, diz claramente: "Não encontrei essa informação no material disponível."
- Para conversa casual e saudações não precisas de contexto — responde livremente com a tua personalidade.
IMPORTANTE - COMO TRATAR MATERIAIS SELECIONADOS:
- Quando o aluno selecionar materiais (PDFs, fichas, exames), ASSUME que o aluno quer ajuda com esses materiais.
- NUNCA perguntes "que ficha pretendo resolver" ou "o que pretendo resolver".
- NUNCA perguntes "em que posso ajudar" quando materiais estão selecionados.
- ASSUME automaticamente que o aluno quer explicação, resolução ou ajuda com os materiais selecionados.
- Analisa os materiais selecionados e oferece ajuda proativamente: "Vejo que selecionaste [nome do material]. Como posso ajudar com este conteúdo?"
- Se a pergunta do aluno for vaga (ex: "ajuda"), usa os materiais selecionados para oferecer ajuda específica sobre o conteúdo.
IMPORTANTE - RESPOSTAS COMPLETAS:
- NUNCA termines respostas com dois pontos (:).
- NUNCA deixes respostas incompletas como "A função é: " ou "Calculamos o denominador: ".
- SEMPRE completa as frases e fornece a resposta completa.
- Se precisares de explicar um cálculo, explica-o completamente com o resultado final.
- Se precisares de definir algo, fornece a definição completa.''',
});
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 4 para poupar heap)
// PASSO 3 — BUSCAR MEMÓRIA DA CONVERSA NA Cloud Firestore (máx 10 para manter contexto)
final conversationId = ChatMemoryService.currentConversationId;
var lastHistoryMessageIsDuplicate = false;
if (conversationId != null) {
final conversationHistory =
await ChatMemoryService.getConversationMessages(
conversationId: conversationId,
limit: 4,
limit: 10,
);
for (final msg in conversationHistory) {
messages.add({
@@ -923,6 +1036,17 @@ IMPORTANTE - RESPOSTAS COMPLETAS:
'content': msg['content'] as String,
});
}
// Verificar se a última mensagem do histórico já é a pergunta atual
// (evita duplicação quando a UI guardou a mensagem antes de chamar ask())
if (conversationHistory.isNotEmpty) {
final lastMsg = conversationHistory.last;
if (lastMsg['role'] == 'user' && lastMsg['content'] == userQuery) {
lastHistoryMessageIsDuplicate = true;
Logger.info(
'Last history message matches current query — skipping duplicate user message',
);
}
}
}
// Log de confirmação de ordem do histórico
@@ -934,8 +1058,13 @@ IMPORTANTE - RESPOSTAS COMPLETAS:
// Small talk: skip PDF lookup entirely and go straight to model
if (_isSmallTalk(userQuery)) {
Logger.info('Small talk detected — skipping PDF lookup');
messages.add({'role': 'user', 'content': userQuery});
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
if (!lastHistoryMessageIsDuplicate) {
messages.add({'role': 'user', 'content': userQuery});
}
// Only save to Firestore if not already saved by UI
if (!lastHistoryMessageIsDuplicate) {
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
}
final response = await _callOllamaAPIWithMessages(messages);
await ChatMemoryService.saveMessage(role: 'assistant', content: response);
return response;
@@ -963,8 +1092,13 @@ IMPORTANTE - RESPOSTAS COMPLETAS:
}
if (pdfContext.isEmpty) {
// Sem contexto encontrado — responder com base na personalidade mas sem inventar conteúdo
messages.add({'role': 'user', 'content': userQuery});
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
if (!lastHistoryMessageIsDuplicate) {
messages.add({'role': 'user', 'content': userQuery});
}
// Only save to Firestore if not already saved by UI
if (!lastHistoryMessageIsDuplicate) {
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
}
final response = await _callOllamaAPIWithMessages(messages);
await ChatMemoryService.saveMessage(role: 'assistant', content: response);
return response;
@@ -979,14 +1113,18 @@ $pdfContext
Pergunta: $userQuery'''
: userQuery;
messages.add({'role': 'user', 'content': userContent});
if (!lastHistoryMessageIsDuplicate) {
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);
// Only save to Firestore if not already saved by UI
if (!lastHistoryMessageIsDuplicate) {
await ChatMemoryService.saveMessage(role: 'user', content: userQuery);
}
// Call API
final response = await _callOllamaAPIWithMessages(messages);