@openenthrium/oe-runtime-sdk 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/index.js +58 -0
- package/package.json +64 -0
- package/src/data/connectionTypes.json +18587 -0
- package/src/engine/index.js +183 -0
- package/src/engine/llm.js +71 -0
- package/src/engine/promptBuilder.js +38 -0
- package/src/index.js +156 -0
- package/src/middleware/auth.js +63 -0
- package/src/providers/embedding/index.js +69 -0
- package/src/providers/llm/index.js +136 -0
- package/src/routes/admin.js +686 -0
- package/src/routes/agents.js +193 -0
- package/src/routes/apiKeys.js +55 -0
- package/src/routes/audio.js +111 -0
- package/src/routes/auth.js +83 -0
- package/src/routes/chat.js +381 -0
- package/src/routes/connectors.js +435 -0
- package/src/routes/dashboard.js +244 -0
- package/src/routes/documents.js +443 -0
- package/src/routes/embed.js +103 -0
- package/src/routes/marketplace.js +49 -0
- package/src/routes/models.js +81 -0
- package/src/routes/oauth.js +423 -0
- package/src/routes/projects.js +238 -0
- package/src/routes/settings.js +45 -0
- package/src/routes/setup.js +42 -0
- package/src/routes/sso.js +218 -0
- package/src/routes/superadmin.js +51 -0
- package/src/routes/templates.js +75 -0
- package/src/routes/threads.js +53 -0
- package/src/routes/workspaces.js +464 -0
- package/src/telemetry/bootstrap.js +39 -0
- package/src/telemetry/registration.js +16 -0
- package/src/utils/activityLog.js +16 -0
- package/src/utils/agentChain.js +141 -0
- package/src/utils/buildVisualization.js +102 -0
- package/src/utils/dlpScanner.js +57 -0
- package/src/utils/ingestionQueue.js +240 -0
- package/src/utils/prepareConnectors.js +66 -0
- package/src/utils/rag/_settings.js +6 -0
- package/src/utils/rag/chroma.js +82 -0
- package/src/utils/rag/lancedb.js +103 -0
- package/src/utils/rag/milvus.js +116 -0
- package/src/utils/rag/pgvector.js +104 -0
- package/src/utils/rag/pinecone.js +71 -0
- package/src/utils/rag/qdrant.js +106 -0
- package/src/utils/rag/weaviate.js +137 -0
- package/src/utils/rag/zilliz.js +115 -0
- package/src/utils/scheduler.js +168 -0
- package/src/utils/tier.js +106 -0
- package/src/utils/tools/adapters/_template.js +74 -0
- package/src/utils/tools/adapters/box.js +71 -0
- package/src/utils/tools/adapters/confluence.js +80 -0
- package/src/utils/tools/adapters/database.js +215 -0
- package/src/utils/tools/adapters/dropbox.js +74 -0
- package/src/utils/tools/adapters/elasticsearch.js +90 -0
- package/src/utils/tools/adapters/filesystem.js +197 -0
- package/src/utils/tools/adapters/freshdesk.js +87 -0
- package/src/utils/tools/adapters/gdrive.js +326 -0
- package/src/utils/tools/adapters/github.js +169 -0
- package/src/utils/tools/adapters/gmail.js +157 -0
- package/src/utils/tools/adapters/graphql.js +73 -0
- package/src/utils/tools/adapters/hubspot.js +101 -0
- package/src/utils/tools/adapters/image-gen.js +120 -0
- package/src/utils/tools/adapters/jira.js +86 -0
- package/src/utils/tools/adapters/kafka.js +126 -0
- package/src/utils/tools/adapters/ldap.js +118 -0
- package/src/utils/tools/adapters/mcp-client.js +138 -0
- package/src/utils/tools/adapters/mongodb.js +119 -0
- package/src/utils/tools/adapters/mqtt.js +106 -0
- package/src/utils/tools/adapters/music-gen.js +118 -0
- package/src/utils/tools/adapters/notion.js +93 -0
- package/src/utils/tools/adapters/ocr.js +107 -0
- package/src/utils/tools/adapters/onedrive.js +72 -0
- package/src/utils/tools/adapters/redis.js +104 -0
- package/src/utils/tools/adapters/rest-api.js +119 -0
- package/src/utils/tools/adapters/s3.js +142 -0
- package/src/utils/tools/adapters/search.js +80 -0
- package/src/utils/tools/adapters/sftp.js +121 -0
- package/src/utils/tools/adapters/shell.js +97 -0
- package/src/utils/tools/adapters/slack.js +83 -0
- package/src/utils/tools/adapters/speech.js +160 -0
- package/src/utils/tools/adapters/ssh.js +113 -0
- package/src/utils/tools/adapters/video-gen.js +152 -0
- package/src/utils/tools/adapters/web3.js +111 -0
- package/src/utils/tools/adapters/zendesk.js +82 -0
- package/src/utils/tools/adapters/zoho-mail.js +246 -0
- package/src/utils/tools/registry.js +229 -0
- package/src/utils/vectorStore.js +68 -0
- package/src/utils/workflowEngine.js +4 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const { embed } = require("../../providers/embedding");
|
|
2
|
+
const getSetting = require("./_settings");
|
|
3
|
+
|
|
4
|
+
let _client = null;
|
|
5
|
+
let _clientKey = null;
|
|
6
|
+
|
|
7
|
+
async function getClient(cfg = {}) {
|
|
8
|
+
const url = cfg.url || (await getSetting("vector_db_url")) || "http://localhost:8000";
|
|
9
|
+
const apiKey = cfg.apiKey || (await getSetting("vector_db_api_key")) || "";
|
|
10
|
+
const key = `${url}:${apiKey}`;
|
|
11
|
+
if (!_client || _clientKey !== key) {
|
|
12
|
+
const { ChromaClient } = await import("chromadb");
|
|
13
|
+
_client = new ChromaClient({
|
|
14
|
+
path: url,
|
|
15
|
+
...(apiKey ? { auth: { provider: "token", credentials: apiKey } } : {}),
|
|
16
|
+
});
|
|
17
|
+
_clientKey = key;
|
|
18
|
+
}
|
|
19
|
+
return _client;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function colName(workspaceSlug) {
|
|
23
|
+
return `ws_${workspaceSlug.replace(/-/g, "_")}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
|
|
27
|
+
const { batchSize = 50, onProgress, shouldCancel } = options;
|
|
28
|
+
const client = await getClient(config);
|
|
29
|
+
const collection = await client.getOrCreateCollection({ name: colName(workspaceSlug) });
|
|
30
|
+
|
|
31
|
+
let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
34
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
35
|
+
|
|
36
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
37
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
38
|
+
embeddingTokens += tokensUsed;
|
|
39
|
+
if (!embeddingModel) embeddingModel = model;
|
|
40
|
+
|
|
41
|
+
await collection.upsert({
|
|
42
|
+
ids: batch.map((_, j) => `${documentUid}_${i + j}`),
|
|
43
|
+
documents: batch.map(c => c.text),
|
|
44
|
+
embeddings,
|
|
45
|
+
metadatas: batch.map(c => ({ documentUid, ...(c.metadata || {}) })),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
chunksProcessed += batch.length;
|
|
49
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
50
|
+
}
|
|
51
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
|
|
55
|
+
try {
|
|
56
|
+
const client = await getClient(config);
|
|
57
|
+
const collection = await client.getOrCreateCollection({ name: colName(workspaceSlug) });
|
|
58
|
+
const { embeddings } = await embed([query]);
|
|
59
|
+
const results = await collection.query({
|
|
60
|
+
queryEmbeddings: [embeddings[0]],
|
|
61
|
+
nResults: topK,
|
|
62
|
+
include: ["documents", "metadatas", "distances"],
|
|
63
|
+
});
|
|
64
|
+
return (results.ids[0] || []).map((_, i) => ({
|
|
65
|
+
text: results.documents[0][i],
|
|
66
|
+
metadata: results.metadatas[0][i] || {},
|
|
67
|
+
score: 1 - (results.distances[0][i] || 0),
|
|
68
|
+
}));
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const client = await getClient(config);
|
|
77
|
+
const collection = await client.getOrCreateCollection({ name: colName(workspaceSlug) });
|
|
78
|
+
await collection.delete({ where: { documentUid } });
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { embed } = require("../../providers/embedding");
|
|
3
|
+
|
|
4
|
+
let lancedb;
|
|
5
|
+
async function getDb() {
|
|
6
|
+
if (!lancedb) lancedb = await import("@lancedb/lancedb");
|
|
7
|
+
const dbPath = path.join(__dirname, "../../../storage/lancedb");
|
|
8
|
+
return lancedb.connect(dbPath);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function ensureFtsIndex(table) {
|
|
12
|
+
try {
|
|
13
|
+
const indices = await table.listIndices();
|
|
14
|
+
if (!indices.some(idx => idx.indexType === "FTS")) {
|
|
15
|
+
await table.createIndex("text", { config: lancedb.Index.fts() });
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}) {
|
|
21
|
+
const { batchSize = 10, onProgress, shouldCancel } = options;
|
|
22
|
+
const db = await getDb();
|
|
23
|
+
const tableName = `ws_${workspaceSlug.replace(/-/g, "_")}`;
|
|
24
|
+
|
|
25
|
+
let chunksProcessed = 0;
|
|
26
|
+
let embeddingTokens = 0;
|
|
27
|
+
let embeddingModel = null;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
30
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
31
|
+
|
|
32
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
33
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
34
|
+
|
|
35
|
+
embeddingTokens += tokensUsed;
|
|
36
|
+
if (!embeddingModel) embeddingModel = model;
|
|
37
|
+
|
|
38
|
+
const records = batch.map((c, j) => ({
|
|
39
|
+
id: `${documentUid}_${i + j}`,
|
|
40
|
+
documentUid,
|
|
41
|
+
text: c.text,
|
|
42
|
+
metadata: JSON.stringify(c.metadata || {}),
|
|
43
|
+
vector: embeddings[j],
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const table = await db.openTable(tableName);
|
|
48
|
+
await table.add(records);
|
|
49
|
+
} catch {
|
|
50
|
+
const table = await db.createTable(tableName, records);
|
|
51
|
+
await ensureFtsIndex(table);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
chunksProcessed += batch.length;
|
|
55
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const table = await db.openTable(tableName);
|
|
60
|
+
await ensureFtsIndex(table);
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function similaritySearch(workspaceSlug, query, topK = 5) {
|
|
67
|
+
const db = await getDb();
|
|
68
|
+
const tableName = `ws_${workspaceSlug.replace(/-/g, "_")}`;
|
|
69
|
+
try {
|
|
70
|
+
const { embeddings } = await embed([query]);
|
|
71
|
+
const table = await db.openTable(tableName);
|
|
72
|
+
const fetchK = Math.max(topK * 3, 20);
|
|
73
|
+
|
|
74
|
+
const [vectorResults, ftsResults] = await Promise.all([
|
|
75
|
+
table.search(embeddings[0]).limit(fetchK).toArray(),
|
|
76
|
+
table.search(query, "fts").limit(fetchK).toArray().catch(() => []),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const RRF_K = 60;
|
|
80
|
+
const scores = {}, texts = {}, metas = {};
|
|
81
|
+
vectorResults.forEach((r, i) => { scores[r.id] = (scores[r.id] || 0) + 1 / (RRF_K + i + 1); texts[r.id] = r.text; metas[r.id] = r.metadata; });
|
|
82
|
+
ftsResults.forEach((r, i) => { scores[r.id] = (scores[r.id] || 0) + 1 / (RRF_K + i + 1); texts[r.id] = r.text; metas[r.id] = r.metadata; });
|
|
83
|
+
|
|
84
|
+
return Object.entries(scores)
|
|
85
|
+
.sort((a, b) => b[1] - a[1])
|
|
86
|
+
.slice(0, topK)
|
|
87
|
+
.map(([id, score]) => ({ text: texts[id], metadata: JSON.parse(metas[id] || "{}"), score }));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`[lancedb] similaritySearch failed for workspace "${workspaceSlug}":`, err?.message || err);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid) {
|
|
95
|
+
const db = await getDb();
|
|
96
|
+
const tableName = `ws_${workspaceSlug.replace(/-/g, "_")}`;
|
|
97
|
+
try {
|
|
98
|
+
const table = await db.openTable(tableName);
|
|
99
|
+
await table.delete(`documentUid = '${documentUid}'`);
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
const { embed } = require("../../providers/embedding");
|
|
2
|
+
const getSetting = require("./_settings");
|
|
3
|
+
|
|
4
|
+
let _client = null, _clientKey = null;
|
|
5
|
+
|
|
6
|
+
async function getClient(cfg = {}) {
|
|
7
|
+
const address = cfg.url || (await getSetting("vector_db_url")) || "localhost:19530";
|
|
8
|
+
const token = cfg.apiKey || (await getSetting("vector_db_api_key")) || "";
|
|
9
|
+
const key = `${address}:${token}`;
|
|
10
|
+
if (!_client || _clientKey !== key) {
|
|
11
|
+
const { MilvusClient } = require("@zilliz/milvus2-sdk-node");
|
|
12
|
+
_client = new MilvusClient({ address, ...(token ? { token } : {}) });
|
|
13
|
+
_clientKey = key;
|
|
14
|
+
}
|
|
15
|
+
return _client;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function colName(workspaceSlug) {
|
|
19
|
+
return `ws_${workspaceSlug.replace(/[-]/g, "_")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function ensureCollection(client, name, dim) {
|
|
23
|
+
const exists = await client.hasCollection({ collection_name: name });
|
|
24
|
+
if (!exists.value) {
|
|
25
|
+
await client.createCollection({
|
|
26
|
+
collection_name: name,
|
|
27
|
+
fields: [
|
|
28
|
+
{ name: "id", data_type: "VarChar", max_length: 128, is_primary_key: true },
|
|
29
|
+
{ name: "documentUid", data_type: "VarChar", max_length: 128 },
|
|
30
|
+
{ name: "text", data_type: "VarChar", max_length: 65535 },
|
|
31
|
+
{ name: "metadata", data_type: "VarChar", max_length: 65535 },
|
|
32
|
+
{ name: "embedding", data_type: "FloatVector", dim },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
await client.createIndex({
|
|
36
|
+
collection_name: name,
|
|
37
|
+
field_name: "embedding",
|
|
38
|
+
index_type: "HNSW",
|
|
39
|
+
metric_type: "COSINE",
|
|
40
|
+
params: { M: 16, efConstruction: 200 },
|
|
41
|
+
});
|
|
42
|
+
await client.loadCollection({ collection_name: name });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
|
|
47
|
+
const { batchSize = 50, onProgress, shouldCancel } = options;
|
|
48
|
+
const client = await getClient(config);
|
|
49
|
+
const col = colName(workspaceSlug);
|
|
50
|
+
|
|
51
|
+
let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
|
|
52
|
+
let collectionReady = false;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
55
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
56
|
+
|
|
57
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
58
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
59
|
+
embeddingTokens += tokensUsed;
|
|
60
|
+
if (!embeddingModel) embeddingModel = model;
|
|
61
|
+
|
|
62
|
+
if (!collectionReady) {
|
|
63
|
+
await ensureCollection(client, col, embeddings[0].length);
|
|
64
|
+
collectionReady = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await client.insert({
|
|
68
|
+
collection_name: col,
|
|
69
|
+
data: batch.map((c, j) => ({
|
|
70
|
+
id: `${documentUid}_${i + j}`.slice(0, 128),
|
|
71
|
+
documentUid: documentUid.slice(0, 128),
|
|
72
|
+
text: (c.text || "").slice(0, 65535),
|
|
73
|
+
metadata: JSON.stringify(c.metadata || {}).slice(0, 65535),
|
|
74
|
+
embedding: embeddings[j],
|
|
75
|
+
})),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
chunksProcessed += batch.length;
|
|
79
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
80
|
+
}
|
|
81
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
|
|
85
|
+
try {
|
|
86
|
+
const client = await getClient(config);
|
|
87
|
+
const { embeddings } = await embed([query]);
|
|
88
|
+
const results = await client.search({
|
|
89
|
+
collection_name: colName(workspaceSlug),
|
|
90
|
+
vectors: [embeddings[0]],
|
|
91
|
+
vector_type: "FloatVector",
|
|
92
|
+
limit: topK,
|
|
93
|
+
output_fields: ["text", "metadata", "documentUid"],
|
|
94
|
+
metric_type: "COSINE",
|
|
95
|
+
});
|
|
96
|
+
return (results.results || []).map(r => ({
|
|
97
|
+
text: r.text || "",
|
|
98
|
+
metadata: (() => { try { return JSON.parse(r.metadata || "{}"); } catch { return {}; } })(),
|
|
99
|
+
score: r.score,
|
|
100
|
+
}));
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
|
|
107
|
+
try {
|
|
108
|
+
const client = await getClient(config);
|
|
109
|
+
await client.deleteEntities({
|
|
110
|
+
collection_name: colName(workspaceSlug),
|
|
111
|
+
expr: `documentUid == "${documentUid}"`,
|
|
112
|
+
});
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const { embed } = require("../../providers/embedding");
|
|
2
|
+
const getSetting = require("./_settings");
|
|
3
|
+
const { v5: uuidv5 } = require("uuid");
|
|
4
|
+
|
|
5
|
+
const UUID_NS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
6
|
+
|
|
7
|
+
let _pool = null, _poolKey = null;
|
|
8
|
+
|
|
9
|
+
async function getPool(cfg = {}) {
|
|
10
|
+
const connStr = cfg.url || (await getSetting("vector_db_url")) || "";
|
|
11
|
+
if (!_pool || _poolKey !== connStr) {
|
|
12
|
+
const { Pool } = require("pg");
|
|
13
|
+
const pgvector = require("pgvector/pg");
|
|
14
|
+
_pool = new Pool({ connectionString: connStr });
|
|
15
|
+
await pgvector.registerTypes(_pool);
|
|
16
|
+
_poolKey = connStr;
|
|
17
|
+
}
|
|
18
|
+
return _pool;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function ensureTable(pool, dim) {
|
|
22
|
+
await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
23
|
+
await pool.query(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS embeddings (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
workspace_slug TEXT NOT NULL,
|
|
27
|
+
document_uid TEXT NOT NULL,
|
|
28
|
+
text TEXT NOT NULL,
|
|
29
|
+
metadata JSONB,
|
|
30
|
+
embedding vector(${dim})
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
await pool.query("CREATE INDEX IF NOT EXISTS idx_emb_workspace ON embeddings(workspace_slug)");
|
|
34
|
+
await pool.query("CREATE INDEX IF NOT EXISTS idx_emb_document ON embeddings(document_uid)");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let _tableReady = false;
|
|
38
|
+
|
|
39
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
|
|
40
|
+
const { batchSize = 50, onProgress, shouldCancel } = options;
|
|
41
|
+
const pool = await getPool(config);
|
|
42
|
+
|
|
43
|
+
let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
46
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
47
|
+
|
|
48
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
49
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
50
|
+
embeddingTokens += tokensUsed;
|
|
51
|
+
if (!embeddingModel) embeddingModel = model;
|
|
52
|
+
|
|
53
|
+
if (!_tableReady) {
|
|
54
|
+
await ensureTable(pool, embeddings[0].length);
|
|
55
|
+
_tableReady = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pgvector = require("pgvector/pg");
|
|
59
|
+
for (let j = 0; j < batch.length; j++) {
|
|
60
|
+
const id = uuidv5(`${documentUid}_${i + j}`, UUID_NS);
|
|
61
|
+
await pool.query(
|
|
62
|
+
`INSERT INTO embeddings (id, workspace_slug, document_uid, text, metadata, embedding)
|
|
63
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
64
|
+
ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, text = EXCLUDED.text`,
|
|
65
|
+
[id, workspaceSlug, documentUid, batch[j].text, batch[j].metadata || {}, pgvector.toSql(embeddings[j])]
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
chunksProcessed += batch.length;
|
|
70
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
71
|
+
}
|
|
72
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
|
|
76
|
+
try {
|
|
77
|
+
const pool = await getPool(config);
|
|
78
|
+
const pgvector = require("pgvector/pg");
|
|
79
|
+
const { embeddings } = await embed([query]);
|
|
80
|
+
const { rows } = await pool.query(
|
|
81
|
+
`SELECT text, metadata, 1 - (embedding <=> $1) AS score
|
|
82
|
+
FROM embeddings
|
|
83
|
+
WHERE workspace_slug = $2
|
|
84
|
+
ORDER BY embedding <=> $1
|
|
85
|
+
LIMIT $3`,
|
|
86
|
+
[pgvector.toSql(embeddings[0]), workspaceSlug, topK]
|
|
87
|
+
);
|
|
88
|
+
return rows.map(r => ({ text: r.text, metadata: r.metadata || {}, score: parseFloat(r.score) }));
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
|
|
95
|
+
try {
|
|
96
|
+
const pool = await getPool(config);
|
|
97
|
+
await pool.query(
|
|
98
|
+
"DELETE FROM embeddings WHERE workspace_slug = $1 AND document_uid = $2",
|
|
99
|
+
[workspaceSlug, documentUid]
|
|
100
|
+
);
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { embed } = require("../../providers/embedding");
|
|
2
|
+
const getSetting = require("./_settings");
|
|
3
|
+
|
|
4
|
+
let _pinecone = null;
|
|
5
|
+
let _pineconeKey = null;
|
|
6
|
+
|
|
7
|
+
async function getIndex(cfg = {}) {
|
|
8
|
+
const apiKey = cfg.apiKey || (await getSetting("vector_db_api_key")) || "";
|
|
9
|
+
const indexName = cfg.index || (await getSetting("vector_db_index")) || "open-enthrium";
|
|
10
|
+
if (!_pinecone || _pineconeKey !== apiKey) {
|
|
11
|
+
const { Pinecone } = await import("@pinecone-database/pinecone");
|
|
12
|
+
_pinecone = new Pinecone({ apiKey });
|
|
13
|
+
_pineconeKey = apiKey;
|
|
14
|
+
}
|
|
15
|
+
return _pinecone.index(indexName);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
|
|
19
|
+
const { batchSize = 100, onProgress, shouldCancel } = options;
|
|
20
|
+
const index = await getIndex(config);
|
|
21
|
+
const ns = index.namespace(workspaceSlug);
|
|
22
|
+
|
|
23
|
+
let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
26
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
27
|
+
|
|
28
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
29
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
30
|
+
embeddingTokens += tokensUsed;
|
|
31
|
+
if (!embeddingModel) embeddingModel = model;
|
|
32
|
+
|
|
33
|
+
await ns.upsert(batch.map((c, j) => ({
|
|
34
|
+
id: `${documentUid}_${i + j}`,
|
|
35
|
+
values: embeddings[j],
|
|
36
|
+
metadata: { documentUid, text: c.text, ...(c.metadata || {}) },
|
|
37
|
+
})));
|
|
38
|
+
|
|
39
|
+
chunksProcessed += batch.length;
|
|
40
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
41
|
+
}
|
|
42
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const index = await getIndex(config);
|
|
48
|
+
const ns = index.namespace(workspaceSlug);
|
|
49
|
+
const { embeddings } = await embed([query]);
|
|
50
|
+
const results = await ns.query({ vector: embeddings[0], topK, includeMetadata: true });
|
|
51
|
+
return (results.matches || []).map(m => ({
|
|
52
|
+
text: m.metadata?.text || "",
|
|
53
|
+
metadata: Object.fromEntries(
|
|
54
|
+
Object.entries(m.metadata || {}).filter(([k]) => k !== "text" && k !== "documentUid")
|
|
55
|
+
),
|
|
56
|
+
score: m.score,
|
|
57
|
+
}));
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const index = await getIndex(config);
|
|
66
|
+
const ns = index.namespace(workspaceSlug);
|
|
67
|
+
await ns.deleteMany({ documentUid });
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { embed } = require("../../providers/embedding");
|
|
2
|
+
const getSetting = require("./_settings");
|
|
3
|
+
const { v5: uuidv5 } = require("uuid");
|
|
4
|
+
|
|
5
|
+
const UUID_NS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
6
|
+
|
|
7
|
+
let _client = null, _clientKey = null;
|
|
8
|
+
|
|
9
|
+
async function getClient(cfg = {}) {
|
|
10
|
+
const url = cfg.url || (await getSetting("vector_db_url")) || "http://localhost:6333";
|
|
11
|
+
const apiKey = cfg.apiKey || (await getSetting("vector_db_api_key")) || "";
|
|
12
|
+
const key = `${url}:${apiKey}`;
|
|
13
|
+
if (!_client || _clientKey !== key) {
|
|
14
|
+
const { QdrantClient } = await import("@qdrant/js-client-rest");
|
|
15
|
+
_client = new QdrantClient({ url, ...(apiKey ? { apiKey } : {}) });
|
|
16
|
+
_clientKey = key;
|
|
17
|
+
}
|
|
18
|
+
return _client;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function colName(workspaceSlug) {
|
|
22
|
+
return `ws_${workspaceSlug.replace(/-/g, "_")}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function ensureCollection(client, name, vectorSize) {
|
|
26
|
+
try {
|
|
27
|
+
await client.getCollection(name);
|
|
28
|
+
} catch {
|
|
29
|
+
await client.createCollection(name, {
|
|
30
|
+
vectors: { size: vectorSize, distance: "Cosine" },
|
|
31
|
+
});
|
|
32
|
+
await client.createPayloadIndex(name, {
|
|
33
|
+
field_name: "documentUid",
|
|
34
|
+
field_schema: "keyword",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function upsertChunksBatched(workspaceSlug, documentUid, chunks, options = {}, config = {}) {
|
|
40
|
+
const { batchSize = 50, onProgress, shouldCancel } = options;
|
|
41
|
+
const client = await getClient(config);
|
|
42
|
+
const col = colName(workspaceSlug);
|
|
43
|
+
|
|
44
|
+
let chunksProcessed = 0, embeddingTokens = 0, embeddingModel = null;
|
|
45
|
+
let collectionReady = false;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < chunks.length; i += batchSize) {
|
|
48
|
+
if (shouldCancel && await shouldCancel()) return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
49
|
+
|
|
50
|
+
const batch = chunks.slice(i, i + batchSize);
|
|
51
|
+
const { embeddings, tokensUsed, model } = await embed(batch.map(c => c.text));
|
|
52
|
+
embeddingTokens += tokensUsed;
|
|
53
|
+
if (!embeddingModel) embeddingModel = model;
|
|
54
|
+
|
|
55
|
+
if (!collectionReady) {
|
|
56
|
+
await ensureCollection(client, col, embeddings[0].length);
|
|
57
|
+
collectionReady = true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await client.upsert(col, {
|
|
61
|
+
wait: true,
|
|
62
|
+
points: batch.map((c, j) => ({
|
|
63
|
+
id: uuidv5(`${documentUid}_${i + j}`, UUID_NS),
|
|
64
|
+
vector: embeddings[j],
|
|
65
|
+
payload: { documentUid, text: c.text, ...(c.metadata || {}) },
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
chunksProcessed += batch.length;
|
|
70
|
+
if (onProgress) await onProgress(chunksProcessed);
|
|
71
|
+
}
|
|
72
|
+
return { chunksProcessed, embeddingTokens, embeddingModel };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function similaritySearch(workspaceSlug, query, topK = 5, config = {}) {
|
|
76
|
+
try {
|
|
77
|
+
const client = await getClient(config);
|
|
78
|
+
const { embeddings } = await embed([query]);
|
|
79
|
+
const results = await client.search(colName(workspaceSlug), {
|
|
80
|
+
vector: embeddings[0],
|
|
81
|
+
limit: topK,
|
|
82
|
+
with_payload: true,
|
|
83
|
+
});
|
|
84
|
+
return results.map(r => ({
|
|
85
|
+
text: r.payload?.text || "",
|
|
86
|
+
metadata: Object.fromEntries(
|
|
87
|
+
Object.entries(r.payload || {}).filter(([k]) => k !== "text" && k !== "documentUid")
|
|
88
|
+
),
|
|
89
|
+
score: r.score,
|
|
90
|
+
}));
|
|
91
|
+
} catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function deleteDocumentChunks(workspaceSlug, documentUid, config = {}) {
|
|
97
|
+
try {
|
|
98
|
+
const client = await getClient(config);
|
|
99
|
+
await client.delete(colName(workspaceSlug), {
|
|
100
|
+
wait: true,
|
|
101
|
+
filter: { must: [{ key: "documentUid", match: { value: documentUid } }] },
|
|
102
|
+
});
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { upsertChunksBatched, similaritySearch, deleteDocumentChunks };
|