// sync-knowledge.js (ODONTO) — monta o nó de conhecimento da Secretária IA com // os dados reais da clínica e sincroniza com o motor NewWhats. // Adaptado do satélite mercado. Fluxo: // 1. Lê dados do banco (clínica, especialidades, dentistas, procedimentos, planos) // 2. Monta texto estruturado do knowledge node // 3. PUT/POST no motor (/api/secretaria/*) — rotas internas, sem auth de sessão import { getConfig } from './config.js' const money = (v) => v == null ? null : `R$ ${Number(v).toFixed(2).replace('.', ',')}` const DIAS = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'] async function motorFetch(motorUrl, method, path, body) { const res = await fetch(`${motorUrl}/api/secretaria${path}`, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }) if (!res.ok) throw new Error(`Motor ${method} /api/secretaria${path} → ${res.status}: ${(await res.text()).slice(0, 200)}`) return res.json() } export async function buildKnowledgeText(pool, clinicaId) { const lines = [] const q = (sql, params) => pool.query(sql, params).then((r) => r.rows) const [clinica] = await q( `SELECT nome_fantasia, telefone, cidade, estado, logradouro, numero, bairro FROM clinicas WHERE id = $1`, [clinicaId]) const nome = clinica?.nome_fantasia || 'Nossa clínica odontológica' lines.push(`=== ${nome.toUpperCase()} ===`) lines.push(`Clínica odontológica. Atendimento via WhatsApp.`) if (clinica?.telefone) lines.push(`Telefone: ${clinica.telefone}`) if (clinica?.logradouro) { const end = [clinica.logradouro, clinica.numero, clinica.bairro, clinica.cidade, clinica.estado].filter(Boolean).join(', ') lines.push(`Endereço: ${end}`) } lines.push('') // Especialidades const esp = await q( `SELECT nome, descricao FROM especialidades WHERE clinica_id = $1 AND ativo::int = 1 ORDER BY ordem NULLS LAST, nome`, [clinicaId]) if (esp.length) { lines.push(`=== ESPECIALIDADES ATENDIDAS ===`) for (const e of esp) lines.push(`• ${e.nome}${e.descricao ? ` — ${e.descricao}` : ''}`) lines.push('') } // Dentistas + horários const dentistas = await q( `SELECT id, nome, especialidade FROM dentistas WHERE clinica_id = $1 AND ativo::int = 1 ORDER BY ordem NULLS LAST, nome`, [clinicaId]) if (dentistas.length) { lines.push(`=== DENTISTAS ===`) for (const d of dentistas) { lines.push(`• ${d.nome}${d.especialidade ? ` (${d.especialidade})` : ''}`) const hr = await q( `SELECT dia_semana, hora_inicio, hora_fim FROM dentistas_horarios WHERE dentista_id = $1 AND ativo::int = 1 ORDER BY dia_semana, hora_inicio`, [d.id]) if (hr.length) { const fmt = hr.map((h) => `${DIAS[h.dia_semana] ?? h.dia_semana} ${String(h.hora_inicio).slice(0, 5)}-${String(h.hora_fim).slice(0, 5)}`) lines.push(` Atende: ${fmt.join(' | ')}`) } } lines.push('') } // Procedimentos + valores const procs = await q( `SELECT id, nome, categoria, valorparticular FROM procedimentos WHERE clinica_id = $1 AND ativo::int = 1 ORDER BY ordem NULLS LAST, nome`, [clinicaId]) if (procs.length) { lines.push(`=== PROCEDIMENTOS E VALORES ===`) for (const p of procs) { const part = p.valorparticular != null ? ` — particular: ${money(p.valorparticular)}` : '' lines.push(`• ${p.nome}${p.categoria ? ` [${p.categoria}]` : ''}${part}`) const vp = await q( `SELECT planonome, valor FROM procedimentos_valores_planos WHERE procedimentoid = $1`, [p.id]) const comValor = vp.filter((x) => x.valor != null) if (comValor.length) lines.push(` Por plano: ${comValor.map((x) => `${x.planonome} ${money(x.valor)}`).join(', ')}`) } lines.push('') } // Planos / convênios const planos = await q( `SELECT nome, tipo, descontopadrao FROM planos WHERE clinica_id = $1 ORDER BY nome`, [clinicaId]) if (planos.length) { lines.push(`=== PLANOS / CONVÊNIOS ACEITOS ===`) for (const pl of planos) { const desc = pl.descontopadrao ? ` — desconto padrão ${pl.descontopadrao}%` : '' lines.push(`• ${pl.nome}${pl.tipo ? ` (${pl.tipo})` : ''}${desc}`) } lines.push('') } lines.push(`=== ATENDIMENTO ===`) lines.push(`Para agendar, remarcar ou tirar dúvidas, fale pelo WhatsApp.`) lines.push(`Dúvidas sobre valores, convênios ou disponibilidade: informe que vai encaminhar para a equipe.`) return lines.join('\n') } export async function syncKnowledge(pool) { const cfg = getConfig() if (!cfg.motorUrl) throw new Error('NEWWHATS_URL não configurado') if (!cfg.clinicaId) throw new Error('NEWWHATS_CLINICA_ID não configurado') const knowledgeText = await buildKnowledgeText(pool, cfg.clinicaId) const agents = await motorFetch(cfg.motorUrl, 'GET', '/agents') if (!agents.length) throw new Error('Nenhum agente no motor. Crie um agente na Secretária IA.') const agent = agents.find((a) => a.active) || agents[0] const nodes = await motorFetch(cfg.motorUrl, 'GET', `/agents/${agent.id}/nodes`) const kn = nodes.find((n) => n.type === 'knowledge') let nodeId const title = 'Base de Conhecimento — Clínica Odontológica' if (kn) { await motorFetch(cfg.motorUrl, 'PUT', `/nodes/${kn.id}`, { title, content: knowledgeText, active: true, sort_order: kn.sort_order ?? 3, }) nodeId = kn.id } else { const created = await motorFetch(cfg.motorUrl, 'POST', `/agents/${agent.id}/nodes`, { type: 'knowledge', title, content: knowledgeText, sort_order: 3, }) nodeId = created.id } return { ok: true, agent_id: agent.id, agent: agent.name, node_id: nodeId, chars: knowledgeText.length, preview: knowledgeText, synced_at: new Date().toISOString(), } }