Refactoring: Migrado para Supabase Postgres
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user