@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.
@@ -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
- };
@@ -1,11 +0,0 @@
1
- import {
2
- RAGService
3
- } from "./chunk-ZYYDTHF3.js";
4
- import "./chunk-254THFDS.js";
5
- import "./chunk-4RAQJVJM.js";
6
- import "./chunk-HRRJ3RYT.js";
7
- import "./chunk-27YRIGK5.js";
8
- import "./chunk-JSBRDJBE.js";
9
- export {
10
- RAGService
11
- };