@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.js CHANGED
@@ -540,6 +540,82 @@ var init_PostgreSQLProvider = __esm({
540
540
  }
541
541
  });
542
542
 
543
+ // src/utils/synonyms.ts
544
+ function resolveMetadataValue(meta, uiKey) {
545
+ var _a;
546
+ const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
547
+ const keys = Object.keys(meta);
548
+ const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
549
+ let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
550
+ if (match !== void 0) return meta[match];
551
+ match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
552
+ if (match !== void 0) return meta[match];
553
+ const isBlacklisted = (kl) => {
554
+ return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
555
+ };
556
+ match = keys.find((k) => {
557
+ const kl = k.toLowerCase();
558
+ if (isBlacklisted(kl)) return false;
559
+ return kl.includes(uiKey.toLowerCase());
560
+ });
561
+ if (match !== void 0) return meta[match];
562
+ match = keys.find((k) => {
563
+ const kl = k.toLowerCase();
564
+ if (isBlacklisted(kl)) return false;
565
+ return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
566
+ });
567
+ return match !== void 0 ? meta[match] : void 0;
568
+ }
569
+ var FIELD_SYNONYMS;
570
+ var init_synonyms = __esm({
571
+ "src/utils/synonyms.ts"() {
572
+ "use strict";
573
+ FIELD_SYNONYMS = {
574
+ name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
575
+ price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd", "variant price"],
576
+ brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
577
+ image: [
578
+ "imageUrl",
579
+ "thumbnail",
580
+ "img",
581
+ "url",
582
+ "photo",
583
+ "picture",
584
+ "media",
585
+ "image_url",
586
+ "main_image",
587
+ "product_image",
588
+ "thumb",
589
+ "image src",
590
+ "variant image"
591
+ ],
592
+ stock: [
593
+ "inventory",
594
+ "quantity",
595
+ "count",
596
+ "availability",
597
+ "stock_level",
598
+ "inStock",
599
+ "is_available",
600
+ "in stock",
601
+ "status",
602
+ "variant inventory qty"
603
+ ],
604
+ category: [
605
+ "product_category",
606
+ "product category",
607
+ "category_name",
608
+ "category name",
609
+ "department",
610
+ "collection",
611
+ "type"
612
+ ],
613
+ description: ["summary", "content", "body", "text", "info", "details", "body (html)", "seo description"],
614
+ link: ["url", "href", "product_url", "page_url", "link"]
615
+ };
616
+ }
617
+ });
618
+
543
619
  // src/providers/vectordb/MultiTablePostgresProvider.ts
544
620
  var MultiTablePostgresProvider_exports = {};
545
621
  __export(MultiTablePostgresProvider_exports, {
@@ -551,6 +627,7 @@ var init_MultiTablePostgresProvider = __esm({
551
627
  "use strict";
552
628
  import_pg2 = require("pg");
553
629
  init_BaseVectorProvider();
630
+ init_synonyms();
554
631
  MultiTablePostgresProvider = class extends BaseVectorProvider {
555
632
  constructor(config) {
556
633
  var _a, _b, _c;
@@ -631,8 +708,13 @@ var init_MultiTablePostgresProvider = __esm({
631
708
  }
632
709
  tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
633
710
  const firstMeta = fileDocs[0].metadata || {};
634
- const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding"];
635
- const csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
711
+ let csvHeaders = [];
712
+ if (Array.isArray(firstMeta.csvHeaders)) {
713
+ csvHeaders = firstMeta.csvHeaders;
714
+ } else {
715
+ const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding", "docId", "docid", "chunkId", "chunkid", "csvHeaders", "csvheaders"];
716
+ csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
717
+ }
636
718
  const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
637
719
  const createTableSql = `
638
720
  CREATE TABLE IF NOT EXISTS "${tableName}" (
@@ -725,10 +807,26 @@ var init_MultiTablePostgresProvider = __esm({
725
807
  };
726
808
  const dynamicKeywordQuery = getDynamicKeywordQuery();
727
809
  const tableLimit = Math.max(topK, 50);
810
+ const metadataFilters = _filter == null ? void 0 : _filter.metadata;
728
811
  const queryPromises = this.tables.map(async (table) => {
729
812
  try {
730
813
  let sqlQuery = "";
731
814
  let params = [];
815
+ let whereClause = "";
816
+ const filterParams = [];
817
+ if (metadataFilters && Object.keys(metadataFilters).length > 0) {
818
+ const conditions = Object.entries(metadataFilters).map(([key, val]) => {
819
+ var _a3;
820
+ filterParams.push(String(val));
821
+ const baseOffset = queryText && dynamicKeywordQuery ? 2 : 1;
822
+ const paramIdx = baseOffset + filterParams.length;
823
+ const synonyms = (_a3 = FIELD_SYNONYMS[key]) != null ? _a3 : [];
824
+ const keysToCheck = [key, ...synonyms];
825
+ const coalesceExprs = keysToCheck.map((k) => `LOWER(metadata->>'${k}')`);
826
+ return `COALESCE(${coalesceExprs.join(", ")}) = LOWER($${paramIdx})`;
827
+ });
828
+ whereClause = `WHERE ${conditions.join(" AND ")}`;
829
+ }
732
830
  if (queryText && dynamicKeywordQuery) {
733
831
  const hasEntityHints = entityHints.length > 0;
734
832
  const exactNameScoreExpr = hasEntityHints ? `+ (
@@ -745,19 +843,21 @@ var init_MultiTablePostgresProvider = __esm({
745
843
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
746
844
  ((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
747
845
  FROM "${table}" t
846
+ ${whereClause}
748
847
  ORDER BY hybrid_score DESC
749
848
  LIMIT ${tableLimit}
750
849
  `;
751
- params = [vectorLiteral, dynamicKeywordQuery];
850
+ params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
752
851
  } else {
753
852
  sqlQuery = `
754
853
  SELECT *,
755
854
  (1 - (embedding <=> $1::vector)) AS hybrid_score
756
855
  FROM "${table}" t
856
+ ${whereClause}
757
857
  ORDER BY hybrid_score DESC
758
858
  LIMIT ${tableLimit}
759
859
  `;
760
- params = [vectorLiteral];
860
+ params = [vectorLiteral, ...filterParams];
761
861
  }
762
862
  const result = await this.pool.query(sqlQuery, params);
763
863
  if (result.rowCount && result.rowCount > 0) {
@@ -990,12 +1090,15 @@ var init_MongoDBProvider = __esm({
990
1090
  }
991
1091
  );
992
1092
  const results = await this.collection.aggregate(pipeline).toArray();
993
- return results.map((res) => ({
994
- id: res._id,
995
- content: res[this.contentKey],
996
- metadata: res[this.metadataKey] || {},
997
- score: res.score
998
- }));
1093
+ return results.map((res) => {
1094
+ const normalizedScore = res.score * 2 - 1;
1095
+ return {
1096
+ id: res._id,
1097
+ content: res[this.contentKey],
1098
+ metadata: res[this.metadataKey] || {},
1099
+ score: normalizedScore
1100
+ };
1101
+ });
999
1102
  }
1000
1103
  async delete(id, namespace) {
1001
1104
  await this.collection.deleteOne(__spreadValues({ _id: id }, namespace ? { namespace } : {}));
@@ -4430,7 +4533,8 @@ var QueryProcessor = class {
4430
4533
  const fieldValuePatterns = [
4431
4534
  new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4432
4535
  new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4433
- new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4536
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4537
+ new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4434
4538
  ];
4435
4539
  for (const pattern of fieldValuePatterns) {
4436
4540
  for (const match of question.matchAll(pattern)) {
@@ -4664,73 +4768,8 @@ var LLMRouter = class {
4664
4768
  }
4665
4769
  };
4666
4770
 
4667
- // src/utils/synonyms.ts
4668
- var FIELD_SYNONYMS = {
4669
- name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
4670
- price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
4671
- brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
4672
- image: [
4673
- "imageUrl",
4674
- "thumbnail",
4675
- "img",
4676
- "url",
4677
- "photo",
4678
- "picture",
4679
- "media",
4680
- "image_url",
4681
- "main_image",
4682
- "product_image",
4683
- "thumb"
4684
- ],
4685
- stock: [
4686
- "inventory",
4687
- "quantity",
4688
- "count",
4689
- "availability",
4690
- "stock_level",
4691
- "inStock",
4692
- "is_available",
4693
- "in stock",
4694
- "status"
4695
- ],
4696
- category: [
4697
- "product_category",
4698
- "product category",
4699
- "category_name",
4700
- "category name",
4701
- "department",
4702
- "collection"
4703
- ],
4704
- description: ["summary", "content", "body", "text", "info", "details"],
4705
- link: ["url", "href", "product_url", "page_url", "link"]
4706
- };
4707
- function resolveMetadataValue(meta, uiKey) {
4708
- var _a;
4709
- const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
4710
- const keys = Object.keys(meta);
4711
- const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
4712
- let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
4713
- if (match !== void 0) return meta[match];
4714
- match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
4715
- if (match !== void 0) return meta[match];
4716
- const isBlacklisted = (kl) => {
4717
- return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
4718
- };
4719
- match = keys.find((k) => {
4720
- const kl = k.toLowerCase();
4721
- if (isBlacklisted(kl)) return false;
4722
- return kl.includes(uiKey.toLowerCase());
4723
- });
4724
- if (match !== void 0) return meta[match];
4725
- match = keys.find((k) => {
4726
- const kl = k.toLowerCase();
4727
- if (isBlacklisted(kl)) return false;
4728
- return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
4729
- });
4730
- return match !== void 0 ? meta[match] : void 0;
4731
- }
4732
-
4733
4771
  // src/utils/UITransformer.ts
4772
+ init_synonyms();
4734
4773
  var UITransformer = class {
4735
4774
  // ─── Public Entry Points ─────────────────────────────────────────────────
4736
4775
  /**
@@ -5027,7 +5066,7 @@ RULES:
5027
5066
  }
5028
5067
  // ─── Transform Helpers ────────────────────────────────────────────────────
5029
5068
  static transformToProductCarousel(data, config, trainedSchema) {
5030
- const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
5069
+ const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
5031
5070
  return {
5032
5071
  type: "product_carousel",
5033
5072
  title: "Recommended Products",
@@ -5986,14 +6025,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
5986
6025
  Identify which keys best correspond to these standard UI properties:
5987
6026
  ${propertyList}
5988
6027
 
5989
- Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
6028
+ 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.
5990
6029
  If no good match is found for a property, omit it.
5991
6030
 
5992
6031
  Example:
5993
6032
  {
5994
- "name": "Product_Title",
5995
- "price": "MSRP_USD",
5996
- "brand": "VendorName"
6033
+ "name": "Title",
6034
+ "price": "Variant Price",
6035
+ "brand": "Vendor",
6036
+ "image": "Image Src",
6037
+ "stock": "Variant Inventory Qty"
5997
6038
  }
5998
6039
  `;
5999
6040
  try {
@@ -6284,7 +6325,10 @@ var Pipeline = class {
6284
6325
  upsertBatchOptions
6285
6326
  );
6286
6327
  if (upsertResult.errors.length > 0) {
6287
- console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
6328
+ console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
6329
+ upsertResult.errors.forEach((err, idx) => {
6330
+ console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
6331
+ });
6288
6332
  }
6289
6333
  return upsertResult.totalProcessed;
6290
6334
  }
@@ -6399,8 +6443,9 @@ var Pipeline = class {
6399
6443
  this.embeddingCache.set(cacheKey, queryVector);
6400
6444
  }
6401
6445
  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;
6446
+ const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
6402
6447
  const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
6403
- const wantsExhaustiveList = hasNumericPredicates || /\b(list|all|show|provide|give|get)\b/i.test(question);
6448
+ const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
6404
6449
  const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
6405
6450
  const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
6406
6451
  const retrieveEnd = performance.now();
@@ -6408,15 +6453,28 @@ var Pipeline = class {
6408
6453
  const retrieveMs = retrieveEnd - embedStart;
6409
6454
  const rerankStart = performance.now();
6410
6455
  const structuredSources = this.applyStructuredFilters(rawSources, filter);
6411
- let sources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6456
+ let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6457
+ const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
6412
6458
  if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
6413
- sources = yield new __await(this.reranker.rerank(sources, question, topK));
6459
+ fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
6414
6460
  } else if (!wantsExhaustiveList) {
6415
- sources = sources.slice(0, topK);
6461
+ fullSources = fullSources.slice(0, topK);
6416
6462
  }
6417
6463
  const rerankMs = performance.now() - rerankStart;
6418
- let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
6464
+ let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
6419
6465
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
6466
+ let displayCount = 15;
6467
+ if (hasMetadataFilter) {
6468
+ displayCount = fullSources.length;
6469
+ } else {
6470
+ const baseThreshold = Math.max(scoreThreshold, 0.4);
6471
+ const highlyRelevant = fullSources.filter((m) => m.score >= baseThreshold);
6472
+ displayCount = Math.max(highlyRelevant.length, topK);
6473
+ if (displayCount > 15) {
6474
+ displayCount = 15;
6475
+ }
6476
+ }
6477
+ const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
6420
6478
  if (graphData && graphData.nodes.length > 0) {
6421
6479
  const graphContext = graphData.nodes.map(
6422
6480
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
@@ -6429,7 +6487,18 @@ ${context}`;
6429
6487
  }
6430
6488
  const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
6431
6489
  const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
6432
- 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.)";
6490
+ const uiTransformationPromise = trainedSchemaPromise.then(
6491
+ (trainedSchema) => this.generateUiTransformation(
6492
+ question,
6493
+ sources,
6494
+ trainedSchema,
6495
+ hasNumericPredicates
6496
+ )
6497
+ ).catch((uiError) => {
6498
+ console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
6499
+ return UITransformer.transform(question, sources, this.config);
6500
+ });
6501
+ 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.)";
6433
6502
  const hardenedHistory = [...history];
6434
6503
  const userQuestion = { role: "user", content: question + restrictionSuffix };
6435
6504
  const messages = [...hardenedHistory, userQuestion];
@@ -6499,19 +6568,7 @@ ${context}`;
6499
6568
  tokens,
6500
6569
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6501
6570
  };
6502
- let uiTransformation;
6503
- try {
6504
- const trainedSchema = yield new __await(trainedSchemaPromise);
6505
- uiTransformation = yield new __await(this.generateUiTransformation(
6506
- question,
6507
- sources,
6508
- trainedSchema,
6509
- hasNumericPredicates
6510
- ));
6511
- } catch (uiError) {
6512
- console.warn("[Pipeline] UI transformation failed before metadata yield:", uiError);
6513
- uiTransformation = UITransformer.transform(question, sources, this.config);
6514
- }
6571
+ const uiTransformation = yield new __await(uiTransformationPromise);
6515
6572
  yield {
6516
6573
  reply: "",
6517
6574
  sources,
@@ -7245,15 +7302,24 @@ function createStreamHandler(configOrPlugin) {
7245
7302
  });
7246
7303
  }
7247
7304
  const encoder = new TextEncoder();
7305
+ let isActive = true;
7248
7306
  const stream = new ReadableStream({
7249
7307
  async start(controller) {
7250
7308
  var _a;
7251
- const enqueue = (text) => controller.enqueue(encoder.encode(text));
7309
+ const enqueue = (text) => {
7310
+ if (!isActive) return;
7311
+ try {
7312
+ controller.enqueue(encoder.encode(text));
7313
+ } catch (err) {
7314
+ console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
7315
+ }
7316
+ };
7252
7317
  try {
7253
7318
  const pipelineStream = plugin.chatStream(message, history, namespace);
7254
7319
  try {
7255
7320
  for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
7256
7321
  const chunk = temp.value;
7322
+ if (!isActive) break;
7257
7323
  if (typeof chunk === "string") {
7258
7324
  enqueue(sseTextFrame(chunk));
7259
7325
  } else {
@@ -7291,12 +7357,27 @@ function createStreamHandler(configOrPlugin) {
7291
7357
  }
7292
7358
  }
7293
7359
  } catch (streamError) {
7294
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7295
- console.error("[createStreamHandler] Stream error:", streamError);
7296
- enqueue(sseErrorFrame(errorMessage));
7360
+ if (isActive) {
7361
+ const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7362
+ console.error("[createStreamHandler] Stream error:", streamError);
7363
+ try {
7364
+ enqueue(sseErrorFrame(errorMessage));
7365
+ } catch (e) {
7366
+ }
7367
+ }
7297
7368
  } finally {
7298
- controller.close();
7369
+ if (isActive) {
7370
+ isActive = false;
7371
+ try {
7372
+ controller.close();
7373
+ } catch (e) {
7374
+ }
7375
+ }
7299
7376
  }
7377
+ },
7378
+ cancel(reason) {
7379
+ isActive = false;
7380
+ console.log("[createStreamHandler] Stream connection closed by client:", reason);
7300
7381
  }
7301
7382
  });
7302
7383
  return new Response(stream, { headers: SSE_HEADERS });
@@ -7357,6 +7438,7 @@ function createUploadHandler(configOrPlugin) {
7357
7438
  if (parsed.data && parsed.data.length > 0) {
7358
7439
  let i = 0;
7359
7440
  let lastRowData = null;
7441
+ const csvCols = Object.keys(parsed.data[0]);
7360
7442
  for (const row of parsed.data) {
7361
7443
  i++;
7362
7444
  const rowData = row;
@@ -7378,12 +7460,14 @@ function createUploadHandler(configOrPlugin) {
7378
7460
  documents.push({
7379
7461
  docId: `${file.name}-row-${i}`,
7380
7462
  content: contentParts.join(", "),
7381
- metadata: __spreadValues(__spreadValues({
7463
+ metadata: __spreadValues(__spreadProps(__spreadValues({
7382
7464
  fileName: file.name,
7383
7465
  fileSize: file.size,
7384
7466
  fileType: file.type,
7385
7467
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
7386
- }, dimension ? { dimension } : {}), rowData)
7468
+ }, dimension ? { dimension } : {}), {
7469
+ csvHeaders: csvCols
7470
+ }), rowData)
7387
7471
  });
7388
7472
  }
7389
7473
  }