@dyrected/core 2.13.0 → 2.13.3
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/LICENSE.md +50 -0
- package/dist/{app-D_ODsBWA.d.ts → app-De-3eNn1.d.cts} +6 -0
- package/dist/{app-D_ODsBWA.d.cts → app-De-3eNn1.d.ts} +6 -0
- package/dist/index.cjs +17 -0
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +5867 -368
- package/dist/server.cjs +37 -7
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +8487 -1033
- package/package.json +9 -9
- package/dist/access-P6Z2XAFS.js +0 -10
- package/dist/ai-X3M2SE5A.js +0 -17
- package/dist/ai-tools-UPKJY3TX.js +0 -14
- package/dist/chunk-254THFDS.js +0 -202
- package/dist/chunk-27YRIGK5.js +0 -1758
- package/dist/chunk-4RAQJVJM.js +0 -18
- package/dist/chunk-DZMBVIMN.js +0 -3910
- package/dist/chunk-HRRJ3RYT.js +0 -70
- package/dist/chunk-JSBRDJBE.js +0 -30
- package/dist/chunk-W7GKJM2X.js +0 -1136
- package/dist/chunk-ZYYDTHF3.js +0 -635
- package/dist/rag.service-YDGEYTXS.js +0 -11
- package/dist/where-sanitizer-CAAIYSSK.js +0 -76
package/dist/chunk-ZYYDTHF3.js
DELETED
|
@@ -1,635 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sanitizeDocForAI
|
|
3
|
-
} from "./chunk-254THFDS.js";
|
|
4
|
-
import {
|
|
5
|
-
AI_CHUNKS_COLLECTION,
|
|
6
|
-
isAICollection
|
|
7
|
-
} from "./chunk-4RAQJVJM.js";
|
|
8
|
-
import {
|
|
9
|
-
isAccessAllowed
|
|
10
|
-
} from "./chunk-HRRJ3RYT.js";
|
|
11
|
-
|
|
12
|
-
// src/services/rag/normalizer.ts
|
|
13
|
-
function stripHtml(html) {
|
|
14
|
-
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").replace(/\s+([.,;:!?])/g, "$1").trim();
|
|
15
|
-
}
|
|
16
|
-
function extractTextFromNode(node) {
|
|
17
|
-
if (!node) return "";
|
|
18
|
-
if (typeof node === "string") return node;
|
|
19
|
-
if (typeof node === "number" || typeof node === "boolean") return String(node);
|
|
20
|
-
if (typeof node.text === "string") {
|
|
21
|
-
let text = node.text;
|
|
22
|
-
if (node.bold) text = `**${text}**`;
|
|
23
|
-
if (node.italic) text = `*${text}*`;
|
|
24
|
-
if (node.code) text = `\`${text}\``;
|
|
25
|
-
return text;
|
|
26
|
-
}
|
|
27
|
-
const childrenText = Array.isArray(node.children) ? node.children.map(extractTextFromNode).join("") : Array.isArray(node.content) ? node.content.map(extractTextFromNode).join("") : "";
|
|
28
|
-
const type = (node.type || node.tag || "").toLowerCase();
|
|
29
|
-
switch (type) {
|
|
30
|
-
case "h1":
|
|
31
|
-
case "heading-one":
|
|
32
|
-
case "heading_1":
|
|
33
|
-
return `
|
|
34
|
-
# ${childrenText}
|
|
35
|
-
`;
|
|
36
|
-
case "h2":
|
|
37
|
-
case "heading-two":
|
|
38
|
-
case "heading_2":
|
|
39
|
-
return `
|
|
40
|
-
## ${childrenText}
|
|
41
|
-
`;
|
|
42
|
-
case "h3":
|
|
43
|
-
case "heading-three":
|
|
44
|
-
case "heading_3":
|
|
45
|
-
return `
|
|
46
|
-
### ${childrenText}
|
|
47
|
-
`;
|
|
48
|
-
case "h4":
|
|
49
|
-
case "h5":
|
|
50
|
-
case "h6":
|
|
51
|
-
return `
|
|
52
|
-
#### ${childrenText}
|
|
53
|
-
`;
|
|
54
|
-
case "paragraph":
|
|
55
|
-
case "p":
|
|
56
|
-
return `
|
|
57
|
-
${childrenText}
|
|
58
|
-
`;
|
|
59
|
-
case "list-item":
|
|
60
|
-
case "li":
|
|
61
|
-
return `- ${childrenText}
|
|
62
|
-
`;
|
|
63
|
-
case "bulleted-list":
|
|
64
|
-
case "ul":
|
|
65
|
-
return `
|
|
66
|
-
${childrenText}
|
|
67
|
-
`;
|
|
68
|
-
case "numbered-list":
|
|
69
|
-
case "ol":
|
|
70
|
-
return `
|
|
71
|
-
${childrenText}
|
|
72
|
-
`;
|
|
73
|
-
case "blockquote":
|
|
74
|
-
case "quote":
|
|
75
|
-
return `
|
|
76
|
-
> ${childrenText}
|
|
77
|
-
`;
|
|
78
|
-
case "code":
|
|
79
|
-
case "code-block":
|
|
80
|
-
return `
|
|
81
|
-
\`\`\`
|
|
82
|
-
${childrenText}
|
|
83
|
-
\`\`\`
|
|
84
|
-
`;
|
|
85
|
-
case "link":
|
|
86
|
-
case "a":
|
|
87
|
-
return node.url || node.href ? `[${childrenText}](${node.url || node.href})` : childrenText;
|
|
88
|
-
default:
|
|
89
|
-
return childrenText;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function normalizeFieldValue(value, _fieldType) {
|
|
93
|
-
if (value === null || value === void 0) return "";
|
|
94
|
-
if (typeof value === "string") {
|
|
95
|
-
const trimmed = value.trim();
|
|
96
|
-
if (!trimmed) return "";
|
|
97
|
-
if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
|
|
98
|
-
return stripHtml(trimmed);
|
|
99
|
-
}
|
|
100
|
-
return trimmed;
|
|
101
|
-
}
|
|
102
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
103
|
-
return String(value);
|
|
104
|
-
}
|
|
105
|
-
if (Array.isArray(value)) {
|
|
106
|
-
const parts = value.map((item) => normalizeFieldValue(item)).filter(Boolean);
|
|
107
|
-
return parts.join("\n\n");
|
|
108
|
-
}
|
|
109
|
-
if (typeof value === "object") {
|
|
110
|
-
const root = value.root || value;
|
|
111
|
-
const extracted = extractTextFromNode(root);
|
|
112
|
-
if (extracted && extracted.trim().length > 0) {
|
|
113
|
-
return extracted.replace(/\n{3,}/g, "\n\n").trim();
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const entries = Object.entries(value).filter(([k, v]) => !k.startsWith("_") && v !== null && v !== void 0 && typeof v !== "function").map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
117
|
-
return entries.join("\n");
|
|
118
|
-
} catch {
|
|
119
|
-
return "";
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return String(value);
|
|
123
|
-
}
|
|
124
|
-
function extractTextFromDoc(doc, targetFields) {
|
|
125
|
-
if (!doc) return [];
|
|
126
|
-
const results = [];
|
|
127
|
-
if (targetFields && targetFields.length > 0) {
|
|
128
|
-
for (const fieldName of targetFields) {
|
|
129
|
-
const val = doc[fieldName];
|
|
130
|
-
const text = normalizeFieldValue(val);
|
|
131
|
-
if (text && text.trim().length > 0) {
|
|
132
|
-
results.push({ field: fieldName, text: text.trim() });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return results;
|
|
136
|
-
}
|
|
137
|
-
for (const [key, val] of Object.entries(doc)) {
|
|
138
|
-
if (key.startsWith("_") || key === "id" || key === "password" || key === "salt" || key === "hash" || key === "resetPasswordToken" || key === "createdAt" || key === "updatedAt" || key === "createdBy" || key === "updatedBy") {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const text = normalizeFieldValue(val);
|
|
142
|
-
if (text && text.trim().length > 0) {
|
|
143
|
-
results.push({ field: key, text: text.trim() });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return results;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/services/rag/chunker.ts
|
|
150
|
-
function hashContent(content, seed = 0) {
|
|
151
|
-
let h1 = 3735928559 ^ seed;
|
|
152
|
-
let h2 = 1103547991 ^ seed;
|
|
153
|
-
for (let i = 0, ch; i < content.length; i++) {
|
|
154
|
-
ch = content.charCodeAt(i);
|
|
155
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
156
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
157
|
-
}
|
|
158
|
-
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
159
|
-
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
160
|
-
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
|
|
161
|
-
}
|
|
162
|
-
function estimateTokens(text) {
|
|
163
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
164
|
-
}
|
|
165
|
-
function splitTextRecursively(text, maxSize, overlap, separators) {
|
|
166
|
-
if (text.length <= maxSize) {
|
|
167
|
-
return [text];
|
|
168
|
-
}
|
|
169
|
-
let chosenSeparator = separators[separators.length - 1] ?? "";
|
|
170
|
-
let nextSeparators = separators;
|
|
171
|
-
for (let i = 0; i < separators.length; i++) {
|
|
172
|
-
const sep = separators[i];
|
|
173
|
-
if (sep === "" || text.includes(sep)) {
|
|
174
|
-
chosenSeparator = sep;
|
|
175
|
-
nextSeparators = separators.slice(i + 1);
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const rawSplits = chosenSeparator === "" ? text.split("") : text.split(chosenSeparator);
|
|
180
|
-
const resultChunks = [];
|
|
181
|
-
let currentChunk = "";
|
|
182
|
-
for (const piece of rawSplits) {
|
|
183
|
-
const candidate = currentChunk ? currentChunk + (chosenSeparator ? chosenSeparator : "") + piece : piece;
|
|
184
|
-
if (candidate.length <= maxSize) {
|
|
185
|
-
currentChunk = candidate;
|
|
186
|
-
} else {
|
|
187
|
-
if (currentChunk.trim().length > 0) {
|
|
188
|
-
resultChunks.push(currentChunk.trim());
|
|
189
|
-
}
|
|
190
|
-
if (piece.length > maxSize) {
|
|
191
|
-
const subChunks = splitTextRecursively(piece, maxSize, overlap, nextSeparators);
|
|
192
|
-
resultChunks.push(...subChunks);
|
|
193
|
-
currentChunk = "";
|
|
194
|
-
} else {
|
|
195
|
-
if (overlap > 0 && currentChunk.length > overlap) {
|
|
196
|
-
const overlapPrefix = currentChunk.slice(currentChunk.length - overlap);
|
|
197
|
-
currentChunk = overlapPrefix + (chosenSeparator ? chosenSeparator : "") + piece;
|
|
198
|
-
} else {
|
|
199
|
-
currentChunk = piece;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
if (currentChunk.trim().length > 0) {
|
|
205
|
-
resultChunks.push(currentChunk.trim());
|
|
206
|
-
}
|
|
207
|
-
return resultChunks;
|
|
208
|
-
}
|
|
209
|
-
function chunkText(text, options = {}) {
|
|
210
|
-
const {
|
|
211
|
-
maxChunkSize = 1500,
|
|
212
|
-
chunkOverlap = 150,
|
|
213
|
-
separators = ["\n## ", "\n### ", "\n#### ", "\n\n", "\n", ". ", "; ", ", ", " ", ""]
|
|
214
|
-
} = options;
|
|
215
|
-
if (!text || text.trim().length === 0) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
const trimmed = text.trim();
|
|
219
|
-
if (trimmed.length <= maxChunkSize) {
|
|
220
|
-
return [
|
|
221
|
-
{
|
|
222
|
-
text: trimmed,
|
|
223
|
-
tokenCount: estimateTokens(trimmed),
|
|
224
|
-
index: 0
|
|
225
|
-
}
|
|
226
|
-
];
|
|
227
|
-
}
|
|
228
|
-
const rawChunks = splitTextRecursively(trimmed, maxChunkSize, chunkOverlap, separators);
|
|
229
|
-
return rawChunks.map((chunk) => chunk.trim()).filter((chunk) => chunk.length > 0).map((chunk, index) => ({
|
|
230
|
-
text: chunk,
|
|
231
|
-
tokenCount: estimateTokens(chunk),
|
|
232
|
-
index
|
|
233
|
-
}));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// src/services/rag/embedding.service.ts
|
|
237
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
238
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
239
|
-
import { embed, embedMany } from "ai";
|
|
240
|
-
function getEmbeddingModel(config) {
|
|
241
|
-
const ai = config?.ai;
|
|
242
|
-
const provider = ai?.provider;
|
|
243
|
-
if (provider === "openrouter" || provider === "agentrouter" && process.env.OPENROUTER_API_KEY || !provider && process.env.OPENROUTER_API_KEY) {
|
|
244
|
-
const apiKey = ai?.apiKey || process.env.OPENROUTER_API_KEY;
|
|
245
|
-
if (apiKey) {
|
|
246
|
-
const baseURL = ai?.baseURL || "https://openrouter.ai/api/v1";
|
|
247
|
-
const modelName = ai?.rag?.embeddingModel || "openai/text-embedding-3-small";
|
|
248
|
-
const openRouterProvider = createOpenAI({ apiKey, baseURL });
|
|
249
|
-
return openRouterProvider.textEmbeddingModel(modelName);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (provider === "openai" || provider === "agentrouter" && process.env.OPENAI_API_KEY || !provider && process.env.OPENAI_API_KEY && !process.env.GEMINI_API_KEY) {
|
|
253
|
-
const apiKey = ai?.apiKey || process.env.OPENAI_API_KEY;
|
|
254
|
-
if (apiKey) {
|
|
255
|
-
const baseURL = ai?.baseURL;
|
|
256
|
-
const modelName = ai?.rag?.embeddingModel || "text-embedding-3-small";
|
|
257
|
-
const openaiProvider = createOpenAI({ apiKey, baseURL });
|
|
258
|
-
return openaiProvider.textEmbeddingModel(modelName);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
const geminiApiKey = ai?.apiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
262
|
-
if (geminiApiKey) {
|
|
263
|
-
const googleProvider = createGoogleGenerativeAI({ apiKey: geminiApiKey });
|
|
264
|
-
const modelName = ai?.rag?.embeddingModel || "text-embedding-004";
|
|
265
|
-
return googleProvider.textEmbeddingModel(modelName);
|
|
266
|
-
}
|
|
267
|
-
if (process.env.OPENROUTER_API_KEY) {
|
|
268
|
-
const openRouterProvider = createOpenAI({
|
|
269
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
270
|
-
baseURL: "https://openrouter.ai/api/v1"
|
|
271
|
-
});
|
|
272
|
-
return openRouterProvider.textEmbeddingModel(ai?.rag?.embeddingModel || "openai/text-embedding-3-small");
|
|
273
|
-
}
|
|
274
|
-
throw new Error(
|
|
275
|
-
"No AI API key found for embeddings. Please set OPENROUTER_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY in your .env.local file."
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
var EmbeddingService = class {
|
|
279
|
-
config;
|
|
280
|
-
model;
|
|
281
|
-
constructor(config) {
|
|
282
|
-
this.config = config;
|
|
283
|
-
this.model = getEmbeddingModel(config);
|
|
284
|
-
}
|
|
285
|
-
async embedText(text) {
|
|
286
|
-
const { embedding } = await embed({
|
|
287
|
-
model: this.model,
|
|
288
|
-
value: text
|
|
289
|
-
});
|
|
290
|
-
return embedding;
|
|
291
|
-
}
|
|
292
|
-
async embedBatch(texts) {
|
|
293
|
-
if (texts.length === 0) return [];
|
|
294
|
-
const batchSize = 64;
|
|
295
|
-
const results = [];
|
|
296
|
-
for (let i = 0; i < texts.length; i += batchSize) {
|
|
297
|
-
const batch = texts.slice(i, i + batchSize);
|
|
298
|
-
const { embeddings } = await embedMany({
|
|
299
|
-
model: this.model,
|
|
300
|
-
values: batch
|
|
301
|
-
});
|
|
302
|
-
results.push(...embeddings);
|
|
303
|
-
}
|
|
304
|
-
return results;
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
function cosineSimilarity(a, b) {
|
|
308
|
-
if (!a || !b || a.length === 0 || b.length === 0 || a.length !== b.length) {
|
|
309
|
-
return 0;
|
|
310
|
-
}
|
|
311
|
-
let dotProduct = 0;
|
|
312
|
-
let normA = 0;
|
|
313
|
-
let normB = 0;
|
|
314
|
-
for (let i = 0; i < a.length; i++) {
|
|
315
|
-
const ai = a[i];
|
|
316
|
-
const bi = b[i];
|
|
317
|
-
dotProduct += ai * bi;
|
|
318
|
-
normA += ai * ai;
|
|
319
|
-
normB += bi * bi;
|
|
320
|
-
}
|
|
321
|
-
if (normA === 0 || normB === 0) return 0;
|
|
322
|
-
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/services/rag/rag.service.ts
|
|
326
|
-
var RAGService = class {
|
|
327
|
-
static embeddingServiceCache = /* @__PURE__ */ new Map();
|
|
328
|
-
static getEmbeddingService(config) {
|
|
329
|
-
const key = config.ai?.apiKey || config.ai?.provider || "default";
|
|
330
|
-
if (!this.embeddingServiceCache.has(key)) {
|
|
331
|
-
this.embeddingServiceCache.set(key, new EmbeddingService(config));
|
|
332
|
-
}
|
|
333
|
-
return this.embeddingServiceCache.get(key);
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Determines whether RAG indexing is enabled for a given collection.
|
|
337
|
-
*/
|
|
338
|
-
static isCollectionRAGEnabled(config, col) {
|
|
339
|
-
if (isAICollection(col.slug) || col.slug.startsWith("_")) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
if (config.ai?.rag?.enabled === false) {
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
if (col.ai?.rag?.enabled === false) {
|
|
346
|
-
return false;
|
|
347
|
-
}
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Indexes a single document's text fields into `_dyrected_ai_chunks`.
|
|
352
|
-
*/
|
|
353
|
-
static async indexDocument(options) {
|
|
354
|
-
const { db, config, collection: collectionSlug, doc, projectId, force = false } = options;
|
|
355
|
-
const col = config.collections?.find((c) => c.slug === collectionSlug);
|
|
356
|
-
if (!col || !this.isCollectionRAGEnabled(config, col)) {
|
|
357
|
-
return { indexed: 0, skipped: 0 };
|
|
358
|
-
}
|
|
359
|
-
const docId = String(doc.id || doc._id || "");
|
|
360
|
-
if (!docId) return { indexed: 0, skipped: 0 };
|
|
361
|
-
const ragConfig = col.ai?.rag;
|
|
362
|
-
const maxChunkSize = ragConfig?.maxChunkSize || config.ai?.rag?.maxChunkSize || 1500;
|
|
363
|
-
const chunkOverlap = ragConfig?.chunkOverlap ?? config.ai?.rag?.chunkOverlap ?? 150;
|
|
364
|
-
const targetFields = ragConfig?.fields;
|
|
365
|
-
const sanitizedDoc = sanitizeDocForAI({
|
|
366
|
-
doc,
|
|
367
|
-
collectionConfig: col,
|
|
368
|
-
globalAIConfig: config.ai
|
|
369
|
-
});
|
|
370
|
-
const titleField = ragConfig?.titleField || col.admin?.useAsTitle || "title";
|
|
371
|
-
const documentTitle = String(sanitizedDoc[titleField] || sanitizedDoc.name || sanitizedDoc.headline || sanitizedDoc.question || `${col.labels?.singular || col.slug} #${docId}`);
|
|
372
|
-
const extractedFields = extractTextFromDoc(sanitizedDoc, targetFields);
|
|
373
|
-
if (extractedFields.length === 0) {
|
|
374
|
-
await this.deleteDocumentChunks({ db, collection: collectionSlug, documentId: docId, projectId });
|
|
375
|
-
return { indexed: 0, skipped: 0 };
|
|
376
|
-
}
|
|
377
|
-
let indexedCount = 0;
|
|
378
|
-
let skippedCount = 0;
|
|
379
|
-
for (const field of extractedFields) {
|
|
380
|
-
const fullFieldText = field.text;
|
|
381
|
-
const contentHash = hashContent(fullFieldText);
|
|
382
|
-
if (!force) {
|
|
383
|
-
try {
|
|
384
|
-
const existing = await db.find({
|
|
385
|
-
collection: AI_CHUNKS_COLLECTION,
|
|
386
|
-
where: {
|
|
387
|
-
projectId,
|
|
388
|
-
collection: collectionSlug,
|
|
389
|
-
documentId: docId,
|
|
390
|
-
field: field.field,
|
|
391
|
-
contentHash
|
|
392
|
-
},
|
|
393
|
-
limit: 1
|
|
394
|
-
});
|
|
395
|
-
if (existing?.docs && existing.docs.length > 0) {
|
|
396
|
-
skippedCount++;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
} catch {
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const chunks = chunkText(fullFieldText, { maxChunkSize, chunkOverlap });
|
|
403
|
-
if (chunks.length === 0) continue;
|
|
404
|
-
try {
|
|
405
|
-
const previousChunks = await db.find({
|
|
406
|
-
collection: AI_CHUNKS_COLLECTION,
|
|
407
|
-
where: {
|
|
408
|
-
projectId,
|
|
409
|
-
collection: collectionSlug,
|
|
410
|
-
documentId: docId,
|
|
411
|
-
field: field.field
|
|
412
|
-
},
|
|
413
|
-
limit: 200
|
|
414
|
-
});
|
|
415
|
-
if (previousChunks?.docs) {
|
|
416
|
-
for (const prev of previousChunks.docs) {
|
|
417
|
-
await db.delete({ collection: AI_CHUNKS_COLLECTION, id: prev.id });
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
} catch {
|
|
421
|
-
}
|
|
422
|
-
const embeddingService = this.getEmbeddingService(config);
|
|
423
|
-
const chunkTexts = chunks.map((c) => c.text);
|
|
424
|
-
const embeddings = await embeddingService.embedBatch(chunkTexts);
|
|
425
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
426
|
-
const chunk = chunks[i];
|
|
427
|
-
const embedding = embeddings[i] || [];
|
|
428
|
-
const chunkRecord = {
|
|
429
|
-
id: `chunk_${docId}_${field.field}_${chunk.index}_${Math.random().toString(36).substring(2, 7)}`,
|
|
430
|
-
projectId,
|
|
431
|
-
collection: collectionSlug,
|
|
432
|
-
documentId: docId,
|
|
433
|
-
field: field.field,
|
|
434
|
-
chunkIndex: chunk.index,
|
|
435
|
-
text: chunk.text,
|
|
436
|
-
embedding,
|
|
437
|
-
tokenCount: chunk.tokenCount,
|
|
438
|
-
contentHash,
|
|
439
|
-
metadata: {
|
|
440
|
-
title: documentTitle,
|
|
441
|
-
slug: typeof doc.slug === "string" ? doc.slug : void 0,
|
|
442
|
-
status: typeof doc.status === "string" ? doc.status : void 0,
|
|
443
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
444
|
-
},
|
|
445
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
446
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
447
|
-
};
|
|
448
|
-
try {
|
|
449
|
-
await db.create({ collection: AI_CHUNKS_COLLECTION, data: chunkRecord });
|
|
450
|
-
indexedCount++;
|
|
451
|
-
} catch (err) {
|
|
452
|
-
console.error(`[dyrected/rag] Failed to save chunk ${chunkRecord.id}:`, err?.message || err);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return { indexed: indexedCount, skipped: skippedCount };
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Deletes all vector chunks associated with a specific document.
|
|
460
|
-
*/
|
|
461
|
-
static async deleteDocumentChunks(options) {
|
|
462
|
-
const { db, collection: collectionSlug, documentId, projectId } = options;
|
|
463
|
-
try {
|
|
464
|
-
const existing = await db.find({
|
|
465
|
-
collection: AI_CHUNKS_COLLECTION,
|
|
466
|
-
where: {
|
|
467
|
-
projectId,
|
|
468
|
-
collection: collectionSlug,
|
|
469
|
-
documentId
|
|
470
|
-
},
|
|
471
|
-
limit: 500
|
|
472
|
-
});
|
|
473
|
-
let count = 0;
|
|
474
|
-
if (existing?.docs) {
|
|
475
|
-
for (const doc of existing.docs) {
|
|
476
|
-
await db.delete({ collection: AI_CHUNKS_COLLECTION, id: doc.id });
|
|
477
|
-
count++;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return count;
|
|
481
|
-
} catch {
|
|
482
|
-
return 0;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Reindexes all documents in a specific collection.
|
|
487
|
-
*/
|
|
488
|
-
static async reindexCollection(options) {
|
|
489
|
-
const { db, config, collection: collectionSlug, projectId, force = false } = options;
|
|
490
|
-
const col = config.collections?.find((c) => c.slug === collectionSlug);
|
|
491
|
-
if (!col || !this.isCollectionRAGEnabled(config, col)) {
|
|
492
|
-
return { collection: collectionSlug, totalDocs: 0, indexedChunks: 0 };
|
|
493
|
-
}
|
|
494
|
-
const result = await db.find({
|
|
495
|
-
collection: collectionSlug,
|
|
496
|
-
where: {},
|
|
497
|
-
limit: 1e3
|
|
498
|
-
});
|
|
499
|
-
const docs = result.docs || [];
|
|
500
|
-
let indexedChunks = 0;
|
|
501
|
-
for (const doc of docs) {
|
|
502
|
-
const res = await this.indexDocument({
|
|
503
|
-
db,
|
|
504
|
-
config,
|
|
505
|
-
collection: collectionSlug,
|
|
506
|
-
doc,
|
|
507
|
-
projectId,
|
|
508
|
-
force
|
|
509
|
-
});
|
|
510
|
-
indexedChunks += res.indexed;
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
collection: collectionSlug,
|
|
514
|
-
totalDocs: docs.length,
|
|
515
|
-
indexedChunks
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
/**
|
|
519
|
-
* Reindexes all RAG-enabled collections across the project.
|
|
520
|
-
*/
|
|
521
|
-
static async reindexAll(options) {
|
|
522
|
-
const { db, config, projectId, force = false } = options;
|
|
523
|
-
const results = [];
|
|
524
|
-
let totalChunks = 0;
|
|
525
|
-
const collections = (config.collections || []).filter((c) => this.isCollectionRAGEnabled(config, c));
|
|
526
|
-
for (const col of collections) {
|
|
527
|
-
try {
|
|
528
|
-
const stats = await this.reindexCollection({
|
|
529
|
-
db,
|
|
530
|
-
config,
|
|
531
|
-
collection: col.slug,
|
|
532
|
-
projectId,
|
|
533
|
-
force
|
|
534
|
-
});
|
|
535
|
-
results.push(stats);
|
|
536
|
-
totalChunks += stats.indexedChunks;
|
|
537
|
-
} catch (err) {
|
|
538
|
-
console.error(`[dyrected/rag] Reindex failed for collection "${col.slug}":`, err?.message || err);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return { collections: results, totalChunks };
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Performs semantic vector search over project chunks with access filtering.
|
|
545
|
-
*/
|
|
546
|
-
static async search(options) {
|
|
547
|
-
const {
|
|
548
|
-
db,
|
|
549
|
-
config,
|
|
550
|
-
query,
|
|
551
|
-
projectId,
|
|
552
|
-
collections: requestedCollections,
|
|
553
|
-
limit = config.ai?.rag?.topK || 4,
|
|
554
|
-
minScore = config.ai?.rag?.minScore ?? 0.45,
|
|
555
|
-
user
|
|
556
|
-
} = options;
|
|
557
|
-
if (!query || !query.trim()) {
|
|
558
|
-
return { query: "", resultsCount: 0, sources: [] };
|
|
559
|
-
}
|
|
560
|
-
const allCollections = config.collections || [];
|
|
561
|
-
const candidateCollections = [];
|
|
562
|
-
for (const col of allCollections) {
|
|
563
|
-
if (!this.isCollectionRAGEnabled(config, col)) continue;
|
|
564
|
-
if (requestedCollections && requestedCollections.length > 0 && !requestedCollections.includes(col.slug)) {
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
|
-
const canRead = await isAccessAllowed(config, col.access?.read, {
|
|
568
|
-
req: { user, siteId: projectId },
|
|
569
|
-
user
|
|
570
|
-
});
|
|
571
|
-
if (canRead) {
|
|
572
|
-
candidateCollections.push(col.slug);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
if (candidateCollections.length === 0) {
|
|
576
|
-
return { query, resultsCount: 0, sources: [] };
|
|
577
|
-
}
|
|
578
|
-
const embeddingService = this.getEmbeddingService(config);
|
|
579
|
-
const queryVector = await embeddingService.embedText(query.trim());
|
|
580
|
-
let chunks;
|
|
581
|
-
try {
|
|
582
|
-
const result = await db.find({
|
|
583
|
-
collection: AI_CHUNKS_COLLECTION,
|
|
584
|
-
where: { projectId },
|
|
585
|
-
limit: 1e3
|
|
586
|
-
});
|
|
587
|
-
chunks = result.docs || [];
|
|
588
|
-
} catch {
|
|
589
|
-
chunks = [];
|
|
590
|
-
}
|
|
591
|
-
const filteredChunks = chunks.filter((c) => candidateCollections.includes(c.collection));
|
|
592
|
-
const scored = filteredChunks.map((chunk) => {
|
|
593
|
-
const score = cosineSimilarity(queryVector, chunk.embedding);
|
|
594
|
-
return {
|
|
595
|
-
chunk,
|
|
596
|
-
score
|
|
597
|
-
};
|
|
598
|
-
}).filter((item) => item.score >= minScore).sort((a, b) => b.score - a.score);
|
|
599
|
-
const topResults = scored.slice(0, limit);
|
|
600
|
-
const sources = topResults.map(({ chunk, score }) => {
|
|
601
|
-
const title = chunk.metadata?.title || `${chunk.collection} #${chunk.documentId}`;
|
|
602
|
-
const url = `/admin/collections/${chunk.collection}/${chunk.documentId}`;
|
|
603
|
-
return {
|
|
604
|
-
id: chunk.id,
|
|
605
|
-
collection: chunk.collection,
|
|
606
|
-
documentId: chunk.documentId,
|
|
607
|
-
title,
|
|
608
|
-
field: chunk.field,
|
|
609
|
-
score: Number(score.toFixed(3)),
|
|
610
|
-
text: `<untrusted_content>
|
|
611
|
-
${chunk.text}
|
|
612
|
-
</untrusted_content>`,
|
|
613
|
-
url,
|
|
614
|
-
metadata: chunk.metadata
|
|
615
|
-
};
|
|
616
|
-
});
|
|
617
|
-
return {
|
|
618
|
-
query,
|
|
619
|
-
resultsCount: sources.length,
|
|
620
|
-
sources
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
export {
|
|
626
|
-
normalizeFieldValue,
|
|
627
|
-
extractTextFromDoc,
|
|
628
|
-
hashContent,
|
|
629
|
-
estimateTokens,
|
|
630
|
-
chunkText,
|
|
631
|
-
getEmbeddingModel,
|
|
632
|
-
EmbeddingService,
|
|
633
|
-
cosineSimilarity,
|
|
634
|
-
RAGService
|
|
635
|
-
};
|