Melhorias no comportamento do chat com IA;
Adição do histórico de conversas com IA.
This commit is contained in:
@@ -360,4 +360,114 @@ class ChatMemoryService {
|
||||
Logger.error('Error clearing history: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a smart title from the first message by extracting keywords
|
||||
static String generateConversationTitle(String firstMessage) {
|
||||
// Remove common Portuguese stopwords and greetings
|
||||
final stopwords = {
|
||||
'olá',
|
||||
'oi',
|
||||
'bom dia',
|
||||
'boa tarde',
|
||||
'boa noite',
|
||||
'consegues',
|
||||
'podes',
|
||||
'pode',
|
||||
'poderia',
|
||||
'poderias',
|
||||
'explicar',
|
||||
'explica',
|
||||
'explicar-me',
|
||||
'explicar-lhe',
|
||||
'sobre',
|
||||
'acerca de',
|
||||
'a respeito de',
|
||||
'relativamente a',
|
||||
'o que é',
|
||||
'o que são',
|
||||
'qual é',
|
||||
'quais são',
|
||||
'como',
|
||||
'onde',
|
||||
'quando',
|
||||
'porquê',
|
||||
'por que',
|
||||
'quero',
|
||||
'quero que',
|
||||
'gostaria',
|
||||
'gostaria de',
|
||||
'preciso',
|
||||
'preciso de',
|
||||
'ajuda',
|
||||
'ajuda-me',
|
||||
'me',
|
||||
'te',
|
||||
'lhe',
|
||||
'nos',
|
||||
'vos',
|
||||
'um',
|
||||
'uma',
|
||||
'uns',
|
||||
'umas',
|
||||
'e',
|
||||
'ou',
|
||||
'mas',
|
||||
'porém',
|
||||
'todavia',
|
||||
'para',
|
||||
'de',
|
||||
'em',
|
||||
'a',
|
||||
'o',
|
||||
'as',
|
||||
'os',
|
||||
'que',
|
||||
'quem',
|
||||
'qual',
|
||||
'quais',
|
||||
'é',
|
||||
'são',
|
||||
'está',
|
||||
'estão',
|
||||
'foi',
|
||||
'foram',
|
||||
'?',
|
||||
'!',
|
||||
'.',
|
||||
',',
|
||||
';',
|
||||
':',
|
||||
};
|
||||
|
||||
// Convert to lowercase and remove punctuation
|
||||
final cleaned = firstMessage
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(r'[?!.;,]'), '')
|
||||
.trim();
|
||||
|
||||
// Split into words and remove stopwords
|
||||
final words = cleaned
|
||||
.split(' ')
|
||||
.where((word) => word.isNotEmpty && !stopwords.contains(word))
|
||||
.toList();
|
||||
|
||||
// Take the first 2-3 meaningful words as the title
|
||||
if (words.isEmpty) {
|
||||
return firstMessage.length > 30
|
||||
? '${firstMessage.substring(0, 30)}...'
|
||||
: firstMessage;
|
||||
}
|
||||
|
||||
final titleWords = words.take(3).join(' ');
|
||||
// Capitalize first letter of each word
|
||||
final title = titleWords
|
||||
.split(' ')
|
||||
.map((word) {
|
||||
if (word.isEmpty) return '';
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return title.length > 40 ? '${title.substring(0, 40)}...' : title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user