@retrivora-ai/rag-engine 2.2.4 → 2.2.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.
@@ -13,13 +13,18 @@ export interface SchemaMap {
13
13
  /**
14
14
  * SchemaMapper — Uses AI to "train" the plugin on unknown database schemas.
15
15
  * It maps arbitrary metadata keys to standard UI properties.
16
+ *
17
+ * Design notes:
18
+ * - Bypasses ILLMProvider.chat() entirely to avoid injecting the full chat
19
+ * system prompt (which can exceed Groq's free-tier TPM limit).
20
+ * - Deduplicates concurrent in-flight training calls for the same schema.
21
+ * - Falls back to heuristics on any failure (non-blocking).
16
22
  */
17
23
  export class SchemaMapper {
18
24
  private static cache = new Map<string, SchemaMap>();
25
+ /** In-flight deduplication — prevents concurrent identical LLM calls. */
26
+ private static pending = new Map<string, Promise<SchemaMap>>();
19
27
 
20
- /**
21
- * Descriptions of standard UI properties to help the AI map fields accurately.
22
- */
23
28
  private static readonly TARGET_PROPERTIES: Record<string, string> = {
24
29
  name: 'The primary title, name, or label of the item',
25
30
  price: 'The numeric cost, price, MSRP, or amount',
@@ -32,74 +37,95 @@ export class SchemaMapper {
32
37
 
33
38
  /**
34
39
  * Trains the plugin on a set of keys.
35
- * This is done once per schema and cached.
40
+ * Results are cached in-process; concurrent calls for the same key set are
41
+ * deduplicated to a single LLM request.
36
42
  */
37
43
  static async train(
38
44
  llm: ILLMProvider,
39
45
  projectId: string,
40
46
  keys: string[]
41
47
  ): Promise<SchemaMap> {
42
- const cacheKey = `${projectId}:${keys.sort().join(',')}`;
48
+ const cacheKey = `${projectId}:${keys.slice().sort().join(',')}`;
49
+
50
+ // 1. Return from in-process cache (fast path)
43
51
  if (this.cache.has(cacheKey)) {
44
52
  return this.cache.get(cacheKey)!;
45
53
  }
46
54
 
47
- console.log(`[SchemaMapper] 🧠 Training AI on new schema keys: ${keys.join(', ')}`);
55
+ // 2. Deduplicate concurrent in-flight requests for the same schema
56
+ if (this.pending.has(cacheKey)) {
57
+ return this.pending.get(cacheKey)!;
58
+ }
59
+
60
+ const promise = this._doTrain(llm, cacheKey, keys);
61
+ this.pending.set(cacheKey, promise);
62
+ promise.finally(() => this.pending.delete(cacheKey));
63
+ return promise;
64
+ }
65
+
66
+ private static async _doTrain(
67
+ llm: ILLMProvider,
68
+ cacheKey: string,
69
+ keys: string[]
70
+ ): Promise<SchemaMap> {
71
+ console.log(`[SchemaMapper] 🧠 Training on new schema keys: ${keys.join(', ')}`);
48
72
 
49
- // Dynamically build the property list for the prompt
50
73
  const propertyList = Object.entries(this.TARGET_PROPERTIES)
51
74
  .map(([prop, desc]) => `- ${prop} (${desc})`)
52
75
  .join('\n');
53
76
 
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 valid JSON object where the keys are the UI properties and the values are the EXACT matching database keys from the list above.
61
- If no good match is found for a property, omit it.
62
-
63
- Example:
64
- {
65
- "name": "Title",
66
- "price": "Variant Price",
67
- "brand": "Vendor",
68
- "image": "Image Src",
69
- "stock": "Variant Inventory Qty"
70
- }
71
- `;
77
+ // Compact prompt no chat system prompt injected.
78
+ const messages = [
79
+ { role: 'system', content: 'You are a database schema expert. Respond ONLY with valid JSON.' },
80
+ {
81
+ role: 'user',
82
+ content:
83
+ `Keys: [${keys.join(', ')}]\n\nMap to:\n${propertyList}\n\n` +
84
+ `Return a JSON object like {"name":"Title","price":"Price",...}. Omit unmapped properties.`,
85
+ },
86
+ ];
72
87
 
73
88
  try {
74
- const response = await llm.chat(
75
- [
76
- { role: 'system', content: 'You are a database schema expert. You ONLY respond with valid JSON.' },
77
- { role: 'user', content: prompt }
78
- ],
79
- ''
80
- );
81
-
82
- // Extract the first balanced JSON object from the response
83
- const startIdx = response.indexOf('{');
84
- if (startIdx !== -1) {
85
- let braceCount = 0;
86
- let endIdx = -1;
87
-
88
- for (let i = startIdx; i < response.length; i++) {
89
- if (response[i] === '{') braceCount++;
90
- else if (response[i] === '}') {
91
- braceCount--;
92
- if (braceCount === 0) {
93
- endIdx = i;
94
- break;
95
- }
96
- }
89
+ // Use a direct fetch against the resolved base URL + auth headers,
90
+ // bypassing ILLMProvider.chat() which injects the full chat system prompt.
91
+ const baseUrl: string = (llm as any)?.baseUrl ?? '';
92
+ const apiKey: string = (llm as any)?.apiKey ?? '';
93
+ const model: string = (llm as any)?.model ?? 'llama-3.1-8b-instant';
94
+
95
+ let responseText: string | undefined;
96
+
97
+ if (baseUrl) {
98
+ const endpoint = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
99
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
100
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
101
+
102
+ const res = await fetch(endpoint, {
103
+ method: 'POST',
104
+ headers,
105
+ body: JSON.stringify({
106
+ model,
107
+ messages,
108
+ max_tokens: 256, // Only needs a small JSON object back
109
+ temperature: 0,
110
+ stream: false,
111
+ }),
112
+ signal: AbortSignal.timeout(8000),
113
+ });
114
+
115
+ if (res.ok) {
116
+ const data = await res.json();
117
+ responseText =
118
+ data?.choices?.[0]?.message?.content ??
119
+ data?.choices?.[0]?.delta?.content ??
120
+ undefined;
121
+ } else {
122
+ console.warn(`[SchemaMapper] LLM returned ${res.status}, falling back to heuristics.`);
97
123
  }
124
+ }
98
125
 
99
- if (endIdx !== -1) {
100
- const jsonContent = response.substring(startIdx, endIdx + 1);
101
- const cleanJson = this.sanitizeJson(jsonContent);
102
- const mapping = JSON.parse(cleanJson) as SchemaMap;
126
+ if (responseText) {
127
+ const mapping = this._parseJson(responseText);
128
+ if (mapping) {
103
129
  this.cache.set(cacheKey, mapping);
104
130
  return mapping;
105
131
  }
@@ -108,24 +134,43 @@ Example:
108
134
  console.warn('[SchemaMapper] AI training failed, falling back to heuristics:', error);
109
135
  }
110
136
 
137
+ // Store empty result so we don't keep retrying on the same schema
138
+ this.cache.set(cacheKey, {});
111
139
  return {};
112
140
  }
113
141
 
114
- /**
115
- * Forgiving JSON parser that fixes common AI formatting mistakes.
116
- */
117
- private static sanitizeJson(s: string): string {
142
+ private static _parseJson(text: string): SchemaMap | null {
143
+ const startIdx = text.indexOf('{');
144
+ if (startIdx === -1) return null;
145
+ let braceCount = 0;
146
+ let endIdx = -1;
147
+ for (let i = startIdx; i < text.length; i++) {
148
+ if (text[i] === '{') braceCount++;
149
+ else if (text[i] === '}') {
150
+ braceCount--;
151
+ if (braceCount === 0) { endIdx = i; break; }
152
+ }
153
+ }
154
+ if (endIdx === -1) return null;
155
+ try {
156
+ return JSON.parse(this._sanitizeJson(text.substring(startIdx, endIdx + 1))) as SchemaMap;
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+
162
+ private static _sanitizeJson(s: string): string {
118
163
  return s
119
- .replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '') // Remove comments
120
- .replace(/[\u201C\u201D\u2018\u2019]/g, '"') // Replace smart quotes
121
- .replace(/'/g, '"') // Replace single quotes
122
- .replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":') // Add quotes to keys
123
- .replace(/,\s*([}\]])/g, '$1') // Remove trailing commas
164
+ .replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, '')
165
+ .replace(/[\u201C\u201D\u2018\u2019]/g, '"')
166
+ .replace(/'/g, '"')
167
+ .replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":')
168
+ .replace(/,\s*([}\]])/g, '$1')
124
169
  .trim();
125
170
  }
126
171
 
127
172
  static getCached(projectId: string, keys: string[]): SchemaMap | undefined {
128
- const cacheKey = `${projectId}:${keys.sort().join(',')}`;
173
+ const cacheKey = `${projectId}:${keys.slice().sort().join(',')}`;
129
174
  return this.cache.get(cacheKey);
130
175
  }
131
176
  }
@@ -128,16 +128,16 @@ export class UITransformer {
128
128
  return this.createTextResponse('No data available', 'No relevant data found for your query.');
129
129
  }
130
130
 
131
- console.log('[UITransformer.transform] Processing retrievedData:', {
132
- userQuery,
133
- retrievedCount: retrievedData.length,
134
- sample: retrievedData.slice(0, 2).map((item) => ({
135
- id: item?.id,
136
- contentType: typeof item?.content,
137
- contentSnippet: String(item?.content ?? '').substring(0, 150),
138
- metadataKeys: Object.keys(item?.metadata || {}),
139
- })),
140
- });
131
+ // console.log('[UITransformer.transform] Processing retrievedData:', {
132
+ // userQuery,
133
+ // retrievedCount: retrievedData.length,
134
+ // sample: retrievedData.slice(0, 2).map((item) => ({
135
+ // id: item?.id,
136
+ // contentType: typeof item?.content,
137
+ // contentSnippet: String(item?.content ?? '').substring(0, 150),
138
+ // metadataKeys: Object.keys(item?.metadata || {}),
139
+ // })),
140
+ // });
141
141
 
142
142
  const resolvedIntent = intent ?? this.detectIntentHeuristic(userQuery);
143
143
 
@@ -642,16 +642,16 @@ RULES:
642
642
  const fallbackData: BarChartData[] = barData.length > 0
643
643
  ? barData
644
644
  : data.slice(0, 12).map((item, index) => {
645
- const meta = item.metadata || {};
646
- const label = String(
647
- this.getDynamicVal(meta, 'name') ??
648
- meta.label ??
649
- meta.title ??
650
- `Result ${index + 1}`,
651
- );
652
- const value = this.extractNumericValue(meta) ?? item.score ?? 0;
653
- return { category: label, value: Number(value) };
654
- });
645
+ const meta = item.metadata || {};
646
+ const label = String(
647
+ this.getDynamicVal(meta, 'name') ??
648
+ meta.label ??
649
+ meta.title ??
650
+ `Result ${index + 1}`,
651
+ );
652
+ const value = this.extractNumericValue(meta) ?? item.score ?? 0;
653
+ return { category: label, value: Number(value) };
654
+ });
655
655
 
656
656
  return {
657
657
  type: horizontal ? 'horizontal_bar' : 'bar_chart',
@@ -724,8 +724,8 @@ RULES:
724
724
  const numericField = this.selectNumericField(profile, query);
725
725
  const values = numericField
726
726
  ? profile.records
727
- .map(record => this.toFiniteNumber(record.fields[numericField.key]))
728
- .filter((value): value is number => value !== null)
727
+ .map(record => this.toFiniteNumber(record.fields[numericField.key]))
728
+ .filter((value): value is number => value !== null)
729
729
  : [];
730
730
 
731
731
  const value = operation === 'count' || values.length === 0