feat(secretaria): memória de longo prazo por contato (a partir das conversas)

A secretária passa a "lembrar" do cliente entre conversas:
- tabela sec_contact_memory (fato + embedding JSON), chave = ext_chat_id/contact_name.
- na sumarização (a cada ~10 trocas) extrai fatos duradouros do cliente via LLM
  e salva com embedding + dedup por similaridade (>0.92).
- buildSystemPrompt injeta "MEMÓRIA DO CLIENTE" com os fatos mais relevantes à
  pergunta (cosseno); sem chave de embedding usa os mais recentes; tudo com
  fallback silencioso (não regride).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 08:05:42 +02:00
parent 31bf45faf5
commit 42a04efe14
4 changed files with 244 additions and 0 deletions
@@ -165,6 +165,21 @@ export async function runMigrations(db: Knex): Promise<void> {
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_chunks_node ON sec_knowledge_chunks (node_id)')
}
// ── sec_contact_memory (memória de longo prazo por contato) ───────────────
// Fatos duradouros do cliente extraídos das conversas, com embedding (JSON).
// Permite a secretária "lembrar" do contato entre conversas/protocolos.
if (!(await db.schema.hasTable('sec_contact_memory'))) {
await db.schema.createTable('sec_contact_memory', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.uuid('agent_id').notNullable().references('id').inTable('sec_agents').onDelete('CASCADE')
t.string('contact_key', 300).notNullable() // ext_chat_id ou contact_name
t.text('content').notNullable()
t.text('embedding').notNullable() // vetor serializado em JSON
t.timestamps(true, true)
})
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_mem_contact ON sec_contact_memory (agent_id, contact_key)')
}
// ── Seeds: agente padrão + nós + calendário ───────────────────────────────
const agentCount = await db('sec_agents').count('id as c').first()
if (Number(agentCount?.c ?? 0) === 0) {