31bf45faf5
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>
107 lines
3.7 KiB
JavaScript
107 lines
3.7 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.chunkText = chunkText;
|
|
exports.hashText = hashText;
|
|
exports.cosineSimilarity = cosineSimilarity;
|
|
exports.embed = embed;
|
|
exports.hasEmbeddingKey = hasEmbeddingKey;
|
|
/**
|
|
* RAG sem pgvector — embeddings + similaridade calculados na aplicação.
|
|
*
|
|
* Os vetores são gerados via OpenAI (text-embedding-3-small) ou Gemini
|
|
* (text-embedding-004), conforme a chave configurada no plugin, e guardados
|
|
* como JSON em coluna text. A busca por similaridade (cosseno) roda em Node.
|
|
* Para a base pequena da secretária (dezenas/centenas de chunks) é rápido e
|
|
* dispensa a extensão pgvector.
|
|
*/
|
|
const crypto_1 = __importDefault(require("crypto"));
|
|
/** Quebra o texto em chunks (~maxLen chars), preferindo limites de parágrafo. */
|
|
function chunkText(text, maxLen = 600) {
|
|
const paras = text.split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
|
|
const merged = [];
|
|
let buf = '';
|
|
for (const p of paras) {
|
|
if (buf && (buf.length + 2 + p.length) > maxLen) {
|
|
merged.push(buf);
|
|
buf = p;
|
|
}
|
|
else {
|
|
buf = buf ? `${buf}\n\n${p}` : p;
|
|
}
|
|
}
|
|
if (buf)
|
|
merged.push(buf);
|
|
// Fatia parágrafos gigantes que excedam bastante o limite.
|
|
const out = [];
|
|
for (const c of merged) {
|
|
if (c.length <= maxLen * 1.5) {
|
|
out.push(c);
|
|
continue;
|
|
}
|
|
for (let i = 0; i < c.length; i += maxLen)
|
|
out.push(c.slice(i, i + maxLen));
|
|
}
|
|
return out.filter(Boolean);
|
|
}
|
|
function hashText(text) {
|
|
return crypto_1.default.createHash('md5').update(text).digest('hex');
|
|
}
|
|
function cosineSimilarity(a, b) {
|
|
const len = Math.min(a.length, b.length);
|
|
let dot = 0, na = 0, nb = 0;
|
|
for (let i = 0; i < len; i++) {
|
|
dot += a[i] * b[i];
|
|
na += a[i] * a[i];
|
|
nb += b[i] * b[i];
|
|
}
|
|
if (!na || !nb)
|
|
return 0;
|
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
}
|
|
/**
|
|
* Gera o embedding de um texto. Usa OpenAI se houver openai_key, senão Gemini.
|
|
* Retorna null em qualquer falha (o chamador deve cair no fallback).
|
|
*/
|
|
async function embed(text, cfg) {
|
|
const input = (text ?? '').slice(0, 8000);
|
|
if (!input.trim())
|
|
return null;
|
|
const openaiKey = cfg?.openai_key;
|
|
const geminiKey = cfg?.gemini_key;
|
|
try {
|
|
if (openaiKey) {
|
|
const res = await fetch('https://api.openai.com/v1/embeddings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${openaiKey}` },
|
|
body: JSON.stringify({ model: 'text-embedding-3-small', input }),
|
|
});
|
|
if (!res.ok)
|
|
return null;
|
|
const data = await res.json();
|
|
return data?.data?.[0]?.embedding ?? null;
|
|
}
|
|
if (geminiKey) {
|
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key=${geminiKey}`;
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ model: 'models/text-embedding-004', content: { parts: [{ text: input }] } }),
|
|
});
|
|
if (!res.ok)
|
|
return null;
|
|
const data = await res.json();
|
|
return data?.embedding?.values ?? null;
|
|
}
|
|
}
|
|
catch {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
function hasEmbeddingKey(cfg) {
|
|
return !!(cfg?.openai_key || cfg?.gemini_key);
|
|
}
|
|
//# sourceMappingURL=embeddings.js.map
|