feat(secretaria): RAG sem pgvector no conhecimento (embeddings + cosseno)

Adiciona busca semântica na Base de Conhecimento da secretária:
- embeddings.ts: chunking, embed (OpenAI text-embedding-3-small ou Gemini
  text-embedding-004) e similaridade cosseno — tudo na aplicação, sem pgvector.
- tabela sec_knowledge_chunks (vetor em JSON/text) com índices.
- brain.ts: no nó 'knowledge', injeta só os top-4 trechos relevantes à pergunta
  do usuário; indexação lazy por hash do conteúdo. Fallback para o conteúdo
  inteiro se não houver chave de embedding ou em qualquer falha (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 07:08:42 +02:00
parent 36405a46f0
commit 31bf45faf5
6 changed files with 368 additions and 7 deletions
@@ -147,6 +147,24 @@ export async function runMigrations(db: Knex): Promise<void> {
})
}
// ── sec_knowledge_chunks (RAG sem pgvector) ───────────────────────────────
// Chunks de conhecimento + embedding (JSON em text). Busca por similaridade
// roda na aplicação (cosseno), então não exige a extensão pgvector.
if (!(await db.schema.hasTable('sec_knowledge_chunks'))) {
await db.schema.createTable('sec_knowledge_chunks', (t) => {
t.uuid('id').primary().defaultTo(db.raw('gen_random_uuid()'))
t.uuid('agent_id').notNullable().references('id').inTable('sec_agents').onDelete('CASCADE')
t.uuid('node_id').notNullable()
t.string('content_hash', 40).notNullable() // detecta quando reindexar
t.integer('chunk_index').notNullable().defaultTo(0)
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_chunks_agent ON sec_knowledge_chunks (agent_id)')
await db.raw('CREATE INDEX IF NOT EXISTS idx_sec_chunks_node ON sec_knowledge_chunks (node_id)')
}
// ── 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) {