@retrivora-ai/rag-engine 1.8.0 → 1.8.2

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.
Files changed (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -0,0 +1,347 @@
1
+ import { Product, VectorMatch } from '../types';
2
+ import { resolveMetadataValue } from './synonyms';
3
+
4
+ // ─── JSON sanitization ────────────────────────────────────────────────────────
5
+
6
+ export function sanitizeJson(raw: string): string {
7
+ let s = raw.trim();
8
+
9
+ // 1. Evaluate simple arithmetic expressions in values e.g. "v": 495 / 1000
10
+ s = s.replace(
11
+ /:\s*(-?\d+(?:\.\d+)?)\s*([+\-*/])\s*(-?\d+(?:\.\d+)?)/g,
12
+ (_, a, op, b) => {
13
+ const n1 = parseFloat(a);
14
+ const n2 = parseFloat(b);
15
+ const result =
16
+ op === '+' ? n1 + n2
17
+ : op === '-' ? n1 - n2
18
+ : op === '*' ? n1 * n2
19
+ : n2 !== 0 ? n1 / n2
20
+ : n1;
21
+ return `: ${result}`;
22
+ },
23
+ );
24
+
25
+ // 2. Strip JS-style comments while preserving strings (e.g. URLs)
26
+ s = s.replace(/("(?:\\.|[^\\"])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m, g1) => g1 || '');
27
+
28
+ // 3. Quote bare (unquoted) object keys
29
+ s = s.replace(/([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)\s*:/g, '$1"$2":');
30
+
31
+ // 4. Replace single-quoted strings with double-quoted ones
32
+ s = s.replace(/'([^'\\]*(?:\\.[^'\\]*)*)'/g, '"$1"');
33
+
34
+ // 5. Remove trailing commas before ] or }
35
+ s = s.replace(/,\s*([}\]])/g, '$1');
36
+
37
+ // 6. Remove completely empty key-value pairs left by prior cleanup
38
+ s = s.replace(/([{,])\s*""\s*(?=[,}])/g, '$1');
39
+
40
+ // 7. Stack-based structure balancer.
41
+ const stack: ('{' | '[')[] = [];
42
+ let inString = false;
43
+ let escape = false;
44
+
45
+ for (const ch of s) {
46
+ if (escape) { escape = false; continue; }
47
+ if (ch === '\\' && inString) { escape = true; continue; }
48
+ if (ch === '"') { inString = !inString; continue; }
49
+ if (inString) continue;
50
+
51
+ if (ch === '{' || ch === '[') {
52
+ stack.push(ch);
53
+ } else if (ch === '}') {
54
+ if (stack[stack.length - 1] === '{') stack.pop();
55
+ } else if (ch === ']') {
56
+ if (stack[stack.length - 1] === '[') stack.pop();
57
+ }
58
+ }
59
+
60
+ if (inString) s += '"';
61
+ for (let i = stack.length - 1; i >= 0; i--) {
62
+ s += stack[i] === '{' ? '}' : ']';
63
+ }
64
+
65
+ const lastClose = Math.max(s.lastIndexOf('}'), s.lastIndexOf(']'));
66
+ if (lastClose !== -1 && lastClose < s.length - 1 && /\S/.test(s.slice(lastClose + 1))) {
67
+ s = s.substring(0, lastClose + 1);
68
+ }
69
+
70
+ return s;
71
+ }
72
+
73
+ export function extractLikelyJsonBlock(raw: string): string {
74
+ const trimmed = raw.trim();
75
+ const objectStart = trimmed.indexOf('{');
76
+ const arrayStart = trimmed.indexOf('[');
77
+
78
+ const starts = [objectStart, arrayStart].filter((index) => index >= 0);
79
+ if (starts.length === 0) return trimmed;
80
+
81
+ return trimmed.slice(Math.min(...starts));
82
+ }
83
+
84
+ export function looksLikeStructuredPayload(raw: string): boolean {
85
+ return /"?(view|chartType|type|data|items|rows|products|results)"?\s*:/.test(raw);
86
+ }
87
+
88
+ export function stripMarkdownTables(raw: string): string {
89
+ return raw.replace(
90
+ /(?:^|\n)\|[^\n]+\|\n\|(?:\s*:?-+:?\s*\|)+\n(?:\|[^\n]*\|\n?)*/g,
91
+ '\n\n',
92
+ );
93
+ }
94
+
95
+ export function stripStructuredUiLabels(raw: string): string {
96
+ return raw.replace(
97
+ /(?:^|\n)\*\*(?:Chart Data|Table Data|Visualization|Visual Representation)\*\:\s*(?=\n|$)/gi,
98
+ '\n',
99
+ ).replace(
100
+ /(?:^|\n)(?:Chart Data|Table Data|Visualization|Visual Representation)\s*\:\s*(?=\n|$)/gi,
101
+ '\n',
102
+ );
103
+ }
104
+
105
+ function extractBareJsonObject(raw: string): string | null {
106
+ let start = raw.indexOf('{');
107
+ if (start === -1) return null;
108
+
109
+ let bestJson: string | null = null;
110
+ let maxLen = 0;
111
+
112
+ while (start !== -1) {
113
+ let depth = 0;
114
+ let inString = false;
115
+ let escape = false;
116
+
117
+ for (let i = start; i < raw.length; i += 1) {
118
+ const ch = raw[i];
119
+ if (escape) { escape = false; continue; }
120
+ if (ch === '\\' && inString) { escape = true; continue; }
121
+ if (ch === '"') { inString = !inString; continue; }
122
+ if (inString) continue;
123
+
124
+ if (ch === '{') depth += 1;
125
+ else if (ch === '}') {
126
+ depth -= 1;
127
+ if (depth === 0) {
128
+ const candidate = raw.slice(start, i + 1);
129
+ if (candidate.length > maxLen) {
130
+ maxLen = candidate.length;
131
+ bestJson = candidate;
132
+ }
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ start = raw.indexOf('{', start + 1);
138
+ }
139
+
140
+ return bestJson;
141
+ }
142
+
143
+ export function extractStructuredPayload(raw: string): { payload: string | null; text: string } {
144
+ let working = raw;
145
+ const fenceRegex = /```(?:[a-z]+)?\s*([\s\S]*?)```/gi;
146
+
147
+ for (const match of Array.from(raw.matchAll(fenceRegex))) {
148
+ const body = match[1]?.trim() ?? '';
149
+ if (!looksLikeStructuredPayload(body)) continue;
150
+
151
+ working = working.replace(match[0], '');
152
+ return {
153
+ payload: body,
154
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
155
+ };
156
+ }
157
+
158
+ const bareJson = extractBareJsonObject(raw);
159
+ if (bareJson && looksLikeStructuredPayload(bareJson)) {
160
+ working = working.replace(bareJson, '');
161
+ return {
162
+ payload: bareJson,
163
+ text: stripStructuredUiLabels(working).replace(/\n{3,}/g, '\n\n').trim(),
164
+ };
165
+ }
166
+
167
+ return {
168
+ payload: null,
169
+ text: stripStructuredUiLabels(raw).replace(/\n{3,}/g, '\n\n').trim(),
170
+ };
171
+ }
172
+
173
+ export function resolveImage(data: Record<string, unknown>): string | undefined {
174
+ const isPlaceholder = (val: unknown) =>
175
+ typeof val === 'string' && (val === '...' || val === '' || val.toLowerCase() === 'null' || val.toLowerCase() === 'undefined');
176
+
177
+ for (const key of ['image', 'img', 'thumbnail', 'images', 'product_image', 'Image', 'Thumbnail']) {
178
+ const v = data[key];
179
+ if (typeof v === 'string' && v && !isPlaceholder(v)) return v;
180
+ if (Array.isArray(v) && typeof v[0] === 'string' && v[0] && !isPlaceholder(v[0])) return v[0];
181
+ }
182
+ return undefined;
183
+ }
184
+
185
+ export function isLikelyUiOnlyMessage(text: string): boolean {
186
+ const trimmed = text.trim();
187
+ return (
188
+ trimmed.length === 0 ||
189
+ trimmed.length < 30 ||
190
+ /^(chart data|table data|visualization|visual representation|product catalog|product recommendations|catalog summary|availability|inventory status|details)\s*:?\s*$/i.test(trimmed)
191
+ );
192
+ }
193
+
194
+ // ─── Extractors ───────────────────────────────────────────────────────────────
195
+
196
+ export function extractProductsFromSources(sources: VectorMatch[] | undefined, isUser: boolean): Product[] {
197
+ if (isUser || !sources) return [];
198
+ return sources
199
+ .filter(s => {
200
+ const m = s.metadata ?? {};
201
+ const keys = Object.keys(m).map(k => k.toLowerCase());
202
+ const hasProductKey = keys.some(k =>
203
+ ['price', 'image', 'img', 'thumbnail', 'images', 'brand', 'product', 'sku', 'category', 'model', 'cost'].includes(k)
204
+ );
205
+ const hasPricePattern = /\$\s*\d+/.test(s.content);
206
+ return hasProductKey || hasPricePattern || m.type === 'product';
207
+ })
208
+ .map(s => {
209
+ const m = (s.metadata ?? {}) as Record<string, unknown>;
210
+ const name = resolveMetadataValue(m, 'name') as string;
211
+ const brand = resolveMetadataValue(m, 'brand') as string;
212
+ const price = resolveMetadataValue(m, 'price') as string | number;
213
+ const description = resolveMetadataValue(m, 'description') as string;
214
+ console.log("🚀 ~ extractProductsFromSources ~ description:", s)
215
+
216
+ return {
217
+ id: s.id,
218
+ name: name ?? s.content.split('\n')[0] ?? 'Unknown Product',
219
+ brand: brand,
220
+ price: price,
221
+ image: resolveImage(m),
222
+ link: resolveMetadataValue(m, 'link') as string,
223
+ description: description ? description : '' //description ?? s.content,
224
+ };
225
+ });
226
+ }
227
+
228
+ export function extractProductsFromContent(
229
+ content: string,
230
+ payloadText: string,
231
+ payloadCandidates: string[],
232
+ productsFromSources: Product[]
233
+ ): { productsFromContent: Product[]; cleanContent: string } {
234
+ const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
235
+ const products: Product[] = [];
236
+ let cleanContent = payloadText;
237
+
238
+ for (const candidate of payloadCandidates) {
239
+ try {
240
+ const data = JSON.parse(sanitizeJson(candidate));
241
+ const itemSet =
242
+ Array.isArray(data.data) ? data.data
243
+ : Array.isArray(data.items) ? data.items
244
+ : Array.isArray(data.products) ? data.products
245
+ : null;
246
+
247
+ if (
248
+ (data.type === 'products' || data.view === 'carousel' || data.view === 'table' || Array.isArray(data.products)) &&
249
+ itemSet
250
+ ) {
251
+ products.push(
252
+ ...itemSet.map((item: Record<string, unknown>) => ({
253
+ ...item,
254
+ image: (item.image as string) ?? resolveImage(item),
255
+ })) as Product[],
256
+ );
257
+ }
258
+ } catch {
259
+ // Ignore malformed product payloads
260
+ }
261
+ }
262
+
263
+ if (cleanContent.includes('"type": "products"') || cleanContent.includes('"type":"products"')) {
264
+ for (const match of Array.from(cleanContent.matchAll(jsonRegex))) {
265
+ try {
266
+ const data = JSON.parse(sanitizeJson(match[1]));
267
+ if (data.type === 'products' && Array.isArray(data.items)) {
268
+ products.push(
269
+ ...data.items.map((item: Record<string, unknown>) => ({
270
+ ...item,
271
+ image: (item.image as string) ?? resolveImage(item),
272
+ })) as Product[],
273
+ );
274
+ cleanContent = cleanContent.replace(match[0], '');
275
+ }
276
+ } catch {
277
+ // Malformed product block — skip silently
278
+ }
279
+ }
280
+ }
281
+
282
+ // Fallback: Scan for plain text bulleted products
283
+ // Requires a bullet or number at the start of the line to prevent matching plain text paragraphs.
284
+ const bulletRegex = /^[ \t]*(?:[•*-]|\d+\.)\s+(?:\*\*)?([^\n\-\$–—\(]+?)(?:\*\*)?(?:\s*\(?Price\s*[:\-–—]?\s*)?(?:\s*[:\-–—]\s*|\s+)\$?([\d,.]+)(?:\s*USD)?/gim;
285
+ const matches = Array.from(cleanContent.matchAll(bulletRegex));
286
+
287
+ if (matches.length >= 2) {
288
+ const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
289
+
290
+ for (const match of matches) {
291
+ let name = match[1]?.trim() || '';
292
+ name = name.replace(/\s*\(?Price\s*$/i, '').replace(/[:\-–—]\s*$/g, '').trim();
293
+ name = name.replace(/^\*\*|\*\*$/g, '').replace(/^\*|\*$/g, '').trim();
294
+
295
+ let price = match[2]?.trim() || '';
296
+ price = price.replace(/[,\s]+$/, '').trim();
297
+
298
+ if (name && price) {
299
+ const newProduct: Product = {
300
+ id: `text-prod-${name}-${price}`,
301
+ name,
302
+ price: `$${price}`,
303
+ };
304
+
305
+ const normName = normalize(name);
306
+ const sourceMatch = productsFromSources.find(s => {
307
+ const sn = normalize(s.name);
308
+ return sn.includes(normName) || normName.includes(sn);
309
+ });
310
+
311
+ if (sourceMatch) {
312
+ newProduct.image = sourceMatch.image;
313
+ newProduct.brand = sourceMatch.brand;
314
+ newProduct.link = sourceMatch.link;
315
+ newProduct.description = sourceMatch.description;
316
+ }
317
+
318
+ products.push(newProduct);
319
+ cleanContent = cleanContent.replace(match[0], `\n**${name}** — $${price}`);
320
+ }
321
+ }
322
+ cleanContent = cleanContent.replace(/\n{3,}/g, '\n\n').trim();
323
+ }
324
+
325
+ return { productsFromContent: products, cleanContent: cleanContent.trim() };
326
+ }
327
+
328
+ export function deduplicateProducts(
329
+ productsFromSources: Product[],
330
+ productsFromContent: Product[],
331
+ messageContent: string
332
+ ): Product[] {
333
+ if (productsFromContent.length > 0) return productsFromContent;
334
+
335
+ const seen = new Set<string>();
336
+ const contentLower = messageContent.toLowerCase();
337
+
338
+ return productsFromSources.filter(p => {
339
+ const id = String(p.id ?? p.name);
340
+ if (seen.has(id)) return false;
341
+ const mentioned =
342
+ contentLower.includes(p.name.toLowerCase()) ||
343
+ (p.brand ? contentLower.includes(p.brand.toLowerCase()) : false);
344
+ if (mentioned) { seen.add(id); return true; }
345
+ return false;
346
+ });
347
+ }
@@ -0,0 +1,129 @@
1
+ import { ILLMProvider } from '../llm/ILLMProvider';
2
+
3
+ export interface SchemaMap {
4
+ name?: string;
5
+ price?: string;
6
+ brand?: string;
7
+ image?: string;
8
+ stock?: string;
9
+ description?: string;
10
+ category?: string;
11
+ }
12
+
13
+ /**
14
+ * SchemaMapper — Uses AI to "train" the plugin on unknown database schemas.
15
+ * It maps arbitrary metadata keys to standard UI properties.
16
+ */
17
+ export class SchemaMapper {
18
+ private static cache = new Map<string, SchemaMap>();
19
+
20
+ /**
21
+ * Descriptions of standard UI properties to help the AI map fields accurately.
22
+ */
23
+ private static readonly TARGET_PROPERTIES: Record<string, string> = {
24
+ name: 'The primary title, name, or label of the item',
25
+ price: 'The numeric cost, price, MSRP, or amount',
26
+ brand: 'The manufacturer, vendor, brand, or maker',
27
+ image: 'The URL to an image, thumbnail, photo, or picture',
28
+ stock: 'The availability, inventory count, quantity, or stock level',
29
+ description: 'The detailed text content, summary, or body info',
30
+ category: 'The group, department, type, or category name',
31
+ };
32
+
33
+ /**
34
+ * Trains the plugin on a set of keys.
35
+ * This is done once per schema and cached.
36
+ */
37
+ static async train(
38
+ llm: ILLMProvider,
39
+ projectId: string,
40
+ keys: string[]
41
+ ): Promise<SchemaMap> {
42
+ const cacheKey = `${projectId}:${keys.sort().join(',')}`;
43
+ if (this.cache.has(cacheKey)) {
44
+ return this.cache.get(cacheKey)!;
45
+ }
46
+
47
+ console.log(`[SchemaMapper] 🧠 Training AI on new schema keys: ${keys.join(', ')}`);
48
+
49
+ // Dynamically build the property list for the prompt
50
+ const propertyList = Object.entries(this.TARGET_PROPERTIES)
51
+ .map(([prop, desc]) => `- ${prop} (${desc})`)
52
+ .join('\n');
53
+
54
+ const prompt = `
55
+ Given these metadata keys from a database: [${keys.join(', ')}]
56
+
57
+ Identify which keys best correspond to these standard UI properties:
58
+ ${propertyList}
59
+
60
+ Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
61
+ If no good match is found for a property, omit it.
62
+
63
+ Example:
64
+ {
65
+ "name": "Product_Title",
66
+ "price": "MSRP_USD",
67
+ "brand": "VendorName"
68
+ }
69
+ `;
70
+
71
+ try {
72
+ const response = await llm.chat(
73
+ [
74
+ { role: 'system', content: 'You are a database schema expert. You ONLY respond with valid JSON.' },
75
+ { role: 'user', content: prompt }
76
+ ],
77
+ ''
78
+ );
79
+
80
+ // Extract the first balanced JSON object from the response
81
+ const startIdx = response.indexOf('{');
82
+ if (startIdx !== -1) {
83
+ let braceCount = 0;
84
+ let endIdx = -1;
85
+
86
+ for (let i = startIdx; i < response.length; i++) {
87
+ if (response[i] === '{') braceCount++;
88
+ else if (response[i] === '}') {
89
+ braceCount--;
90
+ if (braceCount === 0) {
91
+ endIdx = i;
92
+ break;
93
+ }
94
+ }
95
+ }
96
+
97
+ if (endIdx !== -1) {
98
+ const jsonContent = response.substring(startIdx, endIdx + 1);
99
+ const cleanJson = this.sanitizeJson(jsonContent);
100
+ const mapping = JSON.parse(cleanJson) as SchemaMap;
101
+ this.cache.set(cacheKey, mapping);
102
+ return mapping;
103
+ }
104
+ }
105
+ } catch (error) {
106
+ console.warn('[SchemaMapper] AI training failed, falling back to heuristics:', error);
107
+ }
108
+
109
+ return {};
110
+ }
111
+
112
+ /**
113
+ * Forgiving JSON parser that fixes common AI formatting mistakes.
114
+ */
115
+ private static sanitizeJson(s: string): string {
116
+ return s
117
+ .replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '') // Remove comments
118
+ .replace(/[\u201C\u201D\u2018\u2019]/g, '"') // Replace smart quotes
119
+ .replace(/'/g, '"') // Replace single quotes
120
+ .replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":') // Add quotes to keys
121
+ .replace(/,\s*([}\]])/g, '$1') // Remove trailing commas
122
+ .trim();
123
+ }
124
+
125
+ static getCached(projectId: string, keys: string[]): SchemaMap | undefined {
126
+ const cacheKey = `${projectId}:${keys.sort().join(',')}`;
127
+ return this.cache.get(cacheKey);
128
+ }
129
+ }