From d6260747d12044d7e7d97ba3c12e22ad6e2d8e6d Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Wed, 27 May 2026 17:16:26 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20feat:=20melhoria=20da=20estabilidad?= =?UTF-8?q?e=20de=20=C3=A1udio,=20robustez=20de=20JSON,=20fallback=20Gemin?= =?UTF-8?q?i=20e=20inje=C3=A7=C3=A3o=20real=20de=20observa=C3=A7=C3=B5es?= =?UTF-8?q?=20no=20Chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 14 +++++- server.js | 124 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/public/app.js b/public/app.js index d302868..98a680e 100644 --- a/public/app.js +++ b/public/app.js @@ -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); diff --git a/server.js b/server.js index 57bd8f3..5a91c9f 100644 --- a/server.js +++ b/server.js @@ -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);