⚡ 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
|
// Solicita permissão e inicia gravação
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
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) => {
|
mediaRecorder.ondataavailable = (e) => {
|
||||||
if (e.data.size > 0) audioChunks.push(e.data);
|
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;
|
const { message } = req.body;
|
||||||
if (!message) return res.status(400).json({ error: 'Mensagem é obrigatória' });
|
if (!message) return res.status(400).json({ error: 'Mensagem é obrigatória' });
|
||||||
|
|
||||||
// Procura por padrão [KNOWLEDGE: ...] na mensagem
|
// Procura por padrão [KNOWLEDGE: ...] na mensagem (case-insensitive)
|
||||||
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/g;
|
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/gi;
|
||||||
const facts = [];
|
const facts = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = knowledgeRegex.exec(message)) !== null) {
|
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' });
|
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 {
|
try {
|
||||||
// Salva arquivo temporário a partir do buffer
|
// Salva arquivo temporário a partir do buffer
|
||||||
@@ -303,18 +310,23 @@ Retorne APENAS o JSON válido:`;
|
|||||||
// Parse JSON da resposta
|
// Parse JSON da resposta
|
||||||
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
|
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
|
||||||
try {
|
try {
|
||||||
// Limpa markdown code blocks se houver
|
const firstBrace = rawContent.indexOf('{');
|
||||||
const cleanJson = rawContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
const lastBrace = rawContent.lastIndexOf('}');
|
||||||
const parsed = JSON.parse(cleanJson);
|
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||||
parsedData = {
|
const jsonString = rawContent.substring(firstBrace, lastBrace + 1);
|
||||||
criancas: parsed.criancas || null,
|
const parsed = JSON.parse(jsonString);
|
||||||
turma: parsed.turma || 'não informada',
|
parsedData = {
|
||||||
tags: parsed.tags || [],
|
criancas: parsed.criancas || null,
|
||||||
relatorio: parsed.relatorio || rawContent
|
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) {
|
} catch (parseErr) {
|
||||||
// Se não conseguir parsear JSON, retorna o texto como relatório
|
// 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;
|
parsedData.relatorio = rawContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1393,11 +1405,72 @@ app.post('/api/chat', requireAuth, async (req, res) => {
|
|||||||
|
|
||||||
const knowledgeText = getKnowledgeAsText();
|
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 = {
|
const dynamicSystemPrompt = {
|
||||||
role: "system",
|
role: "system",
|
||||||
content: KEMILY_SYSTEM_PROMPT.content
|
content: KEMILY_SYSTEM_PROMPT.content
|
||||||
.replace(/Você é a Kemily/g, `Você é a ${agentName}`)
|
.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];
|
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) {
|
if (!chatResponse || !chatResponse.ok) {
|
||||||
const errorText = chatResponse ? await chatResponse.text() : 'Sem conexão com os servidores de IA';
|
const errorText = chatResponse ? await chatResponse.text() : 'Sem conexão com os servidores de IA';
|
||||||
console.error(`Erro na API de IA (${providerUsed}):`, errorText);
|
console.error(`Erro na API de IA (${providerUsed}):`, errorText);
|
||||||
|
|||||||
Reference in New Issue
Block a user