@retrivora-ai/rag-engine 1.8.8 → 1.9.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.8.8",
3
+ "version": "1.9.0",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  "scripts": {
75
75
  "dev": "next dev",
76
76
  "build": "next build",
77
- "build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/compiled.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --inject-style --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex && cp src/compiled.css dist/index.css && rm src/compiled.css",
77
+ "build:pkg": "tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex,./index.css && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
78
78
  "start": "next start",
79
79
  "lint": "eslint",
80
80
  "prepublishOnly": "npm run build:pkg"
@@ -44,8 +44,6 @@ export interface VectorDBConfig {
44
44
  * - searchFields?: string[] | string // optional override for which columns receive exact-match boosts
45
45
  */
46
46
  options: Record<string, unknown>;
47
- /** Optional distance metric (defaults to cosine if not specified and supported by provider) */
48
- distanceMetric?: 'cosine' | 'euclidean' | 'dotproduct';
49
47
  }
50
48
 
51
49
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * This entry point is CLIENT-SAFE and can be imported in React Client Components.
4
4
  */
5
5
 
6
- import './compiled.css';
6
+ import './index.css';
7
7
 
8
8
  // ── React Components ─────────────────────────────────────────
9
9
  export { ChatWidget } from './components/ChatWidget';
@@ -41,13 +41,9 @@ export class ChromaDBProvider extends BaseVectorProvider {
41
41
  } catch (err) {
42
42
  if (axios.isAxiosError(err) && err.response?.status === 404) {
43
43
  // Create the collection if it doesn't exist
44
- const metric = this.config.distanceMetric || 'cosine';
45
- const space = metric === 'euclidean' ? 'l2' : metric === 'dotproduct' ? 'ip' : 'cosine';
46
-
47
44
  console.log(`[ChromaDBProvider] ⏳ Collection "${this.indexName}" not found. Creating...`);
48
45
  const { data } = await this.http.post('/api/v1/collections', {
49
46
  name: this.indexName,
50
- metadata: { "hnsw:space": space }
51
47
  });
52
48
  this.collectionId = data.id;
53
49
  console.log(`[ChromaDBProvider] ✅ Created collection "${this.indexName}" (id: ${this.collectionId})`);
@@ -66,13 +66,10 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
66
66
  embedding VECTOR(${this.dimensions})
67
67
  )
68
68
  `);
69
- const metric = this.config.distanceMetric || 'cosine';
70
- const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
71
-
72
69
  await client.query(`
73
70
  CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
74
71
  ON ${this.uploadTable}
75
- USING hnsw (embedding ${indexOps})
72
+ USING hnsw (embedding vector_cosine_ops)
76
73
  `);
77
74
 
78
75
  const res = await client.query(`
@@ -150,13 +147,10 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
150
147
  await client.query(createTableSql);
151
148
 
152
149
  // Create Index
153
- const metric = this.config.distanceMetric || 'cosine';
154
- const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
155
-
156
150
  await client.query(`
157
151
  CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
158
152
  ON "${tableName}"
159
- USING hnsw (embedding ${indexOps})
153
+ USING hnsw (embedding vector_cosine_ops)
160
154
  `);
161
155
 
162
156
  // Ensure this new table is added to our search scope
@@ -280,13 +274,6 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
280
274
  let sqlQuery = '';
281
275
  let params: unknown[] = [];
282
276
 
283
- const metric = this.config.distanceMetric || 'cosine';
284
- const scoreExpr = metric === 'euclidean'
285
- ? `1 / (1 + (embedding <-> $1::vector))`
286
- : metric === 'dotproduct'
287
- ? `(embedding <#> $1::vector) * -1`
288
- : `1 - (embedding <=> $1::vector)`;
289
-
290
277
  if (queryText) {
291
278
  const hasEntityHints = entityHints.length > 0;
292
279
  const exactNameScoreExpr = hasEntityHints
@@ -303,9 +290,9 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
303
290
  // Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
304
291
  sqlQuery = `
305
292
  SELECT *,
306
- (${scoreExpr}) AS vector_score,
293
+ (1 - (embedding <=> $1::vector)) AS vector_score,
307
294
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
308
- ((${scoreExpr}) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
295
+ ((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
309
296
  FROM "${table}" t
310
297
  ORDER BY hybrid_score DESC
311
298
  LIMIT 50
@@ -315,7 +302,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
315
302
  // Fallback to Vector-only Search
316
303
  sqlQuery = `
317
304
  SELECT *,
318
- (${scoreExpr}) AS hybrid_score
305
+ (1 - (embedding <=> $1::vector)) AS hybrid_score
319
306
  FROM "${table}" t
320
307
  ORDER BY hybrid_score DESC
321
308
  LIMIT 50
@@ -97,13 +97,10 @@ export class PostgreSQLProvider extends BaseVectorProvider {
97
97
  embedding VECTOR(${this.dimensions})
98
98
  )
99
99
  `);
100
- const metric = this.config.distanceMetric || 'cosine';
101
- const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
102
-
103
100
  await client.query(`
104
101
  CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
105
102
  ON ${this.tableName}
106
- USING hnsw (embedding ${indexOps})
103
+ USING hnsw (embedding vector_cosine_ops)
107
104
  `);
108
105
  } finally {
109
106
  client.release();
@@ -185,19 +182,11 @@ export class PostgreSQLProvider extends BaseVectorProvider {
185
182
  const efSearch = (this.config.options.efSearch as number) || Math.max(topK * 10, 40);
186
183
  await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
187
184
 
188
- const metric = this.config.distanceMetric || 'cosine';
189
- const queryOp = metric === 'euclidean' ? '<->' : metric === 'dotproduct' ? '<#>' : '<=>';
190
- const scoreExpr = metric === 'euclidean'
191
- ? `1 / (1 + (embedding <-> $1::vector))`
192
- : metric === 'dotproduct'
193
- ? `(embedding <#> $1::vector) * -1`
194
- : `1 - (embedding <=> $1::vector)`;
195
-
196
185
  const result = await client.query(
197
- `SELECT id, content, metadata, ${scoreExpr} AS score
186
+ `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
198
187
  FROM ${this.tableName}
199
188
  ${whereClause}
200
- ORDER BY embedding ${queryOp} $1::vector
189
+ ORDER BY embedding <=> $1::vector
201
190
  LIMIT $2`,
202
191
  params
203
192
  );
@@ -120,14 +120,11 @@ export class QdrantProvider extends BaseVectorProvider {
120
120
  const opts = this.config.options as Record<string, unknown>;
121
121
  const dimensionsForCreate = (opts.dimensions as number) || 1536;
122
122
 
123
- const metric = this.config.distanceMetric || 'cosine';
124
- const distance = metric === 'euclidean' ? 'Euclid' : metric === 'dotproduct' ? 'Dot' : 'Cosine';
125
-
126
123
  console.log(`[QdrantProvider] ⏳ Creating collection "${this.indexName}" (dimensions: ${dimensionsForCreate})...`);
127
124
  await this.http.put(`/collections/${this.indexName}`, {
128
125
  vectors: {
129
126
  size: dimensionsForCreate,
130
- distance: distance,
127
+ distance: 'Cosine',
131
128
  },
132
129
  });
133
130
  console.log(`[QdrantProvider] ✅ Created collection "${this.indexName}"`);
@@ -29,8 +29,7 @@ export class UITransformer {
29
29
  ? retrievedData.filter(item => this.determineStockStatus(item))
30
30
  : retrievedData;
31
31
 
32
- const groupByField = this.determineGroupByField(userQuery, filteredData);
33
- const categories = this.detectCategories(filteredData, groupByField);
32
+ const categories = this.detectCategories(filteredData);
34
33
  const hasProducts = filteredData.some(item => this.isProductData(item));
35
34
  const isTimeSeries = filteredData.some(item => this.isTimeSeriesData(item));
36
35
  const isTrendQuery = this.isTrendQuery(userQuery);
@@ -40,16 +39,14 @@ export class UITransformer {
40
39
  return this.transformToLineChart(filteredData);
41
40
  }
42
41
 
43
- const explicitChartRequest = this.isExplicitChartQuery(userQuery);
44
-
45
42
  // 2. High Priority: Products (Always prefer carousel for products unless it's a specific breakdown request)
46
- if (hasProducts && !explicitChartRequest && !this.shouldShowCategoryChart(userQuery)) {
43
+ if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
47
44
  return this.transformToProductCarousel(filteredData, config, trainedSchema);
48
45
  }
49
46
 
50
47
  // 3. Category breakdowns
51
- if (explicitChartRequest || (categories.length > 1 && this.shouldShowCategoryChart(userQuery))) {
52
- return this.transformToPieChart(filteredData, groupByField);
48
+ if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
49
+ return this.transformToPieChart(filteredData);
53
50
  }
54
51
 
55
52
  // 4. Secondary: Products as carousel if not already handled
@@ -90,16 +87,13 @@ export class UITransformer {
90
87
  * Transform data to pie chart format
91
88
  */
92
89
  private static transformToPieChart(
93
- data: VectorMatch[],
94
- groupByField: string
90
+ data: VectorMatch[]
95
91
  ): UITransformationResponse {
96
- let categories = this.detectCategories(data, groupByField);
97
- if (categories.length === 0) categories = ['All Data'];
98
-
99
- const categoryData = this.aggregateByCategory(data, categories, groupByField);
92
+ const categories = this.detectCategories(data);
93
+ const categoryData = this.aggregateByCategory(data, categories);
100
94
 
101
95
  const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => {
102
- const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data, groupByField);
96
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
103
97
  return {
104
98
  label,
105
99
  value: count as number,
@@ -110,7 +104,7 @@ export class UITransformer {
110
104
 
111
105
  return {
112
106
  type: 'pie_chart',
113
- title: `Distribution by ${groupByField.charAt(0).toUpperCase() + groupByField.slice(1)}`,
107
+ title: 'Distribution by Category',
114
108
  description: `Showing breakdown across ${categories.length} categories`,
115
109
  data: pieData,
116
110
  };
@@ -377,16 +371,11 @@ export class UITransformer {
377
371
  return hasTimeKeyword || hasValidDateValue;
378
372
  }
379
373
 
380
- private static isExplicitChartQuery(query: string): boolean {
381
- const normalized = query.toLowerCase();
382
- return normalized.includes('pie chart') ||
383
- normalized.includes('piechart') ||
384
- normalized.includes('bar chart') ||
385
- normalized.includes('barchart') ||
386
- normalized.includes('radar chart');
387
- }
374
+ private static shouldShowCategoryChart(query: string, categories: string[]): boolean {
375
+ if (categories.length < 2) {
376
+ return false;
377
+ }
388
378
 
389
- private static shouldShowCategoryChart(query: string): boolean {
390
379
  const normalized = query.toLowerCase();
391
380
  const chartKeywords = [
392
381
  'distribution',
@@ -400,10 +389,6 @@ export class UITransformer {
400
389
  'split',
401
390
  'category breakdown',
402
391
  'category distribution',
403
- 'pie chart',
404
- 'piechart',
405
- 'bar chart',
406
- 'barchart'
407
392
  ];
408
393
 
409
394
  return chartKeywords.some(keyword => normalized.includes(keyword));
@@ -516,57 +501,38 @@ export class UITransformer {
516
501
  }
517
502
 
518
503
  /**
519
- * Helper: Dynamically determine what field to group by based on user query
520
- */
521
- private static determineGroupByField(query: string, data: VectorMatch[]): string {
522
- const normalized = query.toLowerCase();
523
- const match = normalized.match(/(?:by|across|per|based on)\s+([a-zA-Z]+)/i);
524
-
525
- let targetField = 'category';
526
- if (match && match[1]) {
527
- let field = match[1].toLowerCase();
528
- // Un-pluralize common words to match schema keys
529
- if (field.endsWith('ies')) field = field.replace(/ies$/, 'y');
530
- else if (field.endsWith('s') && field !== 'status') field = field.slice(0, -1);
531
-
532
- const stopWords = ['the', 'all', 'different', 'various', 'some', 'these', 'those'];
533
- if (!stopWords.includes(field)) {
534
- targetField = field;
535
- }
536
- }
537
-
538
- // Verify field exists, or fallback
539
- const hasTargetField = data.some(d => d.metadata && d.metadata[targetField] !== undefined);
540
- if (!hasTargetField && targetField === 'category') {
541
- if (data.some(d => d.metadata && d.metadata['type'] !== undefined)) return 'type';
542
- if (data.some(d => d.metadata && d.metadata['brand'] !== undefined)) return 'brand';
543
- }
544
-
545
- return targetField;
546
- }
547
-
548
- /**
549
- * Helper: Detect grouping values dynamically
504
+ * Helper: Detect categories in data
550
505
  */
551
- private static detectCategories(data: VectorMatch[], groupByField: string): string[] {
506
+ private static detectCategories(data: VectorMatch[]): string[] {
552
507
  const categories = new Set<string>();
553
508
 
554
509
  data.forEach(item => {
555
510
  const meta = item.metadata || {};
556
511
 
557
- if (meta[groupByField] !== undefined) {
558
- const val = meta[groupByField];
559
- if (Array.isArray(val)) val.forEach(v => categories.add(String(v)));
560
- else categories.add(String(val));
561
- } else if (groupByField === 'category') {
562
- // Fallbacks for legacy 'category' extraction
563
- if (meta.type) categories.add(String(meta.type));
564
- if (meta.tag) {
565
- const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
566
- tags.forEach(t => categories.add(String(t)));
567
- }
568
- const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
569
- if (categoryMatch) categories.add(categoryMatch[1].trim());
512
+ // Check for category field
513
+ if (meta.category) {
514
+ categories.add(String(meta.category));
515
+ }
516
+ if (meta.type) {
517
+ categories.add(String(meta.type));
518
+ }
519
+ if (meta.tag) {
520
+ const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
521
+ tags.forEach(t => categories.add(String(t)));
522
+ }
523
+
524
+ // Parse category strings from content lines like "Shoes & Footwear: • Product"
525
+ const contentCategories = Array.from(new Set(
526
+ Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm))
527
+ .map(match => match[1].trim())
528
+ .filter(Boolean)
529
+ ));
530
+ contentCategories.forEach((category) => categories.add(category));
531
+
532
+ // Parse from structured category hints in content
533
+ const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
534
+ if (categoryMatch) {
535
+ categories.add(categoryMatch[1].trim());
570
536
  }
571
537
  });
572
538
 
@@ -574,12 +540,11 @@ export class UITransformer {
574
540
  }
575
541
 
576
542
  /**
577
- * Helper: Aggregate data dynamically
543
+ * Helper: Aggregate data by category
578
544
  */
579
545
  private static aggregateByCategory(
580
546
  data: VectorMatch[],
581
- categories: string[],
582
- groupByField: string
547
+ categories: string[]
583
548
  ): Record<string, number> {
584
549
  const result: Record<string, number> = {};
585
550
 
@@ -589,13 +554,7 @@ export class UITransformer {
589
554
 
590
555
  data.forEach(item => {
591
556
  const meta = item.metadata || {};
592
- let itemCategory = 'Other';
593
-
594
- if (meta[groupByField] !== undefined) {
595
- itemCategory = String(meta[groupByField]);
596
- } else if (groupByField === 'category' && meta.type) {
597
- itemCategory = String(meta.type);
598
- }
557
+ const itemCategory = (meta.category || meta.type || 'Other') as string;
599
558
 
600
559
  if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
601
560
  result[itemCategory]++;
@@ -656,21 +615,15 @@ export class UITransformer {
656
615
  }
657
616
 
658
617
  /**
659
- * Helper: Calculate stock counts dynamically
618
+ * Helper: Calculate stock counts by category
660
619
  */
661
- private static calculateStockCounts(category: string, data: VectorMatch[], groupByField: string): { inStockCount: number; outOfStockCount: number } {
620
+ private static calculateStockCounts(category: string, data: VectorMatch[]): { inStockCount: number; outOfStockCount: number } {
662
621
  let inStock = 0;
663
622
  let outOfStock = 0;
664
623
 
665
624
  data.forEach(d => {
666
625
  const meta = d.metadata || {};
667
- let itemCategory = 'Other';
668
- if (meta[groupByField] !== undefined) {
669
- itemCategory = String(meta[groupByField]);
670
- } else if (groupByField === 'category' && meta.type) {
671
- itemCategory = String(meta.type);
672
- }
673
-
626
+ const itemCategory = (meta.category || meta.type || 'Other') as string;
674
627
  if (itemCategory === category) {
675
628
  if (this.determineStockStatus(d)) {
676
629
  inStock++;
@@ -819,13 +772,12 @@ DATA SHAPE per type:
819
772
  - text: { "content": "<prose answer>" }
820
773
 
821
774
  DECISION RULES (follow strictly):
822
- 1. IF the user explicitly asks for a "pie chart", "bar chart", or "table", you MUST output that exact type. Do not deviate from their request.
823
- 2. bar_chart comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
824
- 3. line_chart trends or changes over time (dates, months, years, sequential events)
825
- 4. pie_chart proportional breakdown or percentage distribution
826
- 5. radar_chart comparing 2 or more products across MULTIPLE attributes.
827
- 6. table multi-field structured records where each item has ≥ 3 attributes
828
- 7. text → conversational or free-form answers where no chart adds value
775
+ 1. bar_chart → comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
776
+ 2. line_chart trends or changes over time (dates, months, years, sequential events)
777
+ 3. pie_chart proportional breakdown or percentage distribution
778
+ 4. radar_chart comparing 2 or more products across MULTIPLE attributes (e.g. comparing features, ratings, or characteristics of 2 specific products). If the user asks to "compare" products and the data contains multiple dimensions or ratings for them, you MUST use this.
779
+ 5. table multi-field structured records where each item has ≥ 3 attributes
780
+ 6. text conversational or free-form answers where no chart adds value
829
781
 
830
782
  IMPORTANT:
831
783
  - Aggregate numeric values from the raw data — do not pass raw object arrays as data.