⚡ feat: melhoria da estabilidade de áudio, robustez de JSON, fallback Gemini e injeção real de observações no Chat
This commit is contained in:
+13
-1
@@ -2014,7 +2014,19 @@ const initApp = () => {
|
||||
// Solicita permissão e inicia gravação
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
|
||||
let options = {};
|
||||
if (typeof MediaRecorder.isTypeSupported === 'function') {
|
||||
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
|
||||
options = { mimeType: 'audio/webm;codecs=opus' };
|
||||
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
|
||||
options = { mimeType: 'audio/webm' };
|
||||
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
|
||||
options = { mimeType: 'audio/ogg' };
|
||||
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
||||
options = { mimeType: 'audio/mp4' };
|
||||
}
|
||||
}
|
||||
mediaRecorder = new MediaRecorder(stream, options);
|
||||
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
if (e.data.size > 0) audioChunks.push(e.data);
|
||||
|
||||
@@ -168,8 +168,8 @@ app.post('/api/conhecimento/extrair', requireAuth, (req, res) => {
|
||||
const { message } = req.body;
|
||||
if (!message) return res.status(400).json({ error: 'Mensagem é obrigatória' });
|
||||
|
||||
// Procura por padrão [KNOWLEDGE: ...] na mensagem
|
||||
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/g;
|
||||
// Procura por padrão [KNOWLEDGE: ...] na mensagem (case-insensitive)
|
||||
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/gi;
|
||||
const facts = [];
|
||||
let match;
|
||||
while ((match = knowledgeRegex.exec(message)) !== null) {
|
||||
@@ -217,7 +217,14 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio'
|
||||
return res.status(400).json({ error: 'Nenhum áudio recebido' });
|
||||
}
|
||||
|
||||
const tempPath = `/tmp/obs_${Date.now()}.webm`;
|
||||
let ext = 'webm';
|
||||
if (req.file.mimetype) {
|
||||
if (req.file.mimetype.includes('mp4')) ext = 'mp4';
|
||||
else if (req.file.mimetype.includes('ogg')) ext = 'ogg';
|
||||
else if (req.file.mimetype.includes('wav')) ext = 'wav';
|
||||
else if (req.file.mimetype.includes('mpeg')) ext = 'mp3';
|
||||
}
|
||||
const tempPath = `/tmp/obs_${Date.now()}.${ext}`;
|
||||
|
||||
try {
|
||||
// Salva arquivo temporário a partir do buffer
|
||||
@@ -303,18 +310,23 @@ Retorne APENAS o JSON válido:`;
|
||||
// Parse JSON da resposta
|
||||
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
|
||||
try {
|
||||
// Limpa markdown code blocks se houver
|
||||
const cleanJson = rawContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
parsedData = {
|
||||
criancas: parsed.criancas || null,
|
||||
turma: parsed.turma || 'não informada',
|
||||
tags: parsed.tags || [],
|
||||
relatorio: parsed.relatorio || rawContent
|
||||
};
|
||||
const firstBrace = rawContent.indexOf('{');
|
||||
const lastBrace = rawContent.lastIndexOf('}');
|
||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||
const jsonString = rawContent.substring(firstBrace, lastBrace + 1);
|
||||
const parsed = JSON.parse(jsonString);
|
||||
parsedData = {
|
||||
criancas: parsed.criancas || null,
|
||||
turma: parsed.turma || 'não informada',
|
||||
tags: parsed.tags || [],
|
||||
relatorio: parsed.relatorio || rawContent
|
||||
};
|
||||
} else {
|
||||
throw new Error('Nenhum JSON encontrado na resposta da IA');
|
||||
}
|
||||
} catch (parseErr) {
|
||||
// Se não conseguir parsear JSON, retorna o texto como relatório
|
||||
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto');
|
||||
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto:', parseErr.message);
|
||||
parsedData.relatorio = rawContent;
|
||||
}
|
||||
|
||||
@@ -1393,11 +1405,72 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
|
||||
const knowledgeText = getKnowledgeAsText();
|
||||
|
||||
// 1.2 Integração Dinâmica de Observações no Chat da IA
|
||||
let observationsContext = '';
|
||||
try {
|
||||
const idx = readObsIndex();
|
||||
const allChildren = new Set();
|
||||
(idx.observations || []).forEach(o => {
|
||||
if (o.criancas) {
|
||||
o.criancas.forEach(c => allChildren.add(c.trim()));
|
||||
}
|
||||
});
|
||||
|
||||
const mentionedChildren = [];
|
||||
const promptLower = promptText.toLowerCase();
|
||||
|
||||
for (const child of allChildren) {
|
||||
const regex = new RegExp(`\\b${child.toLowerCase()}\\b`, 'i');
|
||||
if (regex.test(promptLower)) {
|
||||
mentionedChildren.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedChildren.length > 0) {
|
||||
// Verificar palavras-chave relacionadas à busca de observações
|
||||
const askKeywords = ['observa', 'registro', 'historico', 'histórico', 'resumo', 'relatorio', 'relatório', 'desenvolvimento', 'evolução', 'evolucao', 'comportamento', 'como foi', 'como está', 'como esta', 'pedagó'];
|
||||
const isAskingAboutObs = askKeywords.some(kw => promptLower.includes(kw));
|
||||
|
||||
if (isAskingAboutObs) {
|
||||
let compiledObservations = [];
|
||||
for (const child of mentionedChildren) {
|
||||
const childObs = (idx.observations || []).filter(o =>
|
||||
o.criancas && o.criancas.some(c => c.toLowerCase() === child.toLowerCase())
|
||||
);
|
||||
|
||||
if (childObs.length > 0) {
|
||||
// Carrega as últimas 5 observações
|
||||
const lastObs = childObs.slice(0, 5);
|
||||
const obsTexts = [];
|
||||
for (const o of lastObs) {
|
||||
const filePath = path.join(OBSERVACOES_DIR, o.date?.slice(0, 7), o.filename);
|
||||
if (fs.existsSync(filePath)) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
content = content.replace(/^---[\s\S]*?---\n/, '').trim();
|
||||
obsTexts.push(`- Data: ${o.date} (${o.time}):\n${content}`);
|
||||
}
|
||||
}
|
||||
if (obsTexts.length > 0) {
|
||||
compiledObservations.push(`### Observações de ${child}:\n${obsTexts.join('\n\n')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (compiledObservations.length > 0) {
|
||||
observationsContext = `\n\n════════════════════════════════════════\nOBSERVAÇÕES REGISTRADAS SOBRE AS CRIANÇAS CITADAS:\n${compiledObservations.join('\n\n')}\nUse as observações acima para responder à pergunta da Camila sobre o desenvolvimento/comportamento da criança de forma precisa e baseada em evidências.\n════════════════════════════════════════\n`;
|
||||
console.log(`[Smart Router] Injetando contexto de observações para:`, mentionedChildren);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (obsErr) {
|
||||
console.error('Erro ao integrar observações no prompt da IA:', obsErr);
|
||||
}
|
||||
|
||||
const dynamicSystemPrompt = {
|
||||
role: "system",
|
||||
content: KEMILY_SYSTEM_PROMPT.content
|
||||
.replace(/Você é a Kemily/g, `Você é a ${agentName}`)
|
||||
.replace('INFORMAÇÕES PESSOAIS:', `${knowledgeText}INFORMAÇÕES PESSOAIS:`)
|
||||
.replace('INFORMAÇÕES PESSOAIS:', `${knowledgeText}${observationsContext}INFORMAÇÕES PESSOAIS:`)
|
||||
};
|
||||
|
||||
const enrichedMessages = [dynamicSystemPrompt, ...messages];
|
||||
@@ -1449,6 +1522,29 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Se o OpenRouter e Groq falharem ou retornarem erro, caímos no fallback do Google Gemini
|
||||
if ((!chatResponse || !chatResponse.ok) && process.env.GOOGLE_AI_API_KEY) {
|
||||
console.log('[Smart Router] Usando Google Gemini como fallback de streaming de texto...');
|
||||
providerUsed = 'Google Gemini';
|
||||
try {
|
||||
chatResponse = await fetch('https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.GOOGLE_AI_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash',
|
||||
messages: enrichedMessages,
|
||||
stream: true,
|
||||
temperature
|
||||
})
|
||||
});
|
||||
} catch (geminiErr) {
|
||||
console.error('[Smart Router] Erro crítico: Falha no fallback do Google Gemini também:', geminiErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!chatResponse || !chatResponse.ok) {
|
||||
const errorText = chatResponse ? await chatResponse.text() : 'Sem conexão com os servidores de IA';
|
||||
console.error(`Erro na API de IA (${providerUsed}):`, errorText);
|
||||
|
||||
Reference in New Issue
Block a user