@retrivora-ai/rag-engine 1.9.1 → 1.9.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.
package/dist/server.mjs CHANGED
@@ -519,6 +519,82 @@ var init_PostgreSQLProvider = __esm({
519
519
  }
520
520
  });
521
521
 
522
+ // src/utils/synonyms.ts
523
+ function resolveMetadataValue(meta, uiKey) {
524
+ var _a;
525
+ const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
526
+ const keys = Object.keys(meta);
527
+ const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
528
+ let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
529
+ if (match !== void 0) return meta[match];
530
+ match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
531
+ if (match !== void 0) return meta[match];
532
+ const isBlacklisted = (kl) => {
533
+ return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
534
+ };
535
+ match = keys.find((k) => {
536
+ const kl = k.toLowerCase();
537
+ if (isBlacklisted(kl)) return false;
538
+ return kl.includes(uiKey.toLowerCase());
539
+ });
540
+ if (match !== void 0) return meta[match];
541
+ match = keys.find((k) => {
542
+ const kl = k.toLowerCase();
543
+ if (isBlacklisted(kl)) return false;
544
+ return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
545
+ });
546
+ return match !== void 0 ? meta[match] : void 0;
547
+ }
548
+ var FIELD_SYNONYMS;
549
+ var init_synonyms = __esm({
550
+ "src/utils/synonyms.ts"() {
551
+ "use strict";
552
+ FIELD_SYNONYMS = {
553
+ name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
554
+ price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd", "variant price"],
555
+ brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
556
+ image: [
557
+ "imageUrl",
558
+ "thumbnail",
559
+ "img",
560
+ "url",
561
+ "photo",
562
+ "picture",
563
+ "media",
564
+ "image_url",
565
+ "main_image",
566
+ "product_image",
567
+ "thumb",
568
+ "image src",
569
+ "variant image"
570
+ ],
571
+ stock: [
572
+ "inventory",
573
+ "quantity",
574
+ "count",
575
+ "availability",
576
+ "stock_level",
577
+ "inStock",
578
+ "is_available",
579
+ "in stock",
580
+ "status",
581
+ "variant inventory qty"
582
+ ],
583
+ category: [
584
+ "product_category",
585
+ "product category",
586
+ "category_name",
587
+ "category name",
588
+ "department",
589
+ "collection",
590
+ "type"
591
+ ],
592
+ description: ["summary", "content", "body", "text", "info", "details", "body (html)", "seo description"],
593
+ link: ["url", "href", "product_url", "page_url", "link"]
594
+ };
595
+ }
596
+ });
597
+
522
598
  // src/providers/vectordb/MultiTablePostgresProvider.ts
523
599
  var MultiTablePostgresProvider_exports = {};
524
600
  __export(MultiTablePostgresProvider_exports, {
@@ -530,6 +606,7 @@ var init_MultiTablePostgresProvider = __esm({
530
606
  "src/providers/vectordb/MultiTablePostgresProvider.ts"() {
531
607
  "use strict";
532
608
  init_BaseVectorProvider();
609
+ init_synonyms();
533
610
  MultiTablePostgresProvider = class extends BaseVectorProvider {
534
611
  constructor(config) {
535
612
  var _a, _b, _c;
@@ -610,8 +687,13 @@ var init_MultiTablePostgresProvider = __esm({
610
687
  }
611
688
  tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
612
689
  const firstMeta = fileDocs[0].metadata || {};
613
- const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding"];
614
- const csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
690
+ let csvHeaders = [];
691
+ if (Array.isArray(firstMeta.csvHeaders)) {
692
+ csvHeaders = firstMeta.csvHeaders;
693
+ } else {
694
+ const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding", "docId", "docid", "chunkId", "chunkid", "csvHeaders", "csvheaders"];
695
+ csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
696
+ }
615
697
  const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
616
698
  const createTableSql = `
617
699
  CREATE TABLE IF NOT EXISTS "${tableName}" (
@@ -704,10 +786,26 @@ var init_MultiTablePostgresProvider = __esm({
704
786
  };
705
787
  const dynamicKeywordQuery = getDynamicKeywordQuery();
706
788
  const tableLimit = Math.max(topK, 50);
789
+ const metadataFilters = _filter == null ? void 0 : _filter.metadata;
707
790
  const queryPromises = this.tables.map(async (table) => {
708
791
  try {
709
792
  let sqlQuery = "";
710
793
  let params = [];
794
+ let whereClause = "";
795
+ const filterParams = [];
796
+ if (metadataFilters && Object.keys(metadataFilters).length > 0) {
797
+ const conditions = Object.entries(metadataFilters).map(([key, val]) => {
798
+ var _a3;
799
+ filterParams.push(String(val));
800
+ const baseOffset = queryText && dynamicKeywordQuery ? 2 : 1;
801
+ const paramIdx = baseOffset + filterParams.length;
802
+ const synonyms = (_a3 = FIELD_SYNONYMS[key]) != null ? _a3 : [];
803
+ const keysToCheck = [key, ...synonyms];
804
+ const coalesceExprs = keysToCheck.map((k) => `LOWER(metadata->>'${k}')`);
805
+ return `COALESCE(${coalesceExprs.join(", ")}) = LOWER($${paramIdx})`;
806
+ });
807
+ whereClause = `WHERE ${conditions.join(" AND ")}`;
808
+ }
711
809
  if (queryText && dynamicKeywordQuery) {
712
810
  const hasEntityHints = entityHints.length > 0;
713
811
  const exactNameScoreExpr = hasEntityHints ? `+ (
@@ -724,19 +822,21 @@ var init_MultiTablePostgresProvider = __esm({
724
822
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
725
823
  ((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
726
824
  FROM "${table}" t
825
+ ${whereClause}
727
826
  ORDER BY hybrid_score DESC
728
827
  LIMIT ${tableLimit}
729
828
  `;
730
- params = [vectorLiteral, dynamicKeywordQuery];
829
+ params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
731
830
  } else {
732
831
  sqlQuery = `
733
832
  SELECT *,
734
833
  (1 - (embedding <=> $1::vector)) AS hybrid_score
735
834
  FROM "${table}" t
835
+ ${whereClause}
736
836
  ORDER BY hybrid_score DESC
737
837
  LIMIT ${tableLimit}
738
838
  `;
739
- params = [vectorLiteral];
839
+ params = [vectorLiteral, ...filterParams];
740
840
  }
741
841
  const result = await this.pool.query(sqlQuery, params);
742
842
  if (result.rowCount && result.rowCount > 0) {
@@ -4361,7 +4461,8 @@ var QueryProcessor = class {
4361
4461
  const fieldValuePatterns = [
4362
4462
  new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4363
4463
  new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4364
- new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4464
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4465
+ new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4365
4466
  ];
4366
4467
  for (const pattern of fieldValuePatterns) {
4367
4468
  for (const match of question.matchAll(pattern)) {
@@ -4595,73 +4696,8 @@ var LLMRouter = class {
4595
4696
  }
4596
4697
  };
4597
4698
 
4598
- // src/utils/synonyms.ts
4599
- var FIELD_SYNONYMS = {
4600
- name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
4601
- price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
4602
- brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
4603
- image: [
4604
- "imageUrl",
4605
- "thumbnail",
4606
- "img",
4607
- "url",
4608
- "photo",
4609
- "picture",
4610
- "media",
4611
- "image_url",
4612
- "main_image",
4613
- "product_image",
4614
- "thumb"
4615
- ],
4616
- stock: [
4617
- "inventory",
4618
- "quantity",
4619
- "count",
4620
- "availability",
4621
- "stock_level",
4622
- "inStock",
4623
- "is_available",
4624
- "in stock",
4625
- "status"
4626
- ],
4627
- category: [
4628
- "product_category",
4629
- "product category",
4630
- "category_name",
4631
- "category name",
4632
- "department",
4633
- "collection"
4634
- ],
4635
- description: ["summary", "content", "body", "text", "info", "details"],
4636
- link: ["url", "href", "product_url", "page_url", "link"]
4637
- };
4638
- function resolveMetadataValue(meta, uiKey) {
4639
- var _a;
4640
- const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
4641
- const keys = Object.keys(meta);
4642
- const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
4643
- let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
4644
- if (match !== void 0) return meta[match];
4645
- match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
4646
- if (match !== void 0) return meta[match];
4647
- const isBlacklisted = (kl) => {
4648
- return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
4649
- };
4650
- match = keys.find((k) => {
4651
- const kl = k.toLowerCase();
4652
- if (isBlacklisted(kl)) return false;
4653
- return kl.includes(uiKey.toLowerCase());
4654
- });
4655
- if (match !== void 0) return meta[match];
4656
- match = keys.find((k) => {
4657
- const kl = k.toLowerCase();
4658
- if (isBlacklisted(kl)) return false;
4659
- return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
4660
- });
4661
- return match !== void 0 ? meta[match] : void 0;
4662
- }
4663
-
4664
4699
  // src/utils/UITransformer.ts
4700
+ init_synonyms();
4665
4701
  var UITransformer = class {
4666
4702
  // ─── Public Entry Points ─────────────────────────────────────────────────
4667
4703
  /**
@@ -4958,7 +4994,7 @@ RULES:
4958
4994
  }
4959
4995
  // ─── Transform Helpers ────────────────────────────────────────────────────
4960
4996
  static transformToProductCarousel(data, config, trainedSchema) {
4961
- const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
4997
+ const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
4962
4998
  return {
4963
4999
  type: "product_carousel",
4964
5000
  title: "Recommended Products",
@@ -5917,14 +5953,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
5917
5953
  Identify which keys best correspond to these standard UI properties:
5918
5954
  ${propertyList}
5919
5955
 
5920
- Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
5956
+ 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.
5921
5957
  If no good match is found for a property, omit it.
5922
5958
 
5923
5959
  Example:
5924
5960
  {
5925
- "name": "Product_Title",
5926
- "price": "MSRP_USD",
5927
- "brand": "VendorName"
5961
+ "name": "Title",
5962
+ "price": "Variant Price",
5963
+ "brand": "Vendor",
5964
+ "image": "Image Src",
5965
+ "stock": "Variant Inventory Qty"
5928
5966
  }
5929
5967
  `;
5930
5968
  try {
@@ -6215,7 +6253,10 @@ var Pipeline = class {
6215
6253
  upsertBatchOptions
6216
6254
  );
6217
6255
  if (upsertResult.errors.length > 0) {
6218
- console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
6256
+ console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
6257
+ upsertResult.errors.forEach((err, idx) => {
6258
+ console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
6259
+ });
6219
6260
  }
6220
6261
  return upsertResult.totalProcessed;
6221
6262
  }
@@ -6330,8 +6371,9 @@ var Pipeline = class {
6330
6371
  this.embeddingCache.set(cacheKey, queryVector);
6331
6372
  }
6332
6373
  const graphData = (strategyResult === "graph" || strategyResult === "both") && this.graphDB && ((_j = this.config.rag) == null ? void 0 : _j.useGraphRetrieval) ? yield new __await(this.graphDB.query(searchQuery)) : void 0;
6374
+ const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
6333
6375
  const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
6334
- const wantsExhaustiveList = hasNumericPredicates || /\b(list|all|show|provide|give|get)\b/i.test(question);
6376
+ const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
6335
6377
  const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
6336
6378
  const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
6337
6379
  const retrieveEnd = performance.now();
@@ -6339,15 +6381,27 @@ var Pipeline = class {
6339
6381
  const retrieveMs = retrieveEnd - embedStart;
6340
6382
  const rerankStart = performance.now();
6341
6383
  const structuredSources = this.applyStructuredFilters(rawSources, filter);
6342
- let sources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6384
+ let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6385
+ const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
6343
6386
  if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
6344
- sources = yield new __await(this.reranker.rerank(sources, question, topK));
6387
+ fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
6345
6388
  } else if (!wantsExhaustiveList) {
6346
- sources = sources.slice(0, topK);
6389
+ fullSources = fullSources.slice(0, topK);
6347
6390
  }
6348
6391
  const rerankMs = performance.now() - rerankStart;
6349
- let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
6392
+ let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
6350
6393
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
6394
+ let displayCount = 15;
6395
+ if (hasMetadataFilter) {
6396
+ displayCount = fullSources.length;
6397
+ } else {
6398
+ const highlyRelevant = fullSources.filter((m) => m.score >= 0.4);
6399
+ displayCount = Math.max(highlyRelevant.length, topK);
6400
+ if (displayCount > 15) {
6401
+ displayCount = 15;
6402
+ }
6403
+ }
6404
+ const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
6351
6405
  if (graphData && graphData.nodes.length > 0) {
6352
6406
  const graphContext = graphData.nodes.map(
6353
6407
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -6360,7 +6414,18 @@ ${context}`;
6360
6414
  }
6361
6415
  const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
6362
6416
  const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
6363
- const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. The UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization. If listing products, use simple bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)";
6417
+ const uiTransformationPromise = trainedSchemaPromise.then(
6418
+ (trainedSchema) => this.generateUiTransformation(
6419
+ question,
6420
+ sources,
6421
+ trainedSchema,
6422
+ hasNumericPredicates
6423
+ )
6424
+ ).catch((uiError) => {
6425
+ console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
6426
+ return UITransformer.transform(question, sources, this.config);
6427
+ });
6428
+ const restrictionSuffix = "\n\n(IMPORTANT: Format your response beautifully using rich Markdown! Use bolding for emphasis, headings (##) for structure, bullet points for lists, and proper line breaks between paragraphs to make it highly readable. However, NEVER generate Markdown tables, HTML figures, or text charts (the UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization). If listing products, use simple markdown bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)";
6364
6429
  const hardenedHistory = [...history];
6365
6430
  const userQuestion = { role: "user", content: question + restrictionSuffix };
6366
6431
  const messages = [...hardenedHistory, userQuestion];
@@ -6430,19 +6495,7 @@ ${context}`;
6430
6495
  tokens,
6431
6496
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6432
6497
  };
6433
- let uiTransformation;
6434
- try {
6435
- const trainedSchema = yield new __await(trainedSchemaPromise);
6436
- uiTransformation = yield new __await(this.generateUiTransformation(
6437
- question,
6438
- sources,
6439
- trainedSchema,
6440
- hasNumericPredicates
6441
- ));
6442
- } catch (uiError) {
6443
- console.warn("[Pipeline] UI transformation failed before metadata yield:", uiError);
6444
- uiTransformation = UITransformer.transform(question, sources, this.config);
6445
- }
6498
+ const uiTransformation = yield new __await(uiTransformationPromise);
6446
6499
  yield {
6447
6500
  reply: "",
6448
6501
  sources,
@@ -7176,15 +7229,24 @@ function createStreamHandler(configOrPlugin) {
7176
7229
  });
7177
7230
  }
7178
7231
  const encoder = new TextEncoder();
7232
+ let isActive = true;
7179
7233
  const stream = new ReadableStream({
7180
7234
  async start(controller) {
7181
7235
  var _a;
7182
- const enqueue = (text) => controller.enqueue(encoder.encode(text));
7236
+ const enqueue = (text) => {
7237
+ if (!isActive) return;
7238
+ try {
7239
+ controller.enqueue(encoder.encode(text));
7240
+ } catch (err) {
7241
+ console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
7242
+ }
7243
+ };
7183
7244
  try {
7184
7245
  const pipelineStream = plugin.chatStream(message, history, namespace);
7185
7246
  try {
7186
7247
  for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
7187
7248
  const chunk = temp.value;
7249
+ if (!isActive) break;
7188
7250
  if (typeof chunk === "string") {
7189
7251
  enqueue(sseTextFrame(chunk));
7190
7252
  } else {
@@ -7222,12 +7284,27 @@ function createStreamHandler(configOrPlugin) {
7222
7284
  }
7223
7285
  }
7224
7286
  } catch (streamError) {
7225
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7226
- console.error("[createStreamHandler] Stream error:", streamError);
7227
- enqueue(sseErrorFrame(errorMessage));
7287
+ if (isActive) {
7288
+ const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7289
+ console.error("[createStreamHandler] Stream error:", streamError);
7290
+ try {
7291
+ enqueue(sseErrorFrame(errorMessage));
7292
+ } catch (e) {
7293
+ }
7294
+ }
7228
7295
  } finally {
7229
- controller.close();
7296
+ if (isActive) {
7297
+ isActive = false;
7298
+ try {
7299
+ controller.close();
7300
+ } catch (e) {
7301
+ }
7302
+ }
7230
7303
  }
7304
+ },
7305
+ cancel(reason) {
7306
+ isActive = false;
7307
+ console.log("[createStreamHandler] Stream connection closed by client:", reason);
7231
7308
  }
7232
7309
  });
7233
7310
  return new Response(stream, { headers: SSE_HEADERS });
@@ -7288,6 +7365,7 @@ function createUploadHandler(configOrPlugin) {
7288
7365
  if (parsed.data && parsed.data.length > 0) {
7289
7366
  let i = 0;
7290
7367
  let lastRowData = null;
7368
+ const csvCols = Object.keys(parsed.data[0]);
7291
7369
  for (const row of parsed.data) {
7292
7370
  i++;
7293
7371
  const rowData = row;
@@ -7309,12 +7387,14 @@ function createUploadHandler(configOrPlugin) {
7309
7387
  documents.push({
7310
7388
  docId: `${file.name}-row-${i}`,
7311
7389
  content: contentParts.join(", "),
7312
- metadata: __spreadValues(__spreadValues({
7390
+ metadata: __spreadValues(__spreadProps(__spreadValues({
7313
7391
  fileName: file.name,
7314
7392
  fileSize: file.size,
7315
7393
  fileType: file.type,
7316
7394
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
7317
- }, dimension ? { dimension } : {}), rowData)
7395
+ }, dimension ? { dimension } : {}), {
7396
+ csvHeaders: csvCols
7397
+ }), rowData)
7318
7398
  });
7319
7399
  }
7320
7400
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
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",
@@ -71,7 +71,7 @@ export function ChatWindow({
71
71
  const windowRef = useRef<HTMLDivElement>(null);
72
72
  const bottomRef = useRef<HTMLDivElement>(null);
73
73
  const inputRef = useRef<HTMLTextAreaElement>(null);
74
- const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
74
+ const { messages, send, clear, isLoading, error, stop } = useRagChat(projectId, {
75
75
  namespace: projectId,
76
76
  });
77
77
 
@@ -492,19 +492,29 @@ export function ChatWindow({
492
492
  </button>
493
493
  )}
494
494
 
495
- <button
496
- onClick={sendMessage}
497
- disabled={!input.trim() || isLoading}
498
- className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:scale-105 active:scale-95 shadow-md"
499
- style={{
500
- background:
501
- input.trim() && !isLoading
502
- ? `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})`
503
- : 'rgba(148, 163, 184, 0.2)', // slate-400/20 for light mode disabled
504
- }}
505
- >
506
- <ArrowUp className="w-4 h-4 text-white" />
507
- </button>
495
+ {isLoading ? (
496
+ <button
497
+ onClick={stop}
498
+ className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 shadow-md bg-rose-500 hover:bg-rose-600 text-white cursor-pointer"
499
+ title="Stop generating"
500
+ >
501
+ <div className="w-3.5 h-3.5 bg-white rounded-[2px]" />
502
+ </button>
503
+ ) : (
504
+ <button
505
+ onClick={sendMessage}
506
+ disabled={!input.trim()}
507
+ className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:scale-105 active:scale-95 shadow-md"
508
+ style={{
509
+ background:
510
+ input.trim()
511
+ ? `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})`
512
+ : 'rgba(148, 163, 184, 0.2)', // slate-400/20 for light mode disabled
513
+ }}
514
+ >
515
+ <ArrowUp className="w-4 h-4 text-white" />
516
+ </button>
517
+ )}
508
518
  </div>
509
519
  <p className="text-center text-[10px] text-slate-400 dark:text-white/20 mt-2">
510
520
  Press Enter to send · Shift+Enter for new line
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
- import { Bot, User, ChevronDown, ChevronRight, Activity } from 'lucide-react';
4
+ import { Bot, User, ChevronDown, ChevronRight, Activity, Copy, Check } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import { MessageBubbleProps, Product, UITransformationResponse } from '../types';
7
7
  import { SourceCard } from './SourceCard';
@@ -61,6 +61,17 @@ export function MessageBubble({
61
61
 
62
62
  const [showSources, setShowSources] = React.useState(false);
63
63
  const [showTrace, setShowTrace] = React.useState(false);
64
+ const [copied, setCopied] = React.useState(false);
65
+
66
+ const handleCopy = React.useCallback(async () => {
67
+ try {
68
+ await navigator.clipboard.writeText(message.content);
69
+ setCopied(true);
70
+ setTimeout(() => setCopied(false), 2000);
71
+ } catch (err) {
72
+ console.error('Failed to copy text:', err);
73
+ }
74
+ }, [message.content]);
64
75
 
65
76
  const structuredContent = React.useMemo(
66
77
  () => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
@@ -187,7 +198,7 @@ export function MessageBubble({
187
198
  <div className={`flex flex-col gap-1 min-w-0 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
188
199
  {/* Bubble */}
189
200
  <div
190
- className={`relative ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
201
+ className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
191
202
  ? 'text-white rounded-tr-sm'
192
203
  : 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
193
204
  }`}
@@ -195,6 +206,20 @@ export function MessageBubble({
195
206
  isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
196
207
  }
197
208
  >
209
+ {/* Floating Copy Button for completed assistant messages */}
210
+ {!isUser && !isStreaming && message.content && (
211
+ <button
212
+ onClick={handleCopy}
213
+ className={`absolute top-2 right-2 p-1.5 rounded-lg border transition-all duration-200 cursor-pointer ${
214
+ copied
215
+ ? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-500 dark:text-emerald-400'
216
+ : 'bg-slate-500/5 hover:bg-slate-500/10 border-slate-200 dark:border-white/10 text-slate-400 dark:text-white/30 hover:text-slate-600 dark:hover:text-white/60'
217
+ } opacity-0 group-hover:opacity-100 backdrop-blur-sm shadow-sm scale-95 group-hover:scale-100`}
218
+ title={copied ? "Copied!" : "Copy response"}
219
+ >
220
+ {copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
221
+ </button>
222
+ )}
198
223
  {isStreaming && !message.content ? (
199
224
  <span className="flex items-center gap-1 py-1">
200
225
  <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" />
@@ -240,11 +265,21 @@ export function MessageBubble({
240
265
  if (isUser || structuredContent.payload || !message.uiTransformation) return null;
241
266
  const ui = message.uiTransformation as UITransformationResponse;
242
267
  const textContent = (ui.data as { content?: string })?.content ?? '';
268
+
269
+ const isNegativeOrFallbackText =
270
+ textContent.toLowerCase().includes('cannot answer') ||
271
+ textContent.toLowerCase().includes('no relevant data') ||
272
+ textContent.toLowerCase().includes('no data available') ||
273
+ (typeof ui.title === 'string' && ui.title.toLowerCase().includes('no data')) ||
274
+ (typeof ui.description === 'string' && ui.description.toLowerCase().includes('no relevant data'));
275
+
243
276
  const shouldShow =
244
- ui.type === 'table' ||
245
- !['text', 'table'].includes(ui.type) ||
246
- (ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
247
- !processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase()));
277
+ !isNegativeOrFallbackText &&
278
+ (ui.type === 'table' ||
279
+ !['text', 'table'].includes(ui.type) ||
280
+ (ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
281
+ !processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase())));
282
+
248
283
  if (!shouldShow) return null;
249
284
  return (
250
285
  <div className="w-full mt-3">
@@ -47,7 +47,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
47
47
  className="object-cover group-hover:scale-105 transition-transform duration-500"
48
48
  />
49
49
  ) : (
50
- <div className="text-slate-400 dark:text-white/20 cursor-pointer" onClick={() => onAddToCart?.(product)}>
50
+ <div className="text-slate-400 dark:text-white/20">
51
51
  <ShoppingCart className="w-12 h-12" />
52
52
  </div>
53
53
  )}
@@ -68,8 +68,8 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
68
68
  </h3>
69
69
  {product.price && (
70
70
  <span className="text-sm font-bold" style={{ color: primaryColor }}>
71
- {typeof product.price === 'number'
72
- ? `$${product.price}`
71
+ {typeof product.price === 'number'
72
+ ? `$${product.price}`
73
73
  : (String(product.price).match(/^[\d.]/) ? `$${product.price}` : product.price)}
74
74
  </span>
75
75
  )}
@@ -84,7 +84,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
84
84
  <div className="mt-2 flex gap-2">
85
85
  <button
86
86
  onClick={() => onAddToCart?.(product)}
87
- className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5 cursor-pointer"
87
+ className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5"
88
88
  style={{ background: primaryColor }}
89
89
  >
90
90
  <ShoppingCart className="w-3 h-3" />