@retrivora-ai/rag-engine 1.9.1 → 1.9.3

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) {
@@ -969,12 +1069,15 @@ var init_MongoDBProvider = __esm({
969
1069
  }
970
1070
  );
971
1071
  const results = await this.collection.aggregate(pipeline).toArray();
972
- return results.map((res) => ({
973
- id: res._id,
974
- content: res[this.contentKey],
975
- metadata: res[this.metadataKey] || {},
976
- score: res.score
977
- }));
1072
+ return results.map((res) => {
1073
+ const normalizedScore = res.score * 2 - 1;
1074
+ return {
1075
+ id: res._id,
1076
+ content: res[this.contentKey],
1077
+ metadata: res[this.metadataKey] || {},
1078
+ score: normalizedScore
1079
+ };
1080
+ });
978
1081
  }
979
1082
  async delete(id, namespace) {
980
1083
  await this.collection.deleteOne(__spreadValues({ _id: id }, namespace ? { namespace } : {}));
@@ -4361,7 +4464,8 @@ var QueryProcessor = class {
4361
4464
  const fieldValuePatterns = [
4362
4465
  new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4363
4466
  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")
4467
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4468
+ new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4365
4469
  ];
4366
4470
  for (const pattern of fieldValuePatterns) {
4367
4471
  for (const match of question.matchAll(pattern)) {
@@ -4595,73 +4699,8 @@ var LLMRouter = class {
4595
4699
  }
4596
4700
  };
4597
4701
 
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
4702
  // src/utils/UITransformer.ts
4703
+ init_synonyms();
4665
4704
  var UITransformer = class {
4666
4705
  // ─── Public Entry Points ─────────────────────────────────────────────────
4667
4706
  /**
@@ -4958,7 +4997,7 @@ RULES:
4958
4997
  }
4959
4998
  // ─── Transform Helpers ────────────────────────────────────────────────────
4960
4999
  static transformToProductCarousel(data, config, trainedSchema) {
4961
- const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
5000
+ const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
4962
5001
  return {
4963
5002
  type: "product_carousel",
4964
5003
  title: "Recommended Products",
@@ -5917,14 +5956,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
5917
5956
  Identify which keys best correspond to these standard UI properties:
5918
5957
  ${propertyList}
5919
5958
 
5920
- Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
5959
+ 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
5960
  If no good match is found for a property, omit it.
5922
5961
 
5923
5962
  Example:
5924
5963
  {
5925
- "name": "Product_Title",
5926
- "price": "MSRP_USD",
5927
- "brand": "VendorName"
5964
+ "name": "Title",
5965
+ "price": "Variant Price",
5966
+ "brand": "Vendor",
5967
+ "image": "Image Src",
5968
+ "stock": "Variant Inventory Qty"
5928
5969
  }
5929
5970
  `;
5930
5971
  try {
@@ -6215,7 +6256,10 @@ var Pipeline = class {
6215
6256
  upsertBatchOptions
6216
6257
  );
6217
6258
  if (upsertResult.errors.length > 0) {
6218
- console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
6259
+ console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
6260
+ upsertResult.errors.forEach((err, idx) => {
6261
+ console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
6262
+ });
6219
6263
  }
6220
6264
  return upsertResult.totalProcessed;
6221
6265
  }
@@ -6330,8 +6374,9 @@ var Pipeline = class {
6330
6374
  this.embeddingCache.set(cacheKey, queryVector);
6331
6375
  }
6332
6376
  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;
6377
+ const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
6333
6378
  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);
6379
+ const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
6335
6380
  const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
6336
6381
  const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
6337
6382
  const retrieveEnd = performance.now();
@@ -6339,15 +6384,28 @@ var Pipeline = class {
6339
6384
  const retrieveMs = retrieveEnd - embedStart;
6340
6385
  const rerankStart = performance.now();
6341
6386
  const structuredSources = this.applyStructuredFilters(rawSources, filter);
6342
- let sources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6387
+ let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6388
+ const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
6343
6389
  if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
6344
- sources = yield new __await(this.reranker.rerank(sources, question, topK));
6390
+ fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
6345
6391
  } else if (!wantsExhaustiveList) {
6346
- sources = sources.slice(0, topK);
6392
+ fullSources = fullSources.slice(0, topK);
6347
6393
  }
6348
6394
  const rerankMs = performance.now() - rerankStart;
6349
- let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
6395
+ let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
6350
6396
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
6397
+ let displayCount = 15;
6398
+ if (hasMetadataFilter) {
6399
+ displayCount = fullSources.length;
6400
+ } else {
6401
+ const baseThreshold = Math.max(scoreThreshold, 0.4);
6402
+ const highlyRelevant = fullSources.filter((m) => m.score >= baseThreshold);
6403
+ displayCount = Math.max(highlyRelevant.length, topK);
6404
+ if (displayCount > 15) {
6405
+ displayCount = 15;
6406
+ }
6407
+ }
6408
+ const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
6351
6409
  if (graphData && graphData.nodes.length > 0) {
6352
6410
  const graphContext = graphData.nodes.map(
6353
6411
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -6360,7 +6418,18 @@ ${context}`;
6360
6418
  }
6361
6419
  const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
6362
6420
  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.)";
6421
+ const uiTransformationPromise = trainedSchemaPromise.then(
6422
+ (trainedSchema) => this.generateUiTransformation(
6423
+ question,
6424
+ sources,
6425
+ trainedSchema,
6426
+ hasNumericPredicates
6427
+ )
6428
+ ).catch((uiError) => {
6429
+ console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
6430
+ return UITransformer.transform(question, sources, this.config);
6431
+ });
6432
+ 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
6433
  const hardenedHistory = [...history];
6365
6434
  const userQuestion = { role: "user", content: question + restrictionSuffix };
6366
6435
  const messages = [...hardenedHistory, userQuestion];
@@ -6430,19 +6499,7 @@ ${context}`;
6430
6499
  tokens,
6431
6500
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6432
6501
  };
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
- }
6502
+ const uiTransformation = yield new __await(uiTransformationPromise);
6446
6503
  yield {
6447
6504
  reply: "",
6448
6505
  sources,
@@ -7176,15 +7233,24 @@ function createStreamHandler(configOrPlugin) {
7176
7233
  });
7177
7234
  }
7178
7235
  const encoder = new TextEncoder();
7236
+ let isActive = true;
7179
7237
  const stream = new ReadableStream({
7180
7238
  async start(controller) {
7181
7239
  var _a;
7182
- const enqueue = (text) => controller.enqueue(encoder.encode(text));
7240
+ const enqueue = (text) => {
7241
+ if (!isActive) return;
7242
+ try {
7243
+ controller.enqueue(encoder.encode(text));
7244
+ } catch (err) {
7245
+ console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
7246
+ }
7247
+ };
7183
7248
  try {
7184
7249
  const pipelineStream = plugin.chatStream(message, history, namespace);
7185
7250
  try {
7186
7251
  for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
7187
7252
  const chunk = temp.value;
7253
+ if (!isActive) break;
7188
7254
  if (typeof chunk === "string") {
7189
7255
  enqueue(sseTextFrame(chunk));
7190
7256
  } else {
@@ -7222,12 +7288,27 @@ function createStreamHandler(configOrPlugin) {
7222
7288
  }
7223
7289
  }
7224
7290
  } catch (streamError) {
7225
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7226
- console.error("[createStreamHandler] Stream error:", streamError);
7227
- enqueue(sseErrorFrame(errorMessage));
7291
+ if (isActive) {
7292
+ const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7293
+ console.error("[createStreamHandler] Stream error:", streamError);
7294
+ try {
7295
+ enqueue(sseErrorFrame(errorMessage));
7296
+ } catch (e) {
7297
+ }
7298
+ }
7228
7299
  } finally {
7229
- controller.close();
7300
+ if (isActive) {
7301
+ isActive = false;
7302
+ try {
7303
+ controller.close();
7304
+ } catch (e) {
7305
+ }
7306
+ }
7230
7307
  }
7308
+ },
7309
+ cancel(reason) {
7310
+ isActive = false;
7311
+ console.log("[createStreamHandler] Stream connection closed by client:", reason);
7231
7312
  }
7232
7313
  });
7233
7314
  return new Response(stream, { headers: SSE_HEADERS });
@@ -7288,6 +7369,7 @@ function createUploadHandler(configOrPlugin) {
7288
7369
  if (parsed.data && parsed.data.length > 0) {
7289
7370
  let i = 0;
7290
7371
  let lastRowData = null;
7372
+ const csvCols = Object.keys(parsed.data[0]);
7291
7373
  for (const row of parsed.data) {
7292
7374
  i++;
7293
7375
  const rowData = row;
@@ -7309,12 +7391,14 @@ function createUploadHandler(configOrPlugin) {
7309
7391
  documents.push({
7310
7392
  docId: `${file.name}-row-${i}`,
7311
7393
  content: contentParts.join(", "),
7312
- metadata: __spreadValues(__spreadValues({
7394
+ metadata: __spreadValues(__spreadProps(__spreadValues({
7313
7395
  fileName: file.name,
7314
7396
  fileSize: file.size,
7315
7397
  fileType: file.type,
7316
7398
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
7317
- }, dimension ? { dimension } : {}), rowData)
7399
+ }, dimension ? { dimension } : {}), {
7400
+ csvHeaders: csvCols
7401
+ }), rowData)
7318
7402
  });
7319
7403
  }
7320
7404
  }
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.3",
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" />