Refactoring: Migrado para Supabase Postgres

This commit is contained in:
2026-05-27 22:54:12 +00:00
parent 24157e8906
commit 662e81b421
144 changed files with 13806 additions and 160 deletions
+323 -158
View File
@@ -33,6 +33,10 @@ const requireAuth = (req, res, next) => {
};
const fs = require('fs');
const { Pool } = require('pg');
const dbPool = new Pool({
connectionString: process.env.DATABASE_URL
});
const CONFIG_FILE_PATH = path.join(__dirname, 'agent_config.json');
const KNOWLEDGE_FILE_PATH = path.join(__dirname, 'conhecimento_adquirido.json');
@@ -42,36 +46,49 @@ const TEMPERATURE_PRESETS = {
detalhista: { value: 1.0, label: 'Detalhista e Falante', description: 'Respostas extensas, explicações completas,contextualização máxima. Ideal para estudo e revisão.' }
};
let agentConfigCache = {
agentName: "Kemily",
kemilyAvatarUrl: "assets/kemily.png",
camilaAvatarUrl: "assets/camila_prof.png",
temperaturePreset: "equilibrado"
};
function readAgentConfig() {
try {
if (fs.existsSync(CONFIG_FILE_PATH)) {
return JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, 'utf8'));
}
} catch (e) {
console.error('Erro ao ler agent_config.json:', e);
}
return {
agentName: "Kemily",
kemilyAvatarUrl: "assets/kemily.png",
camilaAvatarUrl: "assets/camila_prof.png"
};
return agentConfigCache;
}
function writeAgentConfig(config) {
async function writeAgentConfig(config) {
agentConfigCache = {
agentName: config.agentName,
kemilyAvatarUrl: config.kemilyAvatarUrl,
camilaAvatarUrl: config.camilaAvatarUrl,
temperaturePreset: config.temperaturePreset || 'equilibrado'
};
try {
await dbPool.query(
`INSERT INTO escola.config (key, agent_name, kemily_avatar_url, camila_avatar_url, temperature_preset, updated_at)
VALUES ('current', $1, $2, $3, $4, NOW())
ON CONFLICT (key) DO UPDATE SET
agent_name = EXCLUDED.agent_name,
kemily_avatar_url = EXCLUDED.kemily_avatar_url,
camila_avatar_url = EXCLUDED.camila_avatar_url,
temperature_preset = EXCLUDED.temperature_preset,
updated_at = NOW();`,
[config.agentName, config.kemilyAvatarUrl, config.camilaAvatarUrl, config.temperaturePreset || 'equilibrado']
);
fs.writeFileSync(CONFIG_FILE_PATH, JSON.stringify(config, null, 2), 'utf8');
// Copiar também para o BotVPS para sincronização com o Hermes
const botConfigDir = path.join(__dirname, '..', 'BotVPS', 'data');
if (fs.existsSync(botConfigDir)) {
fs.writeFileSync(path.join(botConfigDir, 'agent_config.json'), JSON.stringify(config, null, 2), 'utf8');
}
} catch (e) {
console.error('Erro ao salvar agent_config.json:', e);
console.error('Erro ao salvar agent_config no banco:', e);
}
}
// Leitura/escrita do conhecimento adquirido
function readConhecimento() {
// Mantido apenas para compatibilidade de migração na inicialização
try {
if (fs.existsSync(KNOWLEDGE_FILE_PATH)) {
return JSON.parse(fs.readFileSync(KNOWLEDGE_FILE_PATH, 'utf8'));
@@ -82,46 +99,45 @@ function readConhecimento() {
return { facts: [], lastUpdated: null };
}
function writeConhecimento(data) {
async function addKnowledgeFact(fact, source = 'conversa') {
try {
data.lastUpdated = new Date().toISOString();
fs.writeFileSync(KNOWLEDGE_FILE_PATH, JSON.stringify(data, null, 2), 'utf8');
} catch (e) {
console.error('Erro ao salvar conhecimento_adquirido.json:', e);
const res = await dbPool.query("SELECT content FROM escola.conhecimento;");
const exists = res.rows.some(f =>
f.content.toLowerCase().includes(fact.toLowerCase()) ||
fact.toLowerCase().includes(f.content.toLowerCase())
);
if (!exists) {
await dbPool.query(
"INSERT INTO escola.conhecimento (content, source, added_at) VALUES ($1, $2, NOW());",
[fact, source]
);
}
return !exists;
} catch (err) {
console.error('Erro ao adicionar fato no banco:', err);
return false;
}
}
function addKnowledgeFact(fact, source = 'conversa') {
const knowledge = readConhecimento();
// Evitar duplicatas por similaridade simples
const exists = knowledge.facts.some(f =>
f.content.toLowerCase().includes(fact.toLowerCase()) ||
fact.toLowerCase().includes(f.content.toLowerCase())
);
if (!exists) {
knowledge.facts.push({
id: Date.now(),
content: fact,
source,
addedAt: new Date().toISOString()
});
writeConhecimento(knowledge);
async function removeKnowledgeFact(id) {
try {
await dbPool.query("DELETE FROM escola.conhecimento WHERE id = $1;", [id]);
} catch (err) {
console.error('Erro ao remover fato do banco:', err);
}
return !exists;
}
function removeKnowledgeFact(id) {
const knowledge = readConhecimento();
knowledge.facts = knowledge.facts.filter(f => f.id !== id);
writeConhecimento(knowledge);
}
function getKnowledgeAsText() {
const knowledge = readConhecimento();
if (!knowledge.facts.length) return '';
return '\n\n════════════════════════════════════════\nCONHECIMENTO ADQUIRIDO (Aprendido com a Camila):\n════════════════════════════════════════\n' +
knowledge.facts.map(f => `[${f.source}] ${f.content}`).join('\n') +
'\n════════════════════════════════════════\n';
async function getKnowledgeAsText() {
try {
const res = await dbPool.query("SELECT content, source FROM escola.conhecimento ORDER BY added_at DESC;");
if (!res.rows.length) return '';
return '\n\n════════════════════════════════════════\nCONHECIMENTO ADQUIRIDO (Aprendido com a Camila):\n════════════════════════════════════════\n' +
res.rows.map(f => `[${f.source}] ${f.content}`).join('\n') +
'\n════════════════════════════════════════\n';
} catch (err) {
console.error('Erro ao ler conhecimento do banco:', err);
return '';
}
}
// Rota GET para obter configurações do agente
@@ -131,7 +147,7 @@ app.get('/api/agent-config', requireAuth, (req, res) => {
});
// Rota POST para atualizar configurações do agente
app.post('/api/agent-config', requireAuth, (req, res) => {
app.post('/api/agent-config', requireAuth, async (req, res) => {
const { agentName, kemilyAvatarUrl, camilaAvatarUrl, temperaturePreset } = req.body;
if (!agentName) {
return res.status(400).json({ error: 'Nome do agente é obrigatório' });
@@ -142,40 +158,45 @@ app.post('/api/agent-config', requireAuth, (req, res) => {
camilaAvatarUrl: camilaAvatarUrl || "assets/camila_prof.png",
temperaturePreset: temperaturePreset || 'equilibrado'
};
writeAgentConfig(config);
await writeAgentConfig(config);
res.json({ success: true, config });
});
// Rotas de conhecimento adquirido
app.get('/api/conhecimento', requireAuth, (req, res) => {
res.json(readConhecimento());
app.get('/api/conhecimento', requireAuth, async (req, res) => {
try {
const dbRes = await dbPool.query("SELECT id, content, source, added_at as \"addedAt\" FROM escola.conhecimento ORDER BY added_at DESC;");
res.json({ facts: dbRes.rows });
} catch (err) {
console.error('Erro ao listar conhecimento:', err);
res.status(500).json({ error: err.message });
}
});
app.post('/api/conhecimento', requireAuth, (req, res) => {
app.post('/api/conhecimento', requireAuth, async (req, res) => {
const { fact, source } = req.body;
if (!fact) return res.status(400).json({ error: 'Fato é obrigatório' });
const added = addKnowledgeFact(fact, source || 'conversa');
const added = await addKnowledgeFact(fact, source || 'conversa');
res.json({ success: true, added });
});
app.delete('/api/conhecimento/:id', requireAuth, (req, res) => {
removeKnowledgeFact(parseInt(req.params.id));
app.delete('/api/conhecimento/:id', requireAuth, async (req, res) => {
await removeKnowledgeFact(parseInt(req.params.id));
res.json({ success: true });
});
// Analisar mensagem final e extrair conhecimento aprendido (chamado pelo frontend)
app.post('/api/conhecimento/extrair', requireAuth, (req, res) => {
app.post('/api/conhecimento/extrair', requireAuth, async (req, res) => {
const { message } = req.body;
if (!message) return res.status(400).json({ error: 'Mensagem é obrigatória' });
// Procura por padrão [KNOWLEDGE: ...] na mensagem (case-insensitive)
const knowledgeRegex = /\[KNOWLEDGE:\s*([^\]]+)\]/gi;
const facts = [];
let match;
while ((match = knowledgeRegex.exec(message)) !== null) {
const fact = match[1].trim();
if (fact && fact.length > 3) {
const added = addKnowledgeFact(fact, 'conversa');
const added = await addKnowledgeFact(fact, 'conversa');
if (added) facts.push(fact);
}
}
@@ -198,7 +219,7 @@ if (!fs.existsSync(OBSERVACOES_DIR)) {
fs.mkdirSync(OBSERVACOES_DIR, { recursive: true });
}
// Lê índice de observações
// Lê índice de observações (mantido apenas para fins de migração inicial)
function readObsIndex() {
if (!fs.existsSync(OBS_INDEX_FILE)) return { observations: [] };
try {
@@ -206,9 +227,119 @@ function readObsIndex() {
} catch { return { observations: [] }; }
}
// Salva índice de observações
function writeObsIndex(idx) {
fs.writeFileSync(OBS_INDEX_FILE, JSON.stringify(idx, null, 2), 'utf8');
try {
fs.writeFileSync(OBS_INDEX_FILE, JSON.stringify(idx, null, 2), 'utf8');
} catch (err) {
console.error('Erro ao escrever index local:', err);
}
}
// Inicializar banco de dados e migrar dados se estiver vazio
async function initDatabase() {
try {
// 1. Sincronizar cache de configurações do agente ou migrar
const configRes = await dbPool.query("SELECT agent_name, kemily_avatar_url, camila_avatar_url, temperature_preset FROM escola.config WHERE key = 'current';");
if (configRes.rows.length > 0) {
agentConfigCache = {
agentName: configRes.rows[0].agent_name,
kemilyAvatarUrl: configRes.rows[0].kemily_avatar_url,
camilaAvatarUrl: configRes.rows[0].camila_avatar_url,
temperaturePreset: configRes.rows[0].temperature_preset
};
console.log('[Database Init] Configurações do agente carregadas do banco de dados:', agentConfigCache.agentName);
} else {
console.log('[Database Init] Migrando configurações do agente do arquivo local...');
let localConfig = {
agentName: "Kemily",
kemilyAvatarUrl: "assets/kemily.png",
camilaAvatarUrl: "assets/camila_prof.png",
temperaturePreset: "equilibrado"
};
try {
if (fs.existsSync(CONFIG_FILE_PATH)) {
localConfig = JSON.parse(fs.readFileSync(CONFIG_FILE_PATH, 'utf8'));
}
} catch (err) {
console.error('[Database Init] Erro ao ler agent_config.json local:', err.message);
}
agentConfigCache = localConfig;
await dbPool.query(
"INSERT INTO escola.config (key, agent_name, kemily_avatar_url, camila_avatar_url, temperature_preset) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (key) DO NOTHING;",
['current', localConfig.agentName, localConfig.kemilyAvatarUrl, localConfig.camilaAvatarUrl, localConfig.temperaturePreset || 'equilibrado']
);
}
// 2. Verificar/Migrar fatos de conhecimento
const knowledgeRes = await dbPool.query("SELECT COUNT(*) FROM escola.conhecimento;");
if (parseInt(knowledgeRes.rows[0].count) === 0) {
console.log('[Database Init] Migrando conhecimento do arquivo local...');
let localKnowledge = { facts: [], lastUpdated: null };
try {
if (fs.existsSync(KNOWLEDGE_FILE_PATH)) {
localKnowledge = JSON.parse(fs.readFileSync(KNOWLEDGE_FILE_PATH, 'utf8'));
}
} catch (err) {
console.error('[Database Init] Erro ao ler conhecimento_adquirido.json local:', err.message);
}
for (const fact of localKnowledge.facts || []) {
await dbPool.query(
"INSERT INTO escola.conhecimento (content, source, added_at) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;",
[fact.content, fact.source, fact.addedAt]
);
}
}
// 3. Verificar/Migrar observações pedagógicas
const obsRes = await dbPool.query("SELECT COUNT(*) FROM escola.observacoes;");
if (parseInt(obsRes.rows[0].count) === 0) {
console.log('[Database Init] Migrando observações do arquivo local...');
let localIndex = { observations: [] };
try {
if (fs.existsSync(OBS_INDEX_FILE)) {
localIndex = JSON.parse(fs.readFileSync(OBS_INDEX_FILE, 'utf8'));
}
} catch (err) {
console.error('[Database Init] Erro ao ler index.json local:', err.message);
}
for (const obs of localIndex.observations || []) {
let reportContent = obs.preview;
try {
const yearMonth = obs.date.slice(0, 7);
const filePath = path.join(OBSERVACOES_DIR, yearMonth, obs.filename);
if (fs.existsSync(filePath)) {
const rawFile = fs.readFileSync(filePath, 'utf8');
const parts = rawFile.split('---\n');
if (parts.length >= 3) {
reportContent = parts.slice(2).join('---\n').trim();
} else {
reportContent = rawFile;
}
}
} catch (fileErr) {
console.error('[Database Init] Erro ao ler arquivo de relatório:', obs.filename, fileErr.message);
}
await dbPool.query(
"INSERT INTO escola.observacoes (id, filename, date, time, timestamp, criancas, turma, tags, report) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO NOTHING;",
[
obs.id,
obs.filename,
obs.date,
obs.time,
obs.timestamp,
JSON.stringify(obs.criancas),
obs.turma,
JSON.stringify(obs.tags),
reportContent
]
);
}
}
console.log('[Database Init] Inicialização e migração do banco de dados concluídas com sucesso!');
} catch (err) {
console.error('[Database Init] Erro crítico ao inicializar banco de dados:', err);
}
}
// Rota: POST /api/observacao/transcrever — recebe áudio, transcreve via Whisper, gera relatório
@@ -453,7 +584,7 @@ Retorne APENAS o JSON válido:`;
}
} 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:', parseErr.message);
console.warn('Não foi possível parsear JSON da resposta da IA:', parseErr.message);
parsedData.relatorio = rawContent;
}
@@ -470,7 +601,7 @@ Retorne APENAS o JSON válido:`;
});
// Rota: POST /api/observacao/salvar — salva relatório em .md
app.post('/api/observacao/salvar', requireAuth, (req, res) => {
app.post('/api/observacao/salvar', requireAuth, async (req, res) => {
const { report, criancas, turma, tags } = req.body;
if (!report) return res.status(400).json({ error: 'Relatório vazio' });
@@ -479,129 +610,163 @@ app.post('/api/observacao/salvar', requireAuth, (req, res) => {
const dateStr = now.toISOString().split('T')[0];
const timeStr = now.toTimeString().slice(0, 5).replace(':', 'h') + 'min';
const timestamp = now.toISOString();
const id = Date.now();
const filename = `${dateStr}_${timeStr}.md`;
// Salvar no Banco (Supabase)
try {
await dbPool.query(
"INSERT INTO escola.observacoes (id, filename, date, time, timestamp, criancas, turma, tags, report) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);",
[id, filename, dateStr, timeStr, timestamp, JSON.stringify(criancas), turma || 'não informada', JSON.stringify(tags), report]
);
} catch (dbErr) {
console.error('Erro ao salvar observação no banco:', dbErr);
}
// Monta markdown com frontmatter
const mdContent = `---\ndata: "${dateStr}"\nhora: "${timeStr}"\ntimestamp: "${timestamp}"\ncriancas: ${JSON.stringify(criancas)}\nturma: "${turma || 'não informada'}"\ntags: ${JSON.stringify(tags)}\n---\n\n${report}
`;
const filename = `${dateStr}_${timeStr}.md`;
const mdContent = `---\ndata: "${dateStr}"\nhora: "${timeStr}"\ntimestamp: "${timestamp}"\ncriancas: ${JSON.stringify(criancas)}\nturma: "${turma || 'não informada'}"\ntags: ${JSON.stringify(tags)}\n---\n\n${report}\n`;
const monthDir = path.join(OBSERVACOES_DIR, yearMonth);
if (!fs.existsSync(monthDir)) fs.mkdirSync(monthDir, { recursive: true });
const filePath = path.join(monthDir, filename);
fs.writeFileSync(filePath, mdContent, 'utf8');
// Atualiza índice com metadados estruturados
const idx = readObsIndex();
idx.observations.unshift({
id: Date.now(),
filename,
date: dateStr,
time: timeStr,
timestamp,
criancas: criancas || null,
turma: turma || 'não informada',
tags: tags || [],
preview: report.slice(0, 120) + '...'
});
writeObsIndex(idx);
// Também mantemos a escrita no index.json local para fins de compatibilidade
try {
const idx = readObsIndex();
idx.observations.unshift({
id,
filename,
date: dateStr,
time: timeStr,
timestamp,
criancas: criancas || null,
turma: turma || 'não informada',
tags: tags || [],
preview: report.slice(0, 120) + '...'
});
writeObsIndex(idx);
} catch (idxErr) {
console.error('Erro ao salvar índice local:', idxErr);
}
res.json({ success: true, filename, path: filePath });
res.json({ success: true, filename, id });
});
// Rota: GET /api/observacoes — lista todas as observações (índice)
app.get('/api/observacoes', requireAuth, (req, res) => {
const idx = readObsIndex();
res.json(idx);
app.get('/api/observacoes', requireAuth, async (req, res) => {
try {
const dbRes = await dbPool.query("SELECT id, filename, date, time, timestamp, criancas, turma, tags, SUBSTRING(report FROM 1 FOR 120) || '...' as preview FROM escola.observacoes ORDER BY timestamp DESC;");
res.json({ observations: dbRes.rows });
} catch (err) {
console.error('Erro ao listar observações:', err);
res.status(500).json({ error: err.message });
}
});
// Rota: GET /api/observacoes/:yearMonth/:filename — busca relatório específico
app.get('/api/observacoes/:yearMonth/:filename', requireAuth, (req, res) => {
const filePath = path.join(OBSERVACOES_DIR, req.params.yearMonth, req.params.filename);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Não encontrado' });
res.sendFile(filePath);
app.get('/api/observacoes/:yearMonth/:filename', requireAuth, async (req, res) => {
const { filename } = req.params;
try {
const dbRes = await dbPool.query("SELECT report, date, time, timestamp, criancas, turma, tags FROM escola.observacoes WHERE filename = $1;", [filename]);
if (dbRes.rows.length === 0) {
return res.status(404).json({ error: 'Não encontrado' });
}
const obs = dbRes.rows[0];
const mdContent = `---\ndata: "${obs.date}"\nhora: "${obs.time}"\ntimestamp: "${obs.timestamp}"\ncriancas: ${JSON.stringify(obs.criancas)}\nturma: "${obs.turma}"\ntags: ${JSON.stringify(obs.tags)}\n---\n\n${obs.report}`;
res.setHeader('Content-Type', 'text/markdown');
res.send(mdContent);
} catch (err) {
console.error('Erro ao ler observação do banco:', err);
res.status(500).json({ error: err.message });
}
});
// Rota: GET /api/observacoes/buscar — busca observações por criança e/ou período
// Query: ?crianca=Nome&mes=YYYY-MM&tags=social,cognitivo&limit=20
app.get('/api/observacoes/buscar', requireAuth, (req, res) => {
app.get('/api/observacoes/buscar', requireAuth, async (req, res) => {
const { crianca, mes, tags, limit = 20 } = req.query;
const idx = readObsIndex();
let results = idx.observations || [];
try {
let sql = "SELECT id, filename, date, time, timestamp, criancas, turma, tags, report as \"fullContent\", SUBSTRING(report FROM 1 FOR 120) || '...' as preview FROM escola.observacoes WHERE 1=1";
const params = [];
let pCount = 1;
// Filtra por criança
if (crianca) {
const searchName = crianca.toLowerCase();
results = results.filter(o =>
o.criancas && o.criancas.some(c => c.toLowerCase().includes(searchName))
);
}
// Filtra por mês
if (mes) {
results = results.filter(o => o.date && o.date.startsWith(mes));
}
// Filtra por tags
if (tags) {
const tagList = tags.split(',').map(t => t.trim());
results = results.filter(o =>
o.tags && tagList.some(t => o.tags.includes(t))
);
}
// Limita resultados
results = results.slice(0, parseInt(limit));
// Carrega conteúdo completo de cada resultado
const enriched = results.map(o => {
const filePath = path.join(OBSERVACOES_DIR, o.date?.slice(0, 7), o.filename);
let fullContent = '';
if (fs.existsSync(filePath)) {
fullContent = fs.readFileSync(filePath, 'utf8');
if (crianca) {
sql += ` AND EXISTS (
SELECT 1 FROM jsonb_array_elements_text(criancas) AS c
WHERE LOWER(c) LIKE $${pCount}
)`;
params.push(`%${crianca.toLowerCase()}%`);
pCount++;
}
return { ...o, fullContent };
});
res.json({ observations: enriched, total: idx.observations?.length || 0 });
if (mes) {
sql += ` AND date LIKE $${pCount}`;
params.push(`${mes}%`);
pCount++;
}
if (tags) {
const tagList = tags.split(',').map(t => t.trim().toLowerCase());
sql += ` AND EXISTS (
SELECT 1 FROM jsonb_array_elements_text(tags) AS t
WHERE LOWER(t) = ANY($${pCount})
)`;
params.push(tagList);
pCount++;
}
sql += ` ORDER BY timestamp DESC LIMIT $${pCount}`;
params.push(parseInt(limit));
const dbRes = await dbPool.query(sql, params);
const totalRes = await dbPool.query("SELECT COUNT(*) FROM escola.observacoes;");
const totalCount = parseInt(totalRes.rows[0].count);
res.json({ observations: dbRes.rows, total: totalCount });
} catch (err) {
console.error('Erro ao buscar observações:', err);
res.status(500).json({ error: err.message });
}
});
// Rota: GET /api/observacoes/resumo — compila observações em resumo para documentos
// Query: ?crianca=Nome&mes=YYYY-MM
// Retorna resumo estruturado que a IA pode usar em HTPC/planejamentos
app.get('/api/observacoes/resumo', requireAuth, async (req, res) => {
const { crianca, mes } = req.query;
if (!crianca) return res.status(400).json({ error: 'Nome da criança é obrigatório' });
// Busca observações
const idx = readObsIndex();
let results = idx.observations || [];
const searchName = crianca.toLowerCase();
results = results.filter(o =>
o.criancas && o.criancas.some(c => c.toLowerCase().includes(searchName))
);
if (mes) results = results.filter(o => o.date?.startsWith(mes));
try {
let sql = `
SELECT date, time, report, tags, turma, SUBSTRING(report FROM 1 FOR 120) || '...' as preview
FROM escola.observacoes
WHERE EXISTS (
SELECT 1 FROM jsonb_array_elements_text(criancas) AS c
WHERE LOWER(c) LIKE $1
)
`;
const params = [`%${crianca.toLowerCase()}%`];
if (results.length === 0) {
return res.json({ resumo: null, total: 0, message: `Nenhuma observação encontrada para "${crianca}"${mes ? ` em ${mes}` : ''}.` });
}
// Carrega conteúdo completo
const observations = results.slice(0, 15).map(o => {
const filePath = path.join(OBSERVACOES_DIR, o.date?.slice(0, 7), o.filename);
let fullContent = '';
if (fs.existsSync(filePath)) {
fullContent = fs.readFileSync(filePath, 'utf8');
if (mes) {
sql += " AND date LIKE $2";
params.push(`${mes}%`);
}
return { ...o, fullContent };
});
// Compila textos
const texts = observations.map(o => o.fullContent.replace(/^---[\s\S]*?---\n/, '')).filter(Boolean);
const combined = texts.join('\n\n---\n\n');
sql += " ORDER BY timestamp DESC LIMIT 15";
// Pede para IA compilar resumo pedagógico
const compilePrompt = `Você é a Kemily, assistente pedagógica. Compilar um RESUMO PEDAGÓGICO consolidado a partir de várias observações de uma criança.
const dbRes = await dbPool.query(sql, params);
if (dbRes.rows.length === 0) {
return res.json({ resumo: null, total: 0, message: `Nenhuma observação encontrada para "${crianca}"${mes ? ` em ${mes}` : ''}.` });
}
const observations = dbRes.rows;
const texts = observations.map(o => o.report).filter(Boolean);
const combined = texts.join('\n\n---\n\n');
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "Kemily";
const compilePrompt = `Você é a ${agentName}, assistente pedagógica. Compilar um RESUMO PEDAGÓGICO consolidado a partir de várias observações de uma criança.
CRIANÇA: ${crianca}
NÚMERO DE OBSERVAÇÕES: ${observations.length}
@@ -621,7 +786,7 @@ GERE UM RESUMO ESTRUTURADO COM:
[Interações com outras crianças, gestão emocional, socialização]
## Aspectos Cognitivos e de Aprendizagem
[Curiosidade, resolução de problemas, atenção, concentração]
[Curiosidade, resolução de problemas, atenção, concentration]
## Aspectos Motores e de Linguagem
[Desenvolvimento motor, linguagem oral, comunicação]
@@ -630,16 +795,15 @@ GERE UM RESUMO ESTRUTURADO COM:
[Dificuldades, necessidades de suporte, áreas a desenvolver]
## Pontos Fortes
[Conquistas, habilidades desarrolladas, aspectos positivos]
[Conquistas, habilidades desenvolvidas, aspectos positivos]
## Sugestões para Planejamento
[Recomendações para próximos passos, atividades sugeridas, seguimento]
-use linguagem formal-pedagógica
-Máximo 300 palavras
-Sea objetivo e baseado nas evidências das observações`;
-Seja objetivo e baseado nas evidências das observações`;
try {
const compileRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
@@ -1774,4 +1938,5 @@ app.get('*', requireAuth, (req, res) => {
app.listen(PORT, '0.0.0.0', () => {
console.log(`Servidor rodando na porta ${PORT}`);
initDatabase();
});