@retrivora-ai/rag-engine 1.7.3 → 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.
@@ -101,72 +101,198 @@ export class Pipeline {
101
101
 
102
102
  // Augment system prompt with a Universal Visualization Protocol
103
103
  // We use a unique marker to ensure this is only injected once but is ALWAYS present.
104
- const CHART_MARKER = '<!-- UI_PROTOCOL_V6 -->';
105
- const chartInstruction = `\n\n${CHART_MARKER}
106
- ### UNIVERSAL UI PROTOCOL
107
- When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
108
-
109
- 1. VIEW SELECTION
110
- - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
111
- - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
112
- - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
113
-
114
- 2. REQUIRED JSON SHAPE
115
- {
116
- "view": "chart" | "carousel" | "table",
117
- "title": "Short heading",
118
- "description": "One sentence describing the view",
119
- "chartType": "pie" | "bar" | "line", // Required if view is "chart"
120
- "xAxisKey": "label",
121
- "dataKeys": ["value"],
122
- "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
123
- "insights": ["Short takeaway 1", "Short takeaway 2"],
124
- "data": []
125
- }
126
-
127
- 3. DATA RULES
128
- - Build the UI payload from retrieved context only. Do not invent products or values.
129
- - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
130
- - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
131
- - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
132
-
133
- 4. FORMAT RULES
134
- - Return valid JSON inside the \`\`\`ui\`\`\` block.
135
- - Put natural language explanation before or after the block, not inside.
136
-
137
- 5. EXAMPLES
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
105
+ const chartInstruction = `
106
+
107
+ ${CHART_MARKER}
108
+ ### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
109
+
110
+ You are responsible for returning a SINGLE structured UI block when visualization is required.
111
+
112
+ ---
113
+
114
+ ## 1. INTENT CLASSIFICATION (MANDATORY)
115
+
116
+ Classify the query into ONE of the following intents:
117
+
118
+ - "explore" user wants to browse items (e.g., "show me products")
119
+ - "analyze" user wants aggregation/distribution (e.g., "distribution of products")
120
+ - "compare" → user wants side-by-side comparison
121
+ - "detail" → user wants structured/tabular data
122
+
123
+ ---
124
+
125
+ ## 2. VIEW MAPPING (STRICT)
126
+
127
+ | Intent | View |
128
+ |----------|-----------|
129
+ | explore | carousel |
130
+ | analyze | chart |
131
+ | compare | table |
132
+ | detail | table |
133
+
134
+ ⚠️ Override Rules:
135
+ - If query contains "distribution", "percentage", "ratio" ALWAYS chart
136
+ - If query contains "show products", "list items" → ALWAYS carousel
137
+ - NEVER mix views
138
+
139
+ ---
140
+
141
+ ## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
142
+
143
+ \`\`\`ui
144
+ {
145
+ "view": "carousel" | "chart" | "table",
146
+ "title": "string",
147
+ "description": "string",
148
+ "metadata": {
149
+ "intent": "explore" | "analyze" | "compare" | "detail",
150
+ "confidence": 0.0-1.0
151
+ },
152
+
153
+ // CHART ONLY
154
+ "chart": {
155
+ "type": "pie" | "bar" | "line",
156
+ "xKey": "label",
157
+ "yKeys": ["value"],
158
+ "data": []
159
+ },
160
+
161
+ // TABLE ONLY
162
+ "table": {
163
+ "columns": [],
164
+ "rows": []
165
+ },
166
+
167
+ // CAROUSEL ONLY
168
+ "carousel": {
169
+ "items": []
170
+ },
171
+
172
+ "insights": []
173
+ }
174
+ \`\`\`
175
+
176
+ ---
177
+
178
+ ## 4. DATA CONTRACT (CRITICAL)
179
+
180
+ ### 🔹 CAROUSEL ITEM FORMAT
181
+ {
182
+ "id": "string",
183
+ "name": "string",
184
+ "brand": "string",
185
+ "price": number,
186
+ "image": "url",
187
+ "link": "url",
188
+ "inStock": boolean
189
+ }
190
+
191
+ ### 🔹 CHART DATA FORMAT
192
+ {
193
+ "label": "string",
194
+ "value": number,
195
+ "inStock": boolean (optional)
196
+ }
197
+
198
+ ### 🔹 TABLE FORMAT
199
+ - columns MUST match row keys
200
+ - rows MUST be flat objects
201
+
202
+ ---
203
+
204
+ ## 5. DATA RULES
205
+
206
+ - NEVER hallucinate fields
207
+ - ONLY use retrieved vector DB data
208
+ - NO placeholders ("...", "N/A")
209
+ - If image/link missing → REMOVE FIELD
210
+ - Aggregate BEFORE rendering chart
211
+
212
+ ---
213
+
214
+ ## 6. SMART RULES (IMPORTANT)
215
+
216
+ - If user asks BOTH:
217
+ "show products AND distribution"
218
+ → PRIORITIZE "explore" → carousel
219
+
220
+ - If dataset size > 20:
221
+ → aggregate → chart or table
222
+
223
+ ---
224
+
225
+ ## 7. OUTPUT RULES
226
+
227
+ - ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block
228
+ - JSON must be VALID
229
+ - No explanation inside block
230
+
231
+ ---
232
+
233
+ ## 8. EXAMPLES
234
+
138
235
  User: "show me beauty products"
139
- Assistant:
236
+
140
237
  \`\`\`ui
141
238
  {
142
239
  "view": "carousel",
143
240
  "title": "Beauty Products",
144
- "description": "Browse our latest skincare and makeup collection.",
145
- "data": [
146
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
147
- { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
148
- ]
241
+ "description": "Browse available beauty items",
242
+ "metadata": {
243
+ "intent": "explore",
244
+ "confidence": 0.95
245
+ },
246
+ "carousel": {
247
+ "items": [
248
+ {
249
+ "id": "1",
250
+ "name": "Mascara",
251
+ "brand": "Essence",
252
+ "price": 9.99,
253
+ "image": "https://example.com/mascara.jpg",
254
+ "link": "https://example.com/p1",
255
+ "inStock": true
256
+ }
257
+ ]
258
+ },
259
+ "insights": ["Most products are affordable"]
149
260
  }
150
261
  \`\`\`
151
262
 
152
- User: "breakdown of products by category"
153
- Assistant:
263
+ ---
264
+
265
+ User: "distribution of products across categories"
266
+
154
267
  \`\`\`ui
155
268
  {
156
269
  "view": "chart",
157
- "title": "Category Breakdown",
158
- "chartType": "pie",
159
- "xAxisKey": "label",
160
- "dataKeys": ["value"],
161
- "data": [
162
- { "label": "Beauty", "value": 15 },
163
- { "label": "Fragrances", "value": 8 }
164
- ]
270
+ "title": "Product Distribution",
271
+ "description": "Category-wise product distribution",
272
+ "metadata": {
273
+ "intent": "analyze",
274
+ "confidence": 0.98
275
+ },
276
+ "chart": {
277
+ "type": "pie",
278
+ "xKey": "label",
279
+ "yKeys": ["value"],
280
+ "data": [
281
+ { "label": "Beauty", "value": 15 },
282
+ { "label": "Electronics", "value": 10 }
283
+ ]
284
+ },
285
+ "insights": ["Beauty dominates inventory"]
165
286
  }
166
- \`\`\``;
167
-
287
+ \`\`\`
288
+ `;
168
289
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
169
- this.config.llm.systemPrompt = (this.config.llm.systemPrompt || '') + chartInstruction;
290
+ // Clean up ANY existing protocol markers (V1-V7) to avoid prompt bloat
291
+ let cleanPrompt = this.config.llm.systemPrompt || '';
292
+ cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, '');
293
+ cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, '');
294
+
295
+ this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
170
296
  }
171
297
 
172
298
  console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
@@ -128,7 +128,10 @@ export class GeminiProvider implements ILLMProvider {
128
128
  },
129
129
  });
130
130
 
131
- return response.text ?? '';
131
+ const text = typeof (response as { text: unknown }).text === 'function'
132
+ ? (response as { text: () => string }).text()
133
+ : (response.text ?? '');
134
+ return text;
132
135
  }
133
136
 
134
137
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
@@ -162,7 +165,9 @@ export class GeminiProvider implements ILLMProvider {
162
165
  }
163
166
 
164
167
  for await (const chunk of stream) {
165
- const text = chunk.text;
168
+ const text = typeof (chunk as { text: unknown }).text === 'function'
169
+ ? (chunk as { text: () => string }).text()
170
+ : (chunk as { text?: string }).text;
166
171
  if (text) yield text;
167
172
  }
168
173
  }
@@ -151,12 +151,22 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
151
151
  console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
152
152
  console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(', ')}]`);
153
153
 
154
+ // Helper to clean conversational filler for better keyword matching
155
+ const cleanKeywordQuery = (text: string) => {
156
+ return text
157
+ .toLowerCase()
158
+ .replace(/\b(show|me|the|product|cards|of|category|find|list|browse|what|are|display)\b/g, '')
159
+ .trim()
160
+ .replace(/\s+/g, ' & '); // Join remaining words with AND for tsquery
161
+ };
162
+
154
163
  const queryPromises = this.tables.map(async (table) => {
155
164
  try {
156
165
  let sqlQuery = '';
157
166
  let params: unknown[] = [];
158
167
 
159
168
  if (queryText) {
169
+ const cleanedText = cleanKeywordQuery(queryText) || queryText;
160
170
  const hasEntityHints = entityHints.length > 0;
161
171
  const exactNameScoreExpr = hasEntityHints
162
172
  ? `+ (
@@ -173,13 +183,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
173
183
  sqlQuery = `
174
184
  SELECT *,
175
185
  (1 - (embedding <=> $1::vector)) AS vector_score,
176
- COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) AS keyword_score,
177
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), NULLIF(REPLACE(plainto_tsquery('english', $2)::text, '&', '|'), '')::tsquery), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
186
+ COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
187
+ ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
178
188
  FROM "${table}" t
179
189
  ORDER BY hybrid_score DESC
180
190
  LIMIT 50
181
191
  `;
182
- params = [vectorLiteral, queryText];
192
+ params = [vectorLiteral, cleanedText];
183
193
  } else {
184
194
  // Fallback to Vector-only Search
185
195
  sqlQuery = `