@retrivora-ai/rag-engine 1.9.0 → 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.
Files changed (47) hide show
  1. package/dist/{ILLMProvider-BOJFz3Na.d.mts → ILLMProvider-Bw2A28nU.d.mts} +12 -0
  2. package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-Bw2A28nU.d.ts} +12 -0
  3. package/dist/handlers/index.d.mts +2 -2
  4. package/dist/handlers/index.d.ts +2 -2
  5. package/dist/handlers/index.js +1874 -542
  6. package/dist/handlers/index.mjs +1873 -541
  7. package/dist/{index-D3V9Et2M.d.mts → index-B70ZLkfG.d.mts} +1 -1
  8. package/dist/{index-BwpcaziY.d.ts → index-DVu-mkAM.d.ts} +1 -1
  9. package/dist/index.css +83 -0
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +330 -106
  13. package/dist/index.mjs +333 -107
  14. package/dist/server.d.mts +32 -5
  15. package/dist/server.d.ts +32 -5
  16. package/dist/server.js +1871 -736
  17. package/dist/server.mjs +1870 -735
  18. package/package.json +1 -1
  19. package/src/components/ChatWindow.tsx +24 -14
  20. package/src/components/MarkdownComponents.tsx +3 -3
  21. package/src/components/MessageBubble.tsx +89 -7
  22. package/src/components/ProductCard.tsx +29 -2
  23. package/src/components/UIDispatcher.tsx +1 -0
  24. package/src/components/VisualizationRenderer.tsx +143 -11
  25. package/src/config/EmbeddingStrategy.ts +5 -4
  26. package/src/config/RagConfig.ts +10 -0
  27. package/src/config/serverConfig.ts +16 -1
  28. package/src/core/LLMRouter.ts +79 -0
  29. package/src/core/Pipeline.ts +295 -51
  30. package/src/core/ProviderRegistry.ts +6 -0
  31. package/src/core/QueryProcessor.ts +108 -9
  32. package/src/handlers/index.ts +37 -11
  33. package/src/hooks/useRagChat.ts +77 -17
  34. package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
  35. package/src/providers/vectordb/ChromaDBProvider.ts +13 -2
  36. package/src/providers/vectordb/MilvusProvider.ts +18 -2
  37. package/src/providers/vectordb/MultiTablePostgresProvider.ts +48 -16
  38. package/src/providers/vectordb/PostgreSQLProvider.ts +1 -1
  39. package/src/providers/vectordb/QdrantProvider.ts +1 -1
  40. package/src/providers/vectordb/RedisProvider.ts +3 -4
  41. package/src/providers/vectordb/WeaviateProvider.ts +41 -3
  42. package/src/types/chat.ts +2 -0
  43. package/src/types/index.ts +26 -0
  44. package/src/utils/ProductExtractor.ts +5 -3
  45. package/src/utils/SchemaMapper.ts +6 -4
  46. package/src/utils/UITransformer.ts +1350 -490
  47. package/src/utils/synonyms.ts +6 -4
@@ -334,17 +334,300 @@ var init_PineconeProvider = __esm({
334
334
  }
335
335
  });
336
336
 
337
+ // src/providers/vectordb/PostgreSQLProvider.ts
338
+ var PostgreSQLProvider_exports = {};
339
+ __export(PostgreSQLProvider_exports, {
340
+ PostgreSQLProvider: () => PostgreSQLProvider
341
+ });
342
+ var import_pg, PostgreSQLProvider;
343
+ var init_PostgreSQLProvider = __esm({
344
+ "src/providers/vectordb/PostgreSQLProvider.ts"() {
345
+ "use strict";
346
+ import_pg = require("pg");
347
+ init_BaseVectorProvider();
348
+ PostgreSQLProvider = class extends BaseVectorProvider {
349
+ constructor(config) {
350
+ var _a;
351
+ super(config);
352
+ this.tableName = this.indexName.replace(/[^a-z0-9_]/gi, "_");
353
+ const opts = config.options;
354
+ if (!opts.connectionString) throw new Error("[PostgreSQLProvider] options.connectionString is required");
355
+ this.connectionString = opts.connectionString;
356
+ this.dimensions = (_a = opts.dimensions) != null ? _a : 1536;
357
+ }
358
+ static getValidator() {
359
+ return {
360
+ validate(config) {
361
+ const errors = [];
362
+ const opts = config.options || {};
363
+ if (!opts.connectionString) {
364
+ errors.push({
365
+ field: "vectorDb.options.connectionString",
366
+ message: "PostgreSQL connection string is required",
367
+ suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
368
+ severity: "error"
369
+ });
370
+ }
371
+ if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
372
+ errors.push({
373
+ field: "vectorDb.options.tables",
374
+ message: "PostgreSQL tables must be a string or a string array",
375
+ severity: "error"
376
+ });
377
+ }
378
+ return errors;
379
+ }
380
+ };
381
+ }
382
+ static getHealthChecker() {
383
+ return {
384
+ async check(config) {
385
+ const opts = config.options || {};
386
+ const timestamp = Date.now();
387
+ try {
388
+ const { Client } = await import("pg");
389
+ const client = new Client({ connectionString: opts.connectionString });
390
+ await client.connect();
391
+ const result = await client.query(`
392
+ SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
393
+ `);
394
+ const hasVector = result.rows[0].exists;
395
+ await client.end();
396
+ return {
397
+ healthy: true,
398
+ provider: "postgresql",
399
+ capabilities: { pgvectorInstalled: hasVector },
400
+ timestamp
401
+ };
402
+ } catch (error) {
403
+ return {
404
+ healthy: false,
405
+ provider: "postgresql",
406
+ error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
407
+ timestamp
408
+ };
409
+ }
410
+ }
411
+ };
412
+ }
413
+ async initialize() {
414
+ this.pool = new import_pg.Pool({ connectionString: this.connectionString });
415
+ const client = await this.pool.connect();
416
+ try {
417
+ await client.query("CREATE EXTENSION IF NOT EXISTS vector");
418
+ await client.query(`
419
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
420
+ id TEXT PRIMARY KEY,
421
+ namespace TEXT NOT NULL DEFAULT '',
422
+ content TEXT NOT NULL,
423
+ metadata JSONB,
424
+ embedding VECTOR(${this.dimensions})
425
+ )
426
+ `);
427
+ await client.query(`
428
+ CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
429
+ ON ${this.tableName}
430
+ USING hnsw (embedding vector_cosine_ops)
431
+ `);
432
+ } finally {
433
+ client.release();
434
+ }
435
+ }
436
+ async upsert(doc, namespace = "") {
437
+ var _a;
438
+ const vectorLiteral = `[${doc.vector.join(",")}]`;
439
+ await this.pool.query(
440
+ `INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
441
+ VALUES ($1, $2, $3, $4, $5::vector)
442
+ ON CONFLICT (id) DO UPDATE
443
+ SET namespace = EXCLUDED.namespace,
444
+ content = EXCLUDED.content,
445
+ metadata = EXCLUDED.metadata,
446
+ embedding = EXCLUDED.embedding`,
447
+ [doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
448
+ );
449
+ }
450
+ async batchUpsert(docs, namespace = "") {
451
+ if (docs.length === 0) return;
452
+ const client = await this.pool.connect();
453
+ try {
454
+ await client.query("BEGIN");
455
+ const BATCH_SIZE = 50;
456
+ for (let i = 0; i < docs.length; i += BATCH_SIZE) {
457
+ const batch = docs.slice(i, i + BATCH_SIZE);
458
+ const values = [];
459
+ const valuePlaceholders = batch.map((doc, idx) => {
460
+ var _a;
461
+ const offset = idx * 5;
462
+ values.push(doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), `[${doc.vector.join(",")}]`);
463
+ return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}::vector)`;
464
+ }).join(", ");
465
+ const query = `
466
+ INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
467
+ VALUES ${valuePlaceholders}
468
+ ON CONFLICT (id) DO UPDATE
469
+ SET namespace = EXCLUDED.namespace,
470
+ content = EXCLUDED.content,
471
+ metadata = EXCLUDED.metadata,
472
+ embedding = EXCLUDED.embedding
473
+ `;
474
+ await client.query(query, values);
475
+ }
476
+ await client.query("COMMIT");
477
+ } catch (error) {
478
+ await client.query("ROLLBACK");
479
+ throw error;
480
+ } finally {
481
+ client.release();
482
+ }
483
+ }
484
+ async query(vector, topK, namespace, filter) {
485
+ const vectorLiteral = `[${vector.join(",")}]`;
486
+ let whereClause = namespace ? `WHERE namespace = $3` : "";
487
+ const params = [vectorLiteral, topK];
488
+ if (namespace) params.push(namespace);
489
+ const publicFilter = this.sanitizeFilter(filter);
490
+ if (Object.keys(publicFilter).length > 0) {
491
+ const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
492
+ const paramIdx = params.length + 1;
493
+ params.push(String(val));
494
+ return `metadata->>'${key}' = $${paramIdx}`;
495
+ }).join(" AND ");
496
+ whereClause = whereClause ? `${whereClause} AND ${filterConditions}` : `WHERE ${filterConditions}`;
497
+ }
498
+ const client = await this.pool.connect();
499
+ try {
500
+ const efSearch = this.config.options.efSearch || Math.max(topK * 10, 40);
501
+ await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
502
+ const result = await client.query(
503
+ `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
504
+ FROM ${this.tableName}
505
+ ${whereClause}
506
+ ORDER BY embedding <=> $1::vector
507
+ LIMIT $2`,
508
+ params
509
+ );
510
+ return result.rows.map((row) => ({
511
+ id: String(row["id"]),
512
+ score: parseFloat(String(row["score"])),
513
+ content: String(row["content"]),
514
+ metadata: row["metadata"]
515
+ }));
516
+ } finally {
517
+ client.release();
518
+ }
519
+ }
520
+ async delete(id, namespace) {
521
+ const where = namespace ? "WHERE id = $1 AND namespace = $2" : "WHERE id = $1";
522
+ const params = namespace ? [id, namespace] : [id];
523
+ await this.pool.query(`DELETE FROM ${this.tableName} ${where}`, params);
524
+ }
525
+ async deleteNamespace(namespace) {
526
+ await this.pool.query(`DELETE FROM ${this.tableName} WHERE namespace = $1`, [namespace]);
527
+ }
528
+ async ping() {
529
+ try {
530
+ await this.pool.query("SELECT 1");
531
+ return true;
532
+ } catch (e) {
533
+ return false;
534
+ }
535
+ }
536
+ async disconnect() {
537
+ await this.pool.end();
538
+ }
539
+ };
540
+ }
541
+ });
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
+
337
619
  // src/providers/vectordb/MultiTablePostgresProvider.ts
338
620
  var MultiTablePostgresProvider_exports = {};
339
621
  __export(MultiTablePostgresProvider_exports, {
340
622
  MultiTablePostgresProvider: () => MultiTablePostgresProvider
341
623
  });
342
- var import_pg, MultiTablePostgresProvider;
624
+ var import_pg2, MultiTablePostgresProvider;
343
625
  var init_MultiTablePostgresProvider = __esm({
344
626
  "src/providers/vectordb/MultiTablePostgresProvider.ts"() {
345
627
  "use strict";
346
- import_pg = require("pg");
628
+ import_pg2 = require("pg");
347
629
  init_BaseVectorProvider();
630
+ init_synonyms();
348
631
  MultiTablePostgresProvider = class extends BaseVectorProvider {
349
632
  constructor(config) {
350
633
  var _a, _b, _c;
@@ -361,7 +644,7 @@ var init_MultiTablePostgresProvider = __esm({
361
644
  this.uploadTable = opts.uploadTable || "document_chunks";
362
645
  }
363
646
  async initialize() {
364
- this.pool = new import_pg.Pool({ connectionString: this.connectionString });
647
+ this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
365
648
  const client = await this.pool.connect();
366
649
  try {
367
650
  await client.query("CREATE EXTENSION IF NOT EXISTS vector");
@@ -425,8 +708,13 @@ var init_MultiTablePostgresProvider = __esm({
425
708
  }
426
709
  tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
427
710
  const firstMeta = fileDocs[0].metadata || {};
428
- const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding"];
429
- 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
+ }
430
718
  const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
431
719
  const createTableSql = `
432
720
  CREATE TABLE IF NOT EXISTS "${tableName}" (
@@ -505,26 +793,41 @@ var init_MultiTablePostgresProvider = __esm({
505
793
  const allResults = [];
506
794
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
507
795
  const queryText = _filter == null ? void 0 : _filter.queryText;
508
- const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
509
- (hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
510
- ).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
796
+ const entityHints = Array.isArray(_filter == null ? void 0 : _filter.keywords) ? _filter.keywords.map((k) => k.trim().toLowerCase()).filter(Boolean) : [];
511
797
  console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
512
798
  console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
513
799
  const getDynamicKeywordQuery = () => {
514
800
  if (entityHints.length > 0) {
515
- return entityHints.map((h) => h.includes(" ") ? `"${h}"` : h).join(" & ");
801
+ return entityHints.map((h) => h.replace(/\s+/g, " & ")).join(" & ");
516
802
  }
517
803
  if (queryText) {
518
- return queryText.toLowerCase().replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|of|category|find|list|browse|what|are|display|all|under|for|in|with|about)\b/g, "").trim().replace(/\s+/g, " & ");
804
+ return queryText.toLowerCase().replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|organization[s]?|comp(?:any|anies)|of|category|find|list|browse|what|are|display|all|whose|that|which|have|has|having|greater|than|more|less|equal|equals|above|below|over|under|at|least|most|for|in|with|about|get|details|information|info)\b/g, "").replace(/\b\d+(?:\.\d+)?\b/g, "").trim().replace(/\s+/g, " & ");
519
805
  }
520
806
  return "";
521
807
  };
522
808
  const dynamicKeywordQuery = getDynamicKeywordQuery();
809
+ const tableLimit = Math.max(topK, 50);
810
+ const metadataFilters = _filter == null ? void 0 : _filter.metadata;
523
811
  const queryPromises = this.tables.map(async (table) => {
524
812
  try {
525
813
  let sqlQuery = "";
526
814
  let params = [];
527
- if (queryText) {
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
+ }
830
+ if (queryText && dynamicKeywordQuery) {
528
831
  const hasEntityHints = entityHints.length > 0;
529
832
  const exactNameScoreExpr = hasEntityHints ? `+ (
530
833
  SELECT COALESCE(MAX(
@@ -540,19 +843,21 @@ var init_MultiTablePostgresProvider = __esm({
540
843
  COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
541
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
542
845
  FROM "${table}" t
846
+ ${whereClause}
543
847
  ORDER BY hybrid_score DESC
544
- LIMIT 50
848
+ LIMIT ${tableLimit}
545
849
  `;
546
- params = [vectorLiteral, dynamicKeywordQuery];
850
+ params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
547
851
  } else {
548
852
  sqlQuery = `
549
853
  SELECT *,
550
- (1 - (embedding <=> $1::vector)) AS hybrid_score
854
+ (1 - (embedding <=> $1::vector)) AS hybrid_score
551
855
  FROM "${table}" t
856
+ ${whereClause}
552
857
  ORDER BY hybrid_score DESC
553
- LIMIT 50
858
+ LIMIT ${tableLimit}
554
859
  `;
555
- params = [vectorLiteral];
860
+ params = [vectorLiteral, ...filterParams];
556
861
  }
557
862
  const result = await this.pool.query(sqlQuery, params);
558
863
  if (result.rowCount && result.rowCount > 0) {
@@ -875,13 +1180,25 @@ var init_MilvusProvider = __esm({
875
1180
  };
876
1181
  await this.http.post("/v1/vector/upsert", payload);
877
1182
  }
878
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
879
1183
  async query(vector, topK, namespace, _filter) {
1184
+ const sanitizedFilter = this.sanitizeFilter(_filter);
1185
+ const filterParts = [];
1186
+ if (namespace) {
1187
+ filterParts.push(`namespace == "${namespace}"`);
1188
+ }
1189
+ for (const [key, value] of Object.entries(sanitizedFilter)) {
1190
+ if (key === "queryText" || key === "keywords") continue;
1191
+ if (typeof value === "string") {
1192
+ filterParts.push(`metadata["${key}"] == "${value.replace(/"/g, '\\"')}"`);
1193
+ } else if (typeof value === "number") {
1194
+ filterParts.push(`metadata["${key}"] == ${value}`);
1195
+ }
1196
+ }
880
1197
  const payload = {
881
1198
  collectionName: this.indexName,
882
1199
  vector,
883
1200
  limit: topK,
884
- filter: namespace ? `namespace == "${namespace}"` : void 0,
1201
+ filter: filterParts.length > 0 ? filterParts.join(" && ") : void 0,
885
1202
  outputFields: ["content", "metadata"],
886
1203
  searchParams: {
887
1204
  nprobe: this.config.options.nprobe || 16,
@@ -1080,6 +1397,7 @@ var init_QdrantProvider = __esm({
1080
1397
  await this.http.put(`/collections/${this.indexName}/points`, payload);
1081
1398
  }
1082
1399
  async query(vector, topK, namespace, _filter) {
1400
+ var _a;
1083
1401
  const must = [];
1084
1402
  if (namespace) {
1085
1403
  must.push({ key: "namespace", match: { value: namespace } });
@@ -1101,7 +1419,7 @@ var init_QdrantProvider = __esm({
1101
1419
  limit: topK,
1102
1420
  with_payload: true,
1103
1421
  params: {
1104
- hnsw_ef: this.config.options.efSearch || Math.max(topK * 20, 128),
1422
+ hnsw_ef: ((_a = this.config.options) == null ? void 0 : _a.efSearch) || Math.max(topK * 20, 128),
1105
1423
  exact: false
1106
1424
  },
1107
1425
  filter: must.length > 0 ? { must } : void 0
@@ -1226,12 +1544,20 @@ var init_ChromaDBProvider = __esm({
1226
1544
  };
1227
1545
  await this.http.post(`/api/v1/collections/${this.collectionId}/add`, payload);
1228
1546
  }
1229
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1230
1547
  async query(vector, topK, namespace, _filter) {
1548
+ const sanitizedFilter = this.sanitizeFilter(_filter);
1549
+ const whereClauses = [];
1550
+ if (namespace) whereClauses.push({ namespace: { $eq: namespace } });
1551
+ Object.entries(sanitizedFilter).forEach(([key, value]) => {
1552
+ if (key === "namespace") return;
1553
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1554
+ whereClauses.push({ [key]: { $eq: value } });
1555
+ }
1556
+ });
1231
1557
  const payload = {
1232
1558
  query_embeddings: [vector],
1233
1559
  n_results: topK,
1234
- where: namespace ? { namespace: { $eq: namespace } } : void 0
1560
+ where: whereClauses.length > 1 ? { $and: whereClauses } : whereClauses[0]
1235
1561
  };
1236
1562
  const { data } = await this.http.post(`/api/v1/collections/${this.collectionId}/query`, payload);
1237
1563
  const matches = [];
@@ -1336,15 +1662,15 @@ var init_RedisProvider = __esm({
1336
1662
  console.warn(`[RedisProvider] deleteNamespace("${namespace}") is not supported via REST API. Use Redis CLI: SCAN + DEL`);
1337
1663
  }
1338
1664
  /**
1339
- * Redis is TCP-based and has no HTTP health endpoint.
1340
- * Returns true; actual connectivity is validated on the first operation.
1665
+ * Check reachability via a PING command.
1666
+ * Returns false on connection failure so health checks surface real problems.
1341
1667
  */
1342
1668
  async ping() {
1343
1669
  try {
1344
1670
  await this.http.post("/", ["PING"]);
1345
1671
  return true;
1346
1672
  } catch (e) {
1347
- return true;
1673
+ return false;
1348
1674
  }
1349
1675
  }
1350
1676
  async disconnect() {
@@ -1384,15 +1710,17 @@ var init_WeaviateProvider = __esm({
1384
1710
  await this.ping();
1385
1711
  }
1386
1712
  async upsert(doc, namespace) {
1713
+ const primitiveMetadata = this.extractPrimitiveMetadata(doc.metadata);
1387
1714
  const payload = {
1388
1715
  class: this.indexName,
1389
1716
  id: doc.id,
1390
1717
  vector: doc.vector,
1391
- properties: {
1718
+ properties: __spreadProps(__spreadValues({
1392
1719
  content: doc.content,
1393
- metadata: JSON.stringify(doc.metadata || {}),
1720
+ metadata: JSON.stringify(doc.metadata || {})
1721
+ }, primitiveMetadata), {
1394
1722
  namespace: namespace || ""
1395
- }
1723
+ })
1396
1724
  };
1397
1725
  await this.http.post("/v1/objects", payload);
1398
1726
  }
@@ -1402,20 +1730,22 @@ var init_WeaviateProvider = __esm({
1402
1730
  class: this.indexName,
1403
1731
  id: doc.id,
1404
1732
  vector: doc.vector,
1405
- properties: {
1733
+ properties: __spreadProps(__spreadValues({
1406
1734
  content: doc.content,
1407
- metadata: JSON.stringify(doc.metadata || {}),
1735
+ metadata: JSON.stringify(doc.metadata || {})
1736
+ }, this.extractPrimitiveMetadata(doc.metadata)), {
1408
1737
  namespace: namespace || ""
1409
- }
1738
+ })
1410
1739
  }))
1411
1740
  };
1412
1741
  await this.http.post("/v1/batch/objects", payload);
1413
1742
  }
1414
1743
  async query(vector, topK, namespace, _filter) {
1415
1744
  var _a, _b;
1745
+ const queryText = _filter == null ? void 0 : _filter.queryText;
1416
1746
  const sanitizedFilter = this.sanitizeFilter(_filter);
1417
- const queryText = sanitizedFilter.queryText;
1418
1747
  const searchParams = queryText ? `hybrid: { query: ${JSON.stringify(queryText)}, alpha: 0.5 }` : `nearVector: { vector: ${JSON.stringify(vector)} }`;
1748
+ const where = this.buildWhereFilter(namespace, sanitizedFilter);
1419
1749
  const graphqlQuery = {
1420
1750
  query: `
1421
1751
  {
@@ -1423,7 +1753,7 @@ var init_WeaviateProvider = __esm({
1423
1753
  ${this.indexName}(
1424
1754
  ${searchParams}
1425
1755
  limit: ${topK}
1426
- ${namespace ? `where: { path: ["namespace"], operator: Equal, valueString: "${namespace}" }` : ""}
1756
+ ${where ? `where: ${where}` : ""}
1427
1757
  ) {
1428
1758
  content
1429
1759
  metadata
@@ -1467,6 +1797,34 @@ var init_WeaviateProvider = __esm({
1467
1797
  }
1468
1798
  async disconnect() {
1469
1799
  }
1800
+ extractPrimitiveMetadata(metadata) {
1801
+ const result = {};
1802
+ Object.entries(metadata || {}).forEach(([key, value]) => {
1803
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1804
+ result[key] = value;
1805
+ }
1806
+ });
1807
+ return result;
1808
+ }
1809
+ buildWhereFilter(namespace, filter) {
1810
+ const operands = [];
1811
+ if (namespace) operands.push(this.weaviateOperand("namespace", namespace));
1812
+ Object.entries(filter || {}).forEach(([key, value]) => {
1813
+ if (key === "namespace") return;
1814
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1815
+ operands.push(this.weaviateOperand(key, value));
1816
+ }
1817
+ });
1818
+ if (operands.length === 0) return void 0;
1819
+ if (operands.length === 1) return operands[0];
1820
+ return `{ operator: And, operands: [${operands.join(", ")}] }`;
1821
+ }
1822
+ weaviateOperand(key, value) {
1823
+ const path = `path: [${JSON.stringify(key)}], operator: Equal`;
1824
+ if (typeof value === "number") return `{ ${path}, valueNumber: ${value} }`;
1825
+ if (typeof value === "boolean") return `{ ${path}, valueBoolean: ${value} }`;
1826
+ return `{ ${path}, valueString: ${JSON.stringify(value)} }`;
1827
+ }
1470
1828
  };
1471
1829
  }
1472
1830
  });
@@ -1788,7 +2146,7 @@ function readEnum(env, name, fallback, allowed) {
1788
2146
  throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(", ")}`);
1789
2147
  }
1790
2148
  function getEnvConfig(env = process.env, base) {
1791
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa;
2149
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa, _Ga;
1792
2150
  const projectId = (_c = (_b = (_a = readString(env, "RAG_PROJECT_ID")) != null ? _a : readString(env, "NEXT_PUBLIC_PROJECT_ID")) != null ? _b : base == null ? void 0 : base.projectId) != null ? _c : "__default__";
1793
2151
  const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
1794
2152
  const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
@@ -1842,11 +2200,22 @@ function getEnvConfig(env = process.env, base) {
1842
2200
  };
1843
2201
  const embeddingApiKeyByProvider = {
1844
2202
  openai: readString(env, "OPENAI_API_KEY"),
2203
+ anthropic: readString(env, "ANTHROPIC_API_KEY"),
2204
+ // Anthropic needs a separate embedding provider; key kept for completeness
1845
2205
  gemini: readString(env, "GEMINI_API_KEY"),
1846
2206
  ollama: void 0,
1847
2207
  universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
1848
2208
  custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
1849
2209
  };
2210
+ const DEFAULT_MODEL_BY_PROVIDER = {
2211
+ openai: "gpt-4o",
2212
+ anthropic: "claude-3-5-sonnet-20241022",
2213
+ gemini: "gemini-2.0-flash",
2214
+ ollama: "llama3",
2215
+ universal_rest: "default",
2216
+ rest: "default",
2217
+ custom: "default"
2218
+ };
1850
2219
  return {
1851
2220
  projectId,
1852
2221
  vectorDb: {
@@ -1856,8 +2225,8 @@ function getEnvConfig(env = process.env, base) {
1856
2225
  },
1857
2226
  llm: {
1858
2227
  provider: llmProvider,
1859
- model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
1860
- apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
2228
+ model: (_ja = (_ia = readString(env, "LLM_MODEL")) != null ? _ia : DEFAULT_MODEL_BY_PROVIDER[llmProvider]) != null ? _ja : "gpt-4o",
2229
+ apiKey: (_ka = llmApiKeyByProvider[llmProvider]) != null ? _ka : "",
1861
2230
  baseUrl: readString(env, "LLM_BASE_URL"),
1862
2231
  systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
1863
2232
  maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
@@ -1868,7 +2237,7 @@ function getEnvConfig(env = process.env, base) {
1868
2237
  },
1869
2238
  embedding: {
1870
2239
  provider: embeddingProvider,
1871
- model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
2240
+ model: (_la = readString(env, "EMBEDDING_MODEL")) != null ? _la : "text-embedding-3-small",
1872
2241
  apiKey: embeddingApiKeyByProvider[embeddingProvider],
1873
2242
  baseUrl: readString(env, "EMBEDDING_BASE_URL"),
1874
2243
  dimensions: embeddingDimensions,
@@ -1879,17 +2248,17 @@ function getEnvConfig(env = process.env, base) {
1879
2248
  }
1880
2249
  },
1881
2250
  ui: {
1882
- title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
1883
- subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
1884
- primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
1885
- accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
1886
- logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
1887
- placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
1888
- showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
1889
- welcomeMessage: (_za = (_ya = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _ya : readString(env, "UI_WELCOME_MESSAGE")) != null ? _za : "Hello! I'm your AI assistant. Ask me anything about your documents.",
1890
- visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
1891
- borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
1892
- allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
2251
+ title: (_na = (_ma = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _ma : readString(env, "UI_TITLE")) != null ? _na : "AI Assistant",
2252
+ subtitle: (_pa = (_oa = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _oa : readString(env, "UI_SUBTITLE")) != null ? _pa : "Powered by RAG",
2253
+ primaryColor: (_ra = (_qa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _qa : readString(env, "UI_PRIMARY_COLOR")) != null ? _ra : "#10b981",
2254
+ accentColor: (_ta = (_sa = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _sa : readString(env, "UI_ACCENT_COLOR")) != null ? _ta : "#3b82f6",
2255
+ logoUrl: (_ua = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ua : readString(env, "UI_LOGO_URL"),
2256
+ placeholder: (_wa = (_va = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _va : readString(env, "UI_PLACEHOLDER")) != null ? _wa : "Ask me anything\u2026",
2257
+ showSources: ((_ya = (_xa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _xa : readString(env, "UI_SHOW_SOURCES")) != null ? _ya : "true") !== "false",
2258
+ welcomeMessage: (_Aa = (_za = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _za : readString(env, "UI_WELCOME_MESSAGE")) != null ? _Aa : "Hello! I'm your AI assistant. Ask me anything about your documents.",
2259
+ visualStyle: (_Ca = (_Ba = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Ba : readString(env, "UI_VISUAL_STYLE")) != null ? _Ca : "glass",
2260
+ borderRadius: (_Ea = (_Da = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Da : readString(env, "UI_BORDER_RADIUS")) != null ? _Ea : "xl",
2261
+ allowUpload: ((_Ga = (_Fa = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Fa : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Ga : "false") === "true"
1893
2262
  },
1894
2263
  rag: {
1895
2264
  topK: readNumber(env, "RAG_TOP_K", 5),
@@ -2751,7 +3120,7 @@ var LLM_PROFILES = {
2751
3120
  // src/llm/providers/UniversalLLMAdapter.ts
2752
3121
  var UniversalLLMAdapter = class {
2753
3122
  constructor(config) {
2754
- var _a, _b, _c, _d, _e, _f, _g;
3123
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2755
3124
  this.model = config.model;
2756
3125
  const llmConfig = config;
2757
3126
  const options = (_a = llmConfig.options) != null ? _a : {};
@@ -2765,16 +3134,18 @@ var UniversalLLMAdapter = class {
2765
3134
  this.systemPrompt = (_c = llmConfig.systemPrompt) != null ? _c : "You are a helpful AI assistant. Use the provided context to answer the user.";
2766
3135
  this.maxTokens = (_d = llmConfig.maxTokens) != null ? _d : 1024;
2767
3136
  this.temperature = (_e = llmConfig.temperature) != null ? _e : 0;
2768
- const baseUrl = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl;
2769
- if (!baseUrl) {
3137
+ this.apiKey = config.apiKey;
3138
+ this.baseUrl = (_g = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl) != null ? _g : "";
3139
+ if (!this.baseUrl) {
2770
3140
  throw new Error("[UniversalLLMAdapter] baseUrl is required in config or config.options");
2771
3141
  }
3142
+ this.resolvedHeaders = __spreadValues(__spreadValues({
3143
+ "Content-Type": "application/json"
3144
+ }, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {});
2772
3145
  this.http = import_axios2.default.create({
2773
- baseURL: baseUrl,
2774
- headers: __spreadValues(__spreadValues({
2775
- "Content-Type": "application/json"
2776
- }, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {}),
2777
- timeout: (_g = this.opts.timeout) != null ? _g : 6e4
3146
+ baseURL: this.baseUrl,
3147
+ headers: this.resolvedHeaders,
3148
+ timeout: (_h = this.opts.timeout) != null ? _h : 6e4
2778
3149
  });
2779
3150
  }
2780
3151
  async chat(messages, context) {
@@ -2811,6 +3182,92 @@ ${context != null ? context : "None"}` },
2811
3182
  }
2812
3183
  return String(result);
2813
3184
  }
3185
+ /**
3186
+ * Streaming chat using native fetch + ReadableStream.
3187
+ * Parses OpenAI-compatible SSE frames: `data: {...}\n\n`
3188
+ * Works with vLLM, LMStudio, Together AI, Fireworks, and any OpenAI-compatible API.
3189
+ */
3190
+ chatStream(messages, context) {
3191
+ return __asyncGenerator(this, null, function* () {
3192
+ var _a, _b, _c;
3193
+ const path = (_a = this.opts.chatPath) != null ? _a : "/chat/completions";
3194
+ const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
3195
+ const extractPath = ((_b = this.opts.responseExtractPath) != null ? _b : "choices[0].message.content").replace("message.content", "delta.content");
3196
+ const formattedMessages = [
3197
+ { role: "system", content: `${this.systemPrompt}
3198
+
3199
+ Context:
3200
+ ${context != null ? context : "None"}` },
3201
+ ...messages
3202
+ ];
3203
+ let payload;
3204
+ if (this.opts.chatPayloadTemplate) {
3205
+ payload = buildPayload(this.opts.chatPayloadTemplate, {
3206
+ model: this.model,
3207
+ messages: formattedMessages,
3208
+ maxTokens: this.maxTokens,
3209
+ temperature: this.temperature
3210
+ });
3211
+ if (typeof payload === "object" && payload !== null) {
3212
+ payload.stream = true;
3213
+ }
3214
+ } else {
3215
+ payload = {
3216
+ model: this.model,
3217
+ messages: formattedMessages,
3218
+ max_tokens: this.maxTokens,
3219
+ temperature: this.temperature,
3220
+ stream: true
3221
+ };
3222
+ }
3223
+ const response = yield new __await(fetch(url, {
3224
+ method: "POST",
3225
+ headers: this.resolvedHeaders,
3226
+ body: JSON.stringify(payload)
3227
+ }));
3228
+ if (!response.ok) {
3229
+ const errorText = yield new __await(response.text().catch(() => response.statusText));
3230
+ throw new Error(`[UniversalLLMAdapter] Streaming request failed (${response.status}): ${errorText}`);
3231
+ }
3232
+ if (!response.body) {
3233
+ throw new Error("[UniversalLLMAdapter] Response body is null \u2014 server did not send a streaming response.");
3234
+ }
3235
+ const reader = response.body.getReader();
3236
+ const decoder = new TextDecoder("utf-8");
3237
+ let buffer = "";
3238
+ try {
3239
+ while (true) {
3240
+ const { done, value } = yield new __await(reader.read());
3241
+ if (done) break;
3242
+ buffer += decoder.decode(value, { stream: true });
3243
+ const lines = buffer.split("\n");
3244
+ buffer = (_c = lines.pop()) != null ? _c : "";
3245
+ for (const line of lines) {
3246
+ const trimmed = line.trim();
3247
+ if (!trimmed || trimmed === "data: [DONE]") continue;
3248
+ if (!trimmed.startsWith("data:")) continue;
3249
+ try {
3250
+ const json = JSON.parse(trimmed.slice(5).trim());
3251
+ const text = resolvePath(json, extractPath);
3252
+ if (text && typeof text === "string") yield text;
3253
+ } catch (e) {
3254
+ }
3255
+ }
3256
+ }
3257
+ if (buffer.trim() && buffer.trim() !== "data: [DONE]") {
3258
+ const jsonStr = buffer.replace(/^data:\s*/, "").trim();
3259
+ try {
3260
+ const json = JSON.parse(jsonStr);
3261
+ const text = resolvePath(json, extractPath);
3262
+ if (text && typeof text === "string") yield text;
3263
+ } catch (e) {
3264
+ }
3265
+ }
3266
+ } finally {
3267
+ reader.releaseLock();
3268
+ }
3269
+ });
3270
+ }
2814
3271
  async embed(text) {
2815
3272
  var _a, _b;
2816
3273
  const path = (_a = this.opts.embedPath) != null ? _a : "/embeddings";
@@ -3007,6 +3464,7 @@ var ProviderRegistry = class {
3007
3464
  return null;
3008
3465
  }
3009
3466
  static async loadVectorProviderClass(provider) {
3467
+ var _a;
3010
3468
  if (this.vectorProviders[provider]) return this.vectorProviders[provider];
3011
3469
  switch (provider) {
3012
3470
  case "pinecone": {
@@ -3015,6 +3473,11 @@ var ProviderRegistry = class {
3015
3473
  }
3016
3474
  case "pgvector":
3017
3475
  case "postgresql": {
3476
+ const postgresMode = ((_a = process.env.POSTGRES_MODE) != null ? _a : "multi").toLowerCase();
3477
+ if (postgresMode === "single") {
3478
+ const { PostgreSQLProvider: PostgreSQLProvider2 } = await Promise.resolve().then(() => (init_PostgreSQLProvider(), PostgreSQLProvider_exports));
3479
+ return PostgreSQLProvider2;
3480
+ }
3018
3481
  const { MultiTablePostgresProvider: MultiTablePostgresProvider2 } = await Promise.resolve().then(() => (init_MultiTablePostgresProvider(), MultiTablePostgresProvider_exports));
3019
3482
  return MultiTablePostgresProvider2;
3020
3483
  }
@@ -3990,7 +4453,8 @@ var QueryProcessor = class {
3990
4453
  const fieldValuePatterns = [
3991
4454
  new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
3992
4455
  new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
3993
- new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
4456
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
4457
+ new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
3994
4458
  ];
3995
4459
  for (const pattern of fieldValuePatterns) {
3996
4460
  for (const match of question.matchAll(pattern)) {
@@ -4008,6 +4472,76 @@ var QueryProcessor = class {
4008
4472
  }
4009
4473
  return [...hints.values()];
4010
4474
  }
4475
+ static extractNumericPredicates(question, validFields = []) {
4476
+ const predicates = [];
4477
+ const seen = /* @__PURE__ */ new Set();
4478
+ const comparatorPattern = [
4479
+ "greater than or equal to",
4480
+ "more than or equal to",
4481
+ "less than or equal to",
4482
+ "greater than",
4483
+ "more than",
4484
+ "less than",
4485
+ "equal to",
4486
+ "at least",
4487
+ "at most",
4488
+ "above",
4489
+ "over",
4490
+ "below",
4491
+ "under",
4492
+ "equals?",
4493
+ ">=",
4494
+ "<=",
4495
+ ">",
4496
+ "<",
4497
+ "="
4498
+ ].join("|");
4499
+ const addPredicate = (rawField, rawOperator, rawValue) => {
4500
+ const value = Number(rawValue.replace(/,/g, ""));
4501
+ if (!Number.isFinite(value)) return;
4502
+ const operator = this.normalizeNumericOperator(rawOperator);
4503
+ const field = rawField ? this.normalizePredicateField(rawField, validFields) : void 0;
4504
+ const key = `${field != null ? field : "*"}::${operator}::${value}`;
4505
+ if (seen.has(key)) return;
4506
+ seen.add(key);
4507
+ predicates.push(__spreadProps(__spreadValues({}, field ? { field } : {}), { operator, value }));
4508
+ };
4509
+ const scopedPatterns = [
4510
+ new RegExp(`\\b(?:whose|with|having|where|that\\s+have|which\\s+have)\\s+([a-zA-Z][a-zA-Z0-9_\\s\\-/]{1,80}?)\\s+(?:is|are|was|were)?\\s*(?:${comparatorPattern})\\s+([\\d,]+(?:\\.\\d+)?)`, "gi"),
4511
+ new RegExp(`\\b([a-zA-Z][a-zA-Z0-9_\\s\\-/]{1,80}?)\\s+(?:is|are|was|were)?\\s*(?:${comparatorPattern})\\s+([\\d,]+(?:\\.\\d+)?)`, "gi")
4512
+ ];
4513
+ for (const pattern of scopedPatterns) {
4514
+ for (const match of question.matchAll(pattern)) {
4515
+ const full = match[0];
4516
+ const operatorMatch = full.match(new RegExp(`(${comparatorPattern})`, "i"));
4517
+ if (!operatorMatch) continue;
4518
+ addPredicate(match[1], operatorMatch[1], match[2]);
4519
+ }
4520
+ }
4521
+ for (const match of question.matchAll(/\b([a-zA-Z][a-zA-Z0-9_\s\-/]{1,80}?)\s*(>=|<=|>|<|=)\s*([\d,]+(?:\.\d+)?)/g)) {
4522
+ addPredicate(match[1], match[2], match[3]);
4523
+ }
4524
+ return predicates;
4525
+ }
4526
+ static normalizeNumericOperator(operator) {
4527
+ const op = operator.toLowerCase().trim();
4528
+ if (op === ">" || /\b(greater than|more than|above|over)\b/.test(op)) return "gt";
4529
+ if (op === ">=" || /\b(greater than or equal to|more than or equal to|at least)\b/.test(op)) return "gte";
4530
+ if (op === "<" || /\b(less than|below|under)\b/.test(op)) return "lt";
4531
+ if (op === "<=" || /\b(less than or equal to|at most)\b/.test(op)) return "lte";
4532
+ return "eq";
4533
+ }
4534
+ static normalizePredicateField(field, validFields) {
4535
+ const cleaned = this.normalizeHintValue(field).replace(/\b(?:provide|show|get|give|list|all|the|a|an|of|organizations?|companies?|records?|items?|whose|with|having|where|that|which|have|has|is|are|was|were)\b/gi, " ").replace(/\s+/g, " ").trim();
4536
+ if (validFields.length === 0) return cleaned;
4537
+ const comparable = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
4538
+ const cleanedComparable = comparable(cleaned);
4539
+ const matchedField = validFields.find((fieldName) => {
4540
+ const candidate = comparable(fieldName);
4541
+ return candidate === cleanedComparable || candidate.includes(cleanedComparable) || cleanedComparable.includes(candidate);
4542
+ });
4543
+ return matchedField != null ? matchedField : cleaned;
4544
+ }
4011
4545
  /**
4012
4546
  * Constructs a QueryFilter object from extracted hints.
4013
4547
  *
@@ -4028,6 +4562,10 @@ var QueryProcessor = class {
4028
4562
  }
4029
4563
  if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
4030
4564
  if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
4565
+ const numericPredicates = this.extractNumericPredicates(question);
4566
+ if (numericPredicates.length > 0) {
4567
+ filter.__numericPredicates = numericPredicates;
4568
+ }
4031
4569
  return filter;
4032
4570
  }
4033
4571
  /**
@@ -4095,102 +4633,360 @@ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
4095
4633
  }
4096
4634
  };
4097
4635
 
4098
- // src/utils/synonyms.ts
4099
- var FIELD_SYNONYMS = {
4100
- name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
4101
- price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
4102
- brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
4103
- image: [
4104
- "imageUrl",
4105
- "thumbnail",
4106
- "img",
4107
- "url",
4108
- "photo",
4109
- "picture",
4110
- "media",
4111
- "image_url",
4112
- "main_image",
4113
- "product_image",
4114
- "thumb"
4115
- ],
4116
- stock: [
4117
- "inventory",
4118
- "quantity",
4119
- "count",
4120
- "availability",
4121
- "stock_level",
4122
- "inStock",
4123
- "is_available",
4124
- "in stock",
4125
- "status"
4126
- ],
4127
- description: ["summary", "content", "body", "text", "info", "details"],
4128
- link: ["url", "href", "product_url", "page_url", "link"]
4636
+ // src/core/LLMRouter.ts
4637
+ var FAST_MODEL_DEFAULTS = {
4638
+ openai: "gpt-4o-mini",
4639
+ gemini: "gemini-2.0-flash",
4640
+ anthropic: "claude-3-haiku-20240307",
4641
+ ollama: "",
4642
+ // Ollama has no universal lightweight default — reuse main model
4643
+ rest: "",
4644
+ universal_rest: "",
4645
+ custom: ""
4129
4646
  };
4130
- function resolveMetadataValue(meta, uiKey) {
4131
- var _a;
4132
- const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
4133
- const keys = Object.keys(meta);
4134
- const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
4135
- let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
4136
- if (match !== void 0) return meta[match];
4137
- match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
4138
- if (match !== void 0) return meta[match];
4139
- const isBlacklisted = (kl) => {
4140
- return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
4141
- };
4142
- match = keys.find((k) => {
4143
- const kl = k.toLowerCase();
4144
- if (isBlacklisted(kl)) return false;
4145
- return kl.includes(uiKey.toLowerCase());
4146
- });
4147
- if (match !== void 0) return meta[match];
4148
- match = keys.find((k) => {
4149
- const kl = k.toLowerCase();
4150
- if (isBlacklisted(kl)) return false;
4151
- return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
4152
- });
4153
- return match !== void 0 ? meta[match] : void 0;
4154
- }
4155
-
4156
- // src/utils/UITransformer.ts
4157
- var UITransformer = class {
4158
- /**
4159
- * Main transformation method
4160
- * Analyzes user query and retrieved data to determine if a product carousel is needed.
4647
+ var LLMRouter = class {
4648
+ constructor(config) {
4649
+ this.config = config;
4650
+ this.models = /* @__PURE__ */ new Map();
4651
+ }
4652
+ /**
4653
+ * Initialize all LLM roles.
4654
+ *
4655
+ * @param prebuiltDefault - optional pre-built provider (from EmbeddingStrategyResolver).
4656
+ * When provided it is used directly as the 'default' role without re-constructing.
4657
+ */
4658
+ async initialize(prebuiltDefault) {
4659
+ var _a;
4660
+ const defaultModel = prebuiltDefault != null ? prebuiltDefault : LLMFactory.create(this.config.llm, this.config.embedding);
4661
+ this.models.set("default", defaultModel);
4662
+ const envFastModel = process.env.FAST_LLM_MODEL;
4663
+ const providerFastDefault = (_a = FAST_MODEL_DEFAULTS[this.config.llm.provider]) != null ? _a : "";
4664
+ const fastModelName = envFastModel || providerFastDefault;
4665
+ if (fastModelName && fastModelName !== this.config.llm.model) {
4666
+ console.log(`[LLMRouter] Fast role \u2192 provider="${this.config.llm.provider}" model="${fastModelName}"`);
4667
+ const fastConfig = __spreadProps(__spreadValues({}, this.config.llm), {
4668
+ model: fastModelName
4669
+ });
4670
+ this.models.set("fast", LLMFactory.create(fastConfig, this.config.embedding));
4671
+ } else {
4672
+ console.log(`[LLMRouter] Fast role \u2192 reusing default model (no lightweight alternative configured).`);
4673
+ this.models.set("fast", defaultModel);
4674
+ }
4675
+ this.models.set("powerful", defaultModel);
4676
+ }
4677
+ /**
4678
+ * Retrieve a model provider by its task role.
4679
+ * Falls back to 'default' if the requested role is not registered.
4680
+ */
4681
+ get(role) {
4682
+ var _a;
4683
+ const provider = (_a = this.models.get(role)) != null ? _a : this.models.get("default");
4684
+ if (!provider) {
4685
+ throw new Error(`[LLMRouter] No provider registered for role: "${role}". Did you call initialize()?`);
4686
+ }
4687
+ return provider;
4688
+ }
4689
+ };
4690
+
4691
+ // src/utils/UITransformer.ts
4692
+ init_synonyms();
4693
+ var UITransformer = class {
4694
+ // ─── Public Entry Points ─────────────────────────────────────────────────
4695
+ /**
4696
+ * Heuristic-only transform (no LLM required).
4697
+ * Uses the lightweight heuristic intent detector as a fallback.
4698
+ * Prefer `analyzeAndDecide()` in production.
4161
4699
  */
4162
- static transform(userQuery, retrievedData, config, trainedSchema) {
4700
+ static transform(userQuery, retrievedData, config, trainedSchema, intent) {
4701
+ var _a, _b, _c;
4163
4702
  if (!retrievedData || retrievedData.length === 0) {
4164
4703
  return this.createTextResponse("No data available", "No relevant data found for your query.");
4165
4704
  }
4166
- const isStockRequest = this.isStockQuery(userQuery);
4167
- const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
4168
- const categories = this.detectCategories(filteredData);
4705
+ const resolvedIntent = intent != null ? intent : this.detectIntentHeuristic(userQuery);
4706
+ const filteredData = resolvedIntent.filterInStockOnly ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
4707
+ const profile = this.profileData(filteredData);
4169
4708
  const hasProducts = filteredData.some((item) => this.isProductData(item));
4170
- const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
4171
- const isTrendQuery = this.isTrendQuery(userQuery);
4172
- if (isTrendQuery && isTimeSeries) {
4173
- return this.transformToLineChart(filteredData);
4709
+ const wantsPieLikeChart = ["pie_chart", "donut_chart"].includes(resolvedIntent.recommendedChart);
4710
+ if (resolvedIntent.visualizationHint === "trend" && profile.dateFields.length > 0 && profile.numericFields.length > 0) {
4711
+ return this.transformToLineChart(profile);
4174
4712
  }
4175
- if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
4176
- return this.transformToProductCarousel(filteredData, config, trainedSchema);
4713
+ if (wantsPieLikeChart || ["composition", "category_breakdown"].includes(resolvedIntent.visualizationHint)) {
4714
+ const pieChart = this.transformToPieChart(filteredData, profile, userQuery);
4715
+ if (pieChart) return pieChart;
4177
4716
  }
4178
- if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
4179
- return this.transformToPieChart(filteredData);
4717
+ if (["comparison", "ranking"].includes(resolvedIntent.visualizationHint)) {
4718
+ return this.transformToBarChart(filteredData, profile, userQuery, resolvedIntent.visualizationHint === "ranking");
4180
4719
  }
4181
- if (hasProducts) {
4182
- return this.transformToProductCarousel(filteredData, config, trainedSchema);
4720
+ if (resolvedIntent.visualizationHint === "distribution") {
4721
+ return (_a = this.transformToHistogram(profile, userQuery)) != null ? _a : this.transformToBarChart(filteredData, profile, userQuery);
4722
+ }
4723
+ if (resolvedIntent.visualizationHint === "correlation") {
4724
+ return (_b = this.transformToScatterPlot(profile, userQuery)) != null ? _b : this.transformToBarChart(filteredData, profile, userQuery);
4725
+ }
4726
+ if (resolvedIntent.visualizationHint === "geographic") {
4727
+ return this.transformToBarChart(filteredData, profile, userQuery);
4728
+ }
4729
+ if (this.isStructuredListQuery(userQuery)) {
4730
+ return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
4731
+ }
4732
+ if (resolvedIntent.visualizationHint === "kpi") {
4733
+ return (_c = this.transformToMetricCard(profile, userQuery)) != null ? _c : this.transformToText(filteredData);
4183
4734
  }
4184
- if (this.hasMultipleFields(filteredData)) {
4185
- return this.transformToTable(filteredData);
4735
+ if (["tabular", "table"].includes(resolvedIntent.visualizationHint) || resolvedIntent.wantsExplicitTable) {
4736
+ return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
4186
4737
  }
4738
+ if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === "product_browse") {
4739
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
4740
+ }
4741
+ const automatic = this.chooseAutomaticVisualization(filteredData, profile, userQuery);
4742
+ if (automatic) return automatic;
4187
4743
  return this.transformToText(filteredData);
4188
4744
  }
4189
4745
  /**
4190
- * Transform data to product carousel format
4746
+ * LLM-driven entry point (recommended for production).
4747
+ *
4748
+ * Step 1 — Detect intent via a dedicated, lightweight LLM call.
4749
+ * Step 2 — Pass the intent + data to the visualization-selection prompt.
4750
+ * Step 3 — Fall back to the heuristic `transform()` if either LLM call fails.
4751
+ */
4752
+ static async analyzeAndDecide(query, sources, llm) {
4753
+ let intent;
4754
+ try {
4755
+ intent = await this.detectIntent(query, llm);
4756
+ console.debug("[UITransformer] Detected intent:", intent);
4757
+ } catch (err) {
4758
+ console.warn("[UITransformer] Intent detection failed; using heuristic.", err);
4759
+ intent = this.detectIntentHeuristic(query);
4760
+ }
4761
+ if (this.isProductQuery(query) && ["text", "product_browse"].includes(intent.visualizationHint)) {
4762
+ return this.transform(
4763
+ query,
4764
+ sources,
4765
+ void 0,
4766
+ void 0,
4767
+ __spreadProps(__spreadValues({}, intent), { visualizationHint: "product_browse", recommendedChart: "text" })
4768
+ );
4769
+ }
4770
+ try {
4771
+ const context = this.buildContextSummary(sources);
4772
+ const systemPrompt = this.buildVisualizationSystemPrompt();
4773
+ const userPrompt = [
4774
+ `USER QUESTION: ${query}`,
4775
+ "",
4776
+ `DETECTED INTENT (JSON): ${JSON.stringify(intent)}`,
4777
+ "",
4778
+ "RETRIEVED DATA (JSON):",
4779
+ context
4780
+ ].join("\n");
4781
+ const rawResponse = await llm.chat(
4782
+ [{ role: "user", content: userPrompt }],
4783
+ "",
4784
+ { systemPrompt, temperature: 0 }
4785
+ );
4786
+ const parsed = this.parseTransformationResponse(rawResponse);
4787
+ if (parsed) {
4788
+ const intentAllowsTable = intent.wantsExplicitTable || ["tabular", "table", "geographic"].includes(intent.visualizationHint);
4789
+ const intentWantsPieLikeChart = ["pie_chart", "donut_chart"].includes(intent.recommendedChart) || ["composition", "category_breakdown"].includes(intent.visualizationHint);
4790
+ if (parsed.type === "table" && !intentAllowsTable) {
4791
+ console.debug("[UITransformer] LLM chose table but intent says no. Falling back to text.");
4792
+ return this.transform(query, sources, void 0, void 0, intent);
4793
+ }
4794
+ if (intentWantsPieLikeChart && parsed.type !== "pie_chart" && this.detectCategories(sources).length > 1) {
4795
+ console.debug("[UITransformer] LLM ignored pie/composition intent. Using deterministic pie chart.");
4796
+ return this.transform(query, sources, void 0, void 0, intent);
4797
+ }
4798
+ console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
4799
+ return parsed;
4800
+ }
4801
+ console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
4802
+ } catch (err) {
4803
+ console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
4804
+ }
4805
+ return this.transform(query, sources, void 0, void 0, intent);
4806
+ }
4807
+ // ─── Dynamic Intent Detection ─────────────────────────────────────────────
4808
+ /**
4809
+ * Calls the LLM with a compact, focused prompt to extract a structured
4810
+ * `QueryIntent` from the user's query.
4811
+ *
4812
+ * Keeping this as a *separate* call from visualization selection means:
4813
+ * - The prompt is shorter and more reliable.
4814
+ * - The intent object can be reused across both the heuristic and LLM paths.
4815
+ * - It is easy to unit-test intent detection in isolation.
4816
+ */
4817
+ static async detectIntent(query, llm) {
4818
+ const systemPrompt = `You are an intent classifier for a product-search RAG system.
4819
+ Given a user query, return ONLY a valid JSON object with this exact shape \u2014 no prose, no markdown:
4820
+
4821
+ {
4822
+ "visualizationHint": "trend" | "comparison" | "distribution" | "composition" | "correlation" | "ranking" | "kpi" | "tabular" | "geographic" | "product_browse" | "table" | "text",
4823
+ "recommendedChart": "line_chart" | "bar_chart" | "histogram" | "pie_chart" | "donut_chart" | "scatter_plot" | "horizontal_bar" | "metric_card" | "table" | "geo_map" | "text",
4824
+ "filterInStockOnly": boolean,
4825
+ "wantsExplicitTable": boolean,
4826
+ "isTemporal": boolean,
4827
+ "isComparison": boolean,
4828
+ "language": "<BCP-47 language tag, e.g. en, de, hi, ja>",
4829
+ "reasoning": "<one short sentence \u2014 why you chose these values>"
4830
+ }
4831
+
4832
+ RULES:
4833
+ - visualizationHint and recommendedChart
4834
+ "trend" \u2192 keywords: trend, growth, over time; recommendedChart "line_chart"
4835
+ "comparison" \u2192 keywords: compare, versus, top; recommendedChart "bar_chart"
4836
+ "distribution" \u2192 keywords: distribution, spread; recommendedChart "histogram"
4837
+ "composition" \u2192 keywords: share, percentage, breakup, breakdown; recommendedChart "pie_chart" or "donut_chart"
4838
+ "correlation" \u2192 keywords: relation, correlation; recommendedChart "scatter_plot"
4839
+ "ranking" \u2192 keywords: highest, lowest, top 10, ranking; recommendedChart "horizontal_bar"
4840
+ "kpi" \u2192 keywords: total, average, count; recommendedChart "metric_card"
4841
+ "tabular" \u2192 keywords: detailed records, table, grid, spreadsheet; recommendedChart "table"
4842
+ "geographic" \u2192 keywords: region, country, map; recommendedChart "geo_map"
4843
+ "product_browse" \u2192 user is browsing, searching, describing, viewing details for, or asking about one or more products without analytical visualization intent
4844
+ "table" \u2192 legacy alias for "tabular"; use only if the query literally says table
4845
+ "text" \u2192 conversational, factual, or other intent
4846
+ - If the user explicitly asks for a chart type, honor that chart type in recommendedChart.
4847
+ - If a query says "distribution ... in a pie chart", use visualizationHint "composition" and recommendedChart "pie_chart".
4848
+ - filterInStockOnly: true only if user mentions stock, availability, in stock, etc.
4849
+ - wantsExplicitTable: true if the user asks for a table, list, grid, spreadsheet, or detailed records.
4850
+ - isTemporal: true if time words appear (trend, historical, over time, last year, monthly, etc.)
4851
+ - isComparison: true if user compares, ranks, or contrasts entities or categories.
4852
+ - language: detect from the query text itself; default "en" if uncertain.`;
4853
+ const rawResponse = await llm.chat(
4854
+ [{ role: "user", content: `QUERY: ${query}` }],
4855
+ "",
4856
+ { systemPrompt, temperature: 0 }
4857
+ );
4858
+ const parsed = this.parseIntentResponse(rawResponse);
4859
+ if (!parsed) {
4860
+ throw new Error(`Could not parse intent JSON from LLM response: ${rawResponse}`);
4861
+ }
4862
+ return parsed;
4863
+ }
4864
+ /**
4865
+ * Parse and validate the raw LLM response into a `QueryIntent`.
4191
4866
  */
4867
+ static parseIntentResponse(raw) {
4868
+ const jsonStr = this.extractJsonCandidate(raw);
4869
+ if (!jsonStr) return null;
4870
+ try {
4871
+ const obj = JSON.parse(jsonStr);
4872
+ const validHints = [
4873
+ "trend",
4874
+ "comparison",
4875
+ "distribution",
4876
+ "composition",
4877
+ "correlation",
4878
+ "ranking",
4879
+ "kpi",
4880
+ "tabular",
4881
+ "geographic",
4882
+ "category_breakdown",
4883
+ "product_browse",
4884
+ "table",
4885
+ "text"
4886
+ ];
4887
+ const validCharts = [
4888
+ "line_chart",
4889
+ "bar_chart",
4890
+ "histogram",
4891
+ "pie_chart",
4892
+ "donut_chart",
4893
+ "scatter_plot",
4894
+ "horizontal_bar",
4895
+ "metric_card",
4896
+ "table",
4897
+ "geo_map",
4898
+ "text"
4899
+ ];
4900
+ const hint = obj.visualizationHint;
4901
+ if (!hint || !validHints.includes(hint)) return null;
4902
+ const normalizedHint = hint === "category_breakdown" ? "composition" : hint;
4903
+ const recommendedChart = typeof obj.recommendedChart === "string" && validCharts.includes(obj.recommendedChart) ? obj.recommendedChart : this.getRecommendedChartForIntent(normalizedHint);
4904
+ return {
4905
+ visualizationHint: normalizedHint,
4906
+ recommendedChart,
4907
+ filterInStockOnly: Boolean(obj.filterInStockOnly),
4908
+ wantsExplicitTable: Boolean(obj.wantsExplicitTable),
4909
+ isTemporal: Boolean(obj.isTemporal),
4910
+ isComparison: Boolean(obj.isComparison),
4911
+ language: typeof obj.language === "string" && obj.language ? obj.language : "en",
4912
+ reasoning: typeof obj.reasoning === "string" ? obj.reasoning : void 0
4913
+ };
4914
+ } catch (e) {
4915
+ return null;
4916
+ }
4917
+ }
4918
+ /**
4919
+ * Heuristic intent detector — used when the LLM is unavailable.
4920
+ * Intentionally minimal: it should only catch the most obvious signals.
4921
+ * The LLM path handles everything subtle.
4922
+ */
4923
+ static detectIntentHeuristic(query) {
4924
+ const q = query.toLowerCase();
4925
+ const isTemporal = /\b(trend|trends|over time|historical|history|growth|decline|monthly|yearly|weekly|daily|last (year|month|week)|timeline|forecast)\b/.test(q);
4926
+ const isRanking = /\b(highest|lowest|top\s*\d+|bottom\s*\d+|rank(?:ing)?|best|worst|leading)\b/.test(q);
4927
+ const isComparison = /\b(compare|comparison|vs\.?|versus|against|differ|difference|contrast|top)\b/.test(q) || isRanking;
4928
+ const isDistribution = /\b(distribution|spread|histogram|frequency|variance|range)\b/.test(q);
4929
+ const wantsPieLikeChart = /\b(pie|donut|doughnut)(?:\s+chart)?\b/.test(q);
4930
+ const isComposition = wantsPieLikeChart || /\b(share|percentage|percent|breakup|breakdown|composition|split|segmentation|by category|by type|proportion)\b/.test(q);
4931
+ const isCorrelation = /\b(relation|relationship|correlation|correlate|scatter|association|impact of|depend(?:s|ence)? on)\b/.test(q);
4932
+ const isKpi = /\b(total|average|avg|count|sum|median|minimum|maximum|metric|kpi|how many|number of)\b/.test(q);
4933
+ const isGeographic = /\b(region|country|countries|state|city|location|map|geo|geographic|territory)\b/.test(q);
4934
+ const filterInStockOnly = /\b(in[- ]?stock|available|availability|inventory|stock status)\b/.test(q);
4935
+ const wantsExplicitTable = /\b(table|spreadsheet|grid|detailed records|record details|list all|compare all)\b/.test(q);
4936
+ let visualizationHint = "text";
4937
+ if (wantsExplicitTable) visualizationHint = "tabular";
4938
+ else if (isTemporal) visualizationHint = "trend";
4939
+ else if (wantsPieLikeChart) visualizationHint = "composition";
4940
+ else if (isRanking) visualizationHint = "ranking";
4941
+ else if (isComparison) visualizationHint = "comparison";
4942
+ else if (isDistribution) visualizationHint = "distribution";
4943
+ else if (isComposition) visualizationHint = "composition";
4944
+ else if (isCorrelation) visualizationHint = "correlation";
4945
+ else if (isGeographic) visualizationHint = "geographic";
4946
+ else if (isKpi) visualizationHint = "kpi";
4947
+ else if (this.isProductQuery(query)) visualizationHint = "product_browse";
4948
+ return {
4949
+ visualizationHint,
4950
+ recommendedChart: this.getRecommendedChartForIntent(visualizationHint),
4951
+ filterInStockOnly,
4952
+ wantsExplicitTable,
4953
+ isTemporal,
4954
+ isComparison,
4955
+ language: "en"
4956
+ // heuristic cannot reliably detect language
4957
+ };
4958
+ }
4959
+ static getRecommendedChartForIntent(intent) {
4960
+ switch (intent) {
4961
+ case "trend":
4962
+ return "line_chart";
4963
+ case "comparison":
4964
+ return "bar_chart";
4965
+ case "distribution":
4966
+ return "histogram";
4967
+ case "composition":
4968
+ case "category_breakdown":
4969
+ return "pie_chart";
4970
+ case "correlation":
4971
+ return "scatter_plot";
4972
+ case "ranking":
4973
+ return "horizontal_bar";
4974
+ case "kpi":
4975
+ return "metric_card";
4976
+ case "tabular":
4977
+ case "table":
4978
+ return "table";
4979
+ case "geographic":
4980
+ return "geo_map";
4981
+ case "product_browse":
4982
+ case "text":
4983
+ default:
4984
+ return "text";
4985
+ }
4986
+ }
4987
+ // ─── Transform Helpers ────────────────────────────────────────────────────
4192
4988
  static transformToProductCarousel(data, config, trainedSchema) {
4193
- const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
4989
+ const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
4194
4990
  return {
4195
4991
  type: "product_carousel",
4196
4992
  title: "Recommended Products",
@@ -4198,55 +4994,154 @@ var UITransformer = class {
4198
4994
  data: products
4199
4995
  };
4200
4996
  }
4201
- /**
4202
- * Transform data to pie chart format
4203
- */
4204
- static transformToPieChart(data) {
4205
- const categories = this.detectCategories(data);
4206
- const categoryData = this.aggregateByCategory(data, categories);
4997
+ static transformToPieChart(data, profile, query = "") {
4998
+ var _a;
4999
+ const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
5000
+ const categories = dimension ? Array.from(new Set(profile.records.map((record) => {
5001
+ var _a2;
5002
+ return String((_a2 = record.fields[dimension.key]) != null ? _a2 : "");
5003
+ }).filter(Boolean))) : this.detectCategories(data);
5004
+ if (categories.length === 0) return null;
5005
+ const categoryData = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key) : this.aggregateByCategory(data, categories);
4207
5006
  const pieData = Object.entries(categoryData).map(([label, count]) => {
4208
5007
  const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
4209
- return {
4210
- label,
4211
- value: count,
4212
- inStockCount,
4213
- outOfStockCount
4214
- };
5008
+ return { label, value: count, inStockCount, outOfStockCount };
4215
5009
  });
4216
5010
  return {
4217
5011
  type: "pie_chart",
4218
- title: "Distribution by Category",
4219
- description: `Showing breakdown across ${categories.length} categories`,
5012
+ title: `Distribution by ${(_a = dimension == null ? void 0 : dimension.label) != null ? _a : "Category"}`,
5013
+ description: `Showing breakdown across ${pieData.length} categories`,
4220
5014
  data: pieData
4221
5015
  };
4222
5016
  }
4223
- /**
4224
- * Transform data to line chart format
4225
- */
4226
- static transformToLineChart(data) {
4227
- const timePoints = this.extractTimeSeriesData(data);
4228
- const lineData = timePoints.map((point) => ({
4229
- timestamp: point.timestamp,
4230
- value: point.value,
4231
- label: point.label
4232
- }));
5017
+ static transformToLineChart(profile) {
5018
+ const dateField = profile.dateFields[0];
5019
+ const valueField = profile.numericFields[0];
5020
+ const buckets = /* @__PURE__ */ new Map();
5021
+ profile.records.forEach((record) => {
5022
+ var _a, _b, _c;
5023
+ const timestamp = String((_a = record.fields[dateField.key]) != null ? _a : "");
5024
+ const value = (_b = this.toFiniteNumber(record.fields[valueField.key])) != null ? _b : 0;
5025
+ buckets.set(timestamp, ((_c = buckets.get(timestamp)) != null ? _c : 0) + value);
5026
+ });
5027
+ const lineData = Array.from(buckets.entries()).sort(([a], [b]) => Date.parse(a) - Date.parse(b)).slice(0, 24).map(([timestamp, value]) => ({ timestamp, value, label: timestamp }));
4233
5028
  return {
4234
5029
  type: "line_chart",
4235
- title: "Trend Over Time",
5030
+ title: `${valueField.label} Over Time`,
4236
5031
  description: `Showing ${lineData.length} data points`,
4237
5032
  data: lineData
4238
5033
  };
4239
5034
  }
4240
- /**
4241
- * Transform data to table format
4242
- */
4243
- static transformToTable(data) {
4244
- const columns = this.extractTableColumns(data);
4245
- const rows = data.map((item) => this.extractTableRow(item, columns));
4246
- const tableData = {
4247
- columns,
4248
- rows
5035
+ static transformToBarChart(data, profile, query = "", horizontal = false) {
5036
+ var _a;
5037
+ const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
5038
+ const measure = profile ? this.selectNumericField(profile, query) : void 0;
5039
+ const aggregate = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key, measure == null ? void 0 : measure.key) : this.aggregateByCategory(data, this.detectCategories(data));
5040
+ const barData = Object.entries(aggregate).map(([category, value]) => ({ category, value: Number(value) })).sort((a, b) => horizontal ? b.value - a.value : 0).slice(0, 12);
5041
+ const fallbackData = barData.length > 0 ? barData : data.slice(0, 12).map((item, index) => {
5042
+ var _a2, _b, _c, _d, _e;
5043
+ const meta = item.metadata || {};
5044
+ const label = String(
5045
+ (_c = (_b = (_a2 = this.getDynamicVal(meta, "name")) != null ? _a2 : meta.label) != null ? _b : meta.title) != null ? _c : `Result ${index + 1}`
5046
+ );
5047
+ const value = (_e = (_d = this.extractNumericValue(meta)) != null ? _d : item.score) != null ? _e : 0;
5048
+ return { category: label, value: Number(value) };
5049
+ });
5050
+ return {
5051
+ type: horizontal ? "horizontal_bar" : "bar_chart",
5052
+ title: dimension ? `${(_a = measure == null ? void 0 : measure.label) != null ? _a : "Count"} by ${dimension.label}` : "Comparison",
5053
+ description: `Showing ${fallbackData.length} comparable values`,
5054
+ data: fallbackData
5055
+ };
5056
+ }
5057
+ static transformToHistogram(profile, query = "") {
5058
+ const field = this.selectNumericField(profile, query);
5059
+ if (!field) return null;
5060
+ const values = profile.records.map((record) => this.toFiniteNumber(record.fields[field.key])).filter((value) => value !== null).sort((a, b) => a - b);
5061
+ if (values.length === 0) return null;
5062
+ const min = values[0];
5063
+ const max = values[values.length - 1];
5064
+ const bucketCount = Math.min(10, Math.max(3, Math.ceil(Math.sqrt(values.length))));
5065
+ const width = max === min ? 1 : (max - min) / bucketCount;
5066
+ const buckets = Array.from({ length: bucketCount }, (_, index) => {
5067
+ const start = min + index * width;
5068
+ const end = index === bucketCount - 1 ? max : start + width;
5069
+ return { category: `${this.formatNumber(start)}-${this.formatNumber(end)}`, value: 0 };
5070
+ });
5071
+ values.forEach((value) => {
5072
+ const bucketIndex = max === min ? 0 : Math.min(bucketCount - 1, Math.floor((value - min) / width));
5073
+ buckets[bucketIndex].value += 1;
5074
+ });
5075
+ return {
5076
+ type: "histogram",
5077
+ title: `${field.label} Distribution`,
5078
+ description: `Showing ${values.length} values across ${bucketCount} buckets`,
5079
+ data: buckets
5080
+ };
5081
+ }
5082
+ static transformToScatterPlot(profile, query = "") {
5083
+ const fields = this.rankFieldsByQuery(profile.numericFields, query).slice(0, 2);
5084
+ if (fields.length < 2) return null;
5085
+ const [xField, yField] = fields;
5086
+ const points = profile.records.map((record) => {
5087
+ var _a;
5088
+ const x = this.toFiniteNumber(record.fields[xField.key]);
5089
+ const y = this.toFiniteNumber(record.fields[yField.key]);
5090
+ if (x === null || y === null) return null;
5091
+ return { x, y, label: String((_a = this.getRecordLabel(record)) != null ? _a : record.id) };
5092
+ }).filter((point) => point !== null).slice(0, 100);
5093
+ if (points.length === 0) return null;
5094
+ return {
5095
+ type: "scatter_plot",
5096
+ title: `${xField.label} vs ${yField.label}`,
5097
+ description: `Showing ${points.length} paired values`,
5098
+ data: points
5099
+ };
5100
+ }
5101
+ static transformToMetricCard(profile, query = "") {
5102
+ const operation = this.detectAggregationOperation(query);
5103
+ const numericField = this.selectNumericField(profile, query);
5104
+ const values = numericField ? profile.records.map((record) => this.toFiniteNumber(record.fields[numericField.key])).filter((value2) => value2 !== null) : [];
5105
+ const value = operation === "count" || values.length === 0 ? profile.records.length : this.calculateAggregate(values, operation);
5106
+ const metric = {
5107
+ label: numericField ? `${operation} ${numericField.label}` : "Count",
5108
+ value,
5109
+ operation
5110
+ };
5111
+ return {
5112
+ type: "metric_card",
5113
+ title: metric.label,
5114
+ description: `Calculated from ${profile.records.length} result${profile.records.length === 1 ? "" : "s"}`,
5115
+ data: metric
5116
+ };
5117
+ }
5118
+ static transformToRadarChart(data) {
5119
+ const attributeMap = {};
5120
+ data.forEach((item) => {
5121
+ var _a, _b, _c;
5122
+ const meta = item.metadata || {};
5123
+ const seriesName = String((_c = (_b = (_a = meta.name) != null ? _a : meta.product) != null ? _b : item.id) != null ? _c : "Item");
5124
+ Object.entries(meta).forEach(([key, val]) => {
5125
+ if (typeof val === "number" && !["price", "id"].includes(key.toLowerCase())) {
5126
+ if (!attributeMap[key]) attributeMap[key] = {};
5127
+ attributeMap[key][seriesName] = val;
5128
+ }
5129
+ });
5130
+ });
5131
+ const radarData = Object.entries(attributeMap).map(([attribute, series]) => __spreadValues({
5132
+ attribute
5133
+ }, series));
5134
+ return {
5135
+ type: "radar_chart",
5136
+ title: "Product Comparison",
5137
+ description: `Comparing ${data.length} items across ${radarData.length} attributes`,
5138
+ data: radarData.length > 0 ? radarData : data.map((d) => ({ attribute: d.content.substring(0, 40) }))
4249
5139
  };
5140
+ }
5141
+ static transformToTable(data, query = "") {
5142
+ const columns = this.extractTableColumns(data, query);
5143
+ const rows = data.map((item) => this.extractTableRow(item, columns));
5144
+ const tableData = { columns, rows };
4250
5145
  return {
4251
5146
  type: "table",
4252
5147
  title: "Detailed Results",
@@ -4254,28 +5149,17 @@ var UITransformer = class {
4254
5149
  data: tableData
4255
5150
  };
4256
5151
  }
4257
- /**
4258
- * Transform data to text format (fallback)
4259
- */
4260
5152
  static transformToText(data) {
4261
- const textContent = data.map((item) => item.content).join("\n\n");
4262
5153
  return this.createTextResponse(
4263
5154
  "Search Results",
4264
- textContent,
5155
+ data.map((item) => item.content).join("\n\n"),
4265
5156
  `Found ${data.length} relevant results`
4266
5157
  );
4267
5158
  }
4268
- /**
4269
- * Helper: Create text response
4270
- */
4271
5159
  static createTextResponse(title, content, description) {
4272
- return {
4273
- type: "text",
4274
- title,
4275
- description,
4276
- data: { content }
4277
- };
5160
+ return { type: "text", title, description, data: { content } };
4278
5161
  }
5162
+ // ─── LLM Response Parsing ─────────────────────────────────────────────────
4279
5163
  static parseTransformationResponse(raw) {
4280
5164
  const payloadText = this.extractJsonCandidate(raw);
4281
5165
  if (!payloadText) return null;
@@ -4312,9 +5196,7 @@ var UITransformer = class {
4312
5196
  if (char === "{") depth += 1;
4313
5197
  if (char === "}") {
4314
5198
  depth -= 1;
4315
- if (depth === 0) {
4316
- return cleaned.slice(start, i + 1);
4317
- }
5199
+ if (depth === 0) return cleaned.slice(start, i + 1);
4318
5200
  }
4319
5201
  }
4320
5202
  return null;
@@ -4322,19 +5204,16 @@ var UITransformer = class {
4322
5204
  static normalizeTransformation(payload) {
4323
5205
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
4324
5206
  if (!payload || typeof payload !== "object") return null;
4325
- const payloadObj = payload;
4326
- const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
5207
+ const p = payload;
5208
+ const type = this.normalizeVisualizationType(
5209
+ String((_c = (_b = (_a = p.type) != null ? _a : p.view) != null ? _b : p.chartType) != null ? _c : "")
5210
+ );
4327
5211
  if (!type) return null;
4328
- const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
4329
- const description = payloadObj.description ? String(payloadObj.description) : void 0;
4330
- const rawData = (_j = (_i = (_h = (_g = (_f = payloadObj.data) != null ? _f : payloadObj.table) != null ? _g : payloadObj.rows) != null ? _h : payloadObj.items) != null ? _i : payloadObj.content) != null ? _j : null;
5212
+ const title = String((_e = (_d = p.title) != null ? _d : p.heading) != null ? _e : "Visualization");
5213
+ const description = p.description ? String(p.description) : void 0;
5214
+ const rawData = (_j = (_i = (_h = (_g = (_f = p.data) != null ? _f : p.table) != null ? _g : p.rows) != null ? _h : p.items) != null ? _i : p.content) != null ? _j : null;
4331
5215
  const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
4332
- const transformation = {
4333
- type,
4334
- title,
4335
- description,
4336
- data
4337
- };
5216
+ const transformation = { type, title, description, data };
4338
5217
  return this.validateTransformation(transformation) ? transformation : null;
4339
5218
  }
4340
5219
  static normalizeVisualizationType(type) {
@@ -4344,10 +5223,23 @@ var UITransformer = class {
4344
5223
  pie_chart: "pie_chart",
4345
5224
  bar: "bar_chart",
4346
5225
  bar_chart: "bar_chart",
5226
+ histogram: "histogram",
5227
+ horizontal_bar: "horizontal_bar",
5228
+ horizontalbar: "horizontal_bar",
4347
5229
  line: "line_chart",
4348
5230
  line_chart: "line_chart",
5231
+ scatter: "scatter_plot",
5232
+ scatter_plot: "scatter_plot",
5233
+ scatterplot: "scatter_plot",
4349
5234
  radar: "radar_chart",
4350
5235
  radar_chart: "radar_chart",
5236
+ metric: "metric_card",
5237
+ metric_card: "metric_card",
5238
+ card: "metric_card",
5239
+ kpi: "metric_card",
5240
+ geo: "geo_map",
5241
+ geo_map: "geo_map",
5242
+ map: "geo_map",
4351
5243
  table: "table",
4352
5244
  text: "text",
4353
5245
  product_carousel: "product_carousel",
@@ -4355,21 +5247,31 @@ var UITransformer = class {
4355
5247
  };
4356
5248
  return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
4357
5249
  }
4358
- static validateTransformation(transformation) {
4359
- const { type, data } = transformation;
5250
+ static validateTransformation(t) {
5251
+ const { type, data } = t;
4360
5252
  switch (type) {
4361
5253
  case "pie_chart":
4362
5254
  case "bar_chart":
5255
+ case "histogram":
5256
+ case "horizontal_bar":
5257
+ return Array.isArray(data) && data.every(
5258
+ (i) => i !== null && typeof i === "object" && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
5259
+ );
5260
+ case "scatter_plot":
4363
5261
  return Array.isArray(data) && data.every(
4364
- (item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
5262
+ (i) => i !== null && typeof i === "object" && (typeof i.x === "number" || !Number.isNaN(Number(i.x))) && (typeof i.y === "number" || !Number.isNaN(Number(i.y)))
4365
5263
  );
4366
5264
  case "line_chart":
4367
5265
  return Array.isArray(data) && data.every(
4368
- (item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
5266
+ (i) => i !== null && typeof i === "object" && "timestamp" in i && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
4369
5267
  );
5268
+ case "metric_card":
5269
+ return typeof data === "object" && data !== null && (typeof data.value === "number" || !Number.isNaN(Number(data.value)));
5270
+ case "geo_map":
5271
+ return Array.isArray(data) || typeof data === "object" && data !== null;
4370
5272
  case "radar_chart":
4371
5273
  return Array.isArray(data) && data.every(
4372
- (item) => item !== null && typeof item === "object" && "attribute" in item
5274
+ (i) => i !== null && typeof i === "object" && "attribute" in i
4373
5275
  );
4374
5276
  case "table":
4375
5277
  return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
@@ -4378,15 +5280,27 @@ var UITransformer = class {
4378
5280
  case "product_carousel":
4379
5281
  case "carousel":
4380
5282
  return Array.isArray(data) && data.every(
4381
- (item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
5283
+ (i) => i !== null && typeof i === "object" && ("id" in i || "name" in i)
4382
5284
  );
4383
5285
  default:
4384
5286
  return false;
4385
5287
  }
4386
5288
  }
4387
- /**
4388
- * Helper: Check if data item is product-related
4389
- */
5289
+ // ─── Data Inspection Helpers ──────────────────────────────────────────────
5290
+ static isStructuredListQuery(query) {
5291
+ const q = query.toLowerCase();
5292
+ const asksForRecords = /\b(list|provide|show|give|get|find|which|whose|records?|details?)\b/.test(q);
5293
+ const hasNumericPredicate = /\b(greater than|more than|above|over|less than|below|under|at least|at most|minimum|maximum|>=|<=|>|<|equals?|equal to)\b\s*\$?\d/i.test(q) || /\b\d+\b\s*(?:or more|or less|and above|and below)\b/i.test(q);
5294
+ const hasEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
5295
+ return asksForRecords && (hasNumericPredicate || hasEntityTerms);
5296
+ }
5297
+ static isProductQuery(query) {
5298
+ const q = query.toLowerCase();
5299
+ const productTerms = /\b(product|products|item|items|sku|catalog|catalogue|price|prices|brand|model|stock|inventory|in stock|out of stock|buy|shop)\b/.test(q);
5300
+ const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list)\b/.test(q);
5301
+ const nonProductEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
5302
+ return productTerms && productAction && !nonProductEntityTerms;
5303
+ }
4390
5304
  static isProductData(item) {
4391
5305
  const content = (item.content || "").toLowerCase();
4392
5306
  const productKeywords = [
@@ -4412,360 +5326,582 @@ var UITransformer = class {
4412
5326
  "fragrance"
4413
5327
  ];
4414
5328
  const hasKeywords = productKeywords.some((kw) => content.includes(kw));
4415
- const hasMetadataKey = Object.keys(item.metadata || {}).some(
4416
- (k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
4417
- );
4418
- const hasPricePattern = /\$\s*\d+/.test(content);
5329
+ const metadata = item.metadata || {};
5330
+ const hasMetadataKey = Object.keys(metadata).some((k) => {
5331
+ const val = metadata[k];
5332
+ return ["price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase()) && val !== null && val !== void 0 && val !== "";
5333
+ });
5334
+ const hasPricePattern = /\$\s*\d+\.\d{2}/.test(content);
4419
5335
  return hasKeywords || hasMetadataKey || hasPricePattern;
4420
5336
  }
4421
- /**
4422
- * Helper: Check if data contains time series
4423
- */
4424
5337
  static isTimeSeriesData(item) {
4425
- const content = (item.content || "").toLowerCase();
4426
- const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
4427
- const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
4428
5338
  const metadata = item.metadata || {};
4429
5339
  const maybeDateKeys = Object.keys(metadata).filter(
4430
5340
  (k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
4431
5341
  );
4432
- const hasValidDateValue = maybeDateKeys.some((key) => {
5342
+ return maybeDateKeys.some((key) => {
4433
5343
  const value = metadata[key];
4434
- if (typeof value === "string") {
4435
- return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
4436
- }
5344
+ if (typeof value === "string") return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
4437
5345
  return typeof value === "number";
4438
5346
  });
4439
- return hasTimeKeyword || hasValidDateValue;
4440
5347
  }
4441
- static shouldShowCategoryChart(query, categories) {
4442
- if (categories.length < 2) {
4443
- return false;
4444
- }
4445
- const normalized = query.toLowerCase();
4446
- const chartKeywords = [
4447
- "distribution",
4448
- "breakdown",
4449
- "by category",
4450
- "by type",
4451
- "compare",
4452
- "share",
4453
- "percentage",
4454
- "segmentation",
4455
- "split",
4456
- "category breakdown",
4457
- "category distribution"
4458
- ];
4459
- return chartKeywords.some((keyword) => normalized.includes(keyword));
5348
+ static hasMultipleFields(data) {
5349
+ const fieldCount = /* @__PURE__ */ new Set();
5350
+ data.forEach((item) => Object.keys(item.metadata || {}).forEach((k) => fieldCount.add(k)));
5351
+ return fieldCount.size > 2;
4460
5352
  }
4461
- static isTrendQuery(query) {
4462
- const normalized = query.toLowerCase();
4463
- const trendKeywords = [
4464
- "trend",
4465
- "over time",
4466
- "historical",
4467
- "growth",
4468
- "decline",
4469
- "increase",
4470
- "decrease",
4471
- "year",
4472
- "month",
4473
- "week",
4474
- "day",
4475
- "comparison",
4476
- "compare",
4477
- "changes",
4478
- "timeline"
4479
- ];
4480
- return trendKeywords.some((keyword) => normalized.includes(keyword));
5353
+ static profileData(data) {
5354
+ const records = data.map((item) => {
5355
+ const fields2 = {};
5356
+ Object.entries(item.metadata || {}).forEach(([key, value]) => {
5357
+ const primitive = this.toPrimitive(value);
5358
+ if (primitive !== null) fields2[key] = primitive;
5359
+ });
5360
+ if (!fields2.content && item.content) fields2.content = item.content.substring(0, 500);
5361
+ return {
5362
+ id: item.id,
5363
+ content: item.content,
5364
+ score: item.score,
5365
+ fields: fields2,
5366
+ source: item
5367
+ };
5368
+ });
5369
+ const keys = Array.from(new Set(records.flatMap((record) => Object.keys(record.fields))));
5370
+ const fields = keys.map((key) => {
5371
+ const values = records.map((record) => record.fields[key]).filter((value) => value !== void 0 && value !== null && String(value).trim() !== "");
5372
+ const uniqueCount = new Set(values.map((value) => String(value).toLowerCase())).size;
5373
+ return {
5374
+ key,
5375
+ label: this.humanizeFieldName(key),
5376
+ kind: this.inferFieldKind(key, values, records.length, uniqueCount),
5377
+ values,
5378
+ uniqueCount
5379
+ };
5380
+ });
5381
+ return {
5382
+ records,
5383
+ fields,
5384
+ numericFields: fields.filter((field) => field.kind === "number"),
5385
+ dateFields: fields.filter((field) => field.kind === "date"),
5386
+ categoricalFields: fields.filter((field) => field.kind === "category"),
5387
+ booleanFields: fields.filter((field) => field.kind === "boolean")
5388
+ };
4481
5389
  }
4482
- static isStockQuery(query) {
4483
- const normalized = query.toLowerCase();
4484
- return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
5390
+ static toPrimitive(value) {
5391
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
5392
+ if (value === null || value === void 0) return null;
5393
+ if (value instanceof Date) return value.toISOString();
5394
+ return null;
4485
5395
  }
4486
- /**
4487
- * Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
4488
- */
4489
- static getDynamicVal(meta, uiKey, config, trainedSchema) {
4490
- var _a;
4491
- if (!meta) return void 0;
4492
- const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
4493
- if (mapping && mapping[uiKey]) {
4494
- const mappedKey = mapping[uiKey];
4495
- if (meta[mappedKey] !== void 0) return meta[mappedKey];
5396
+ static inferFieldKind(key, values, rowCount, uniqueCount) {
5397
+ const lowerKey = key.toLowerCase();
5398
+ if (values.length === 0) return "text";
5399
+ if (values.every((value) => typeof value === "boolean" || /^(true|false|yes|no|in_stock|out_of_stock|available|unavailable)$/i.test(String(value)))) {
5400
+ return "boolean";
4496
5401
  }
4497
- if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
4498
- const trainedKey = trainedSchema[uiKey];
4499
- if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
5402
+ if (/(date|time|timestamp|created|updated|month|year|period)/i.test(lowerKey) && values.some((value) => this.isDateLike(value))) {
5403
+ return "date";
4500
5404
  }
4501
- return resolveMetadataValue(meta, uiKey);
5405
+ const numericCount = values.filter((value) => this.toFiniteNumber(value) !== null).length;
5406
+ if (numericCount / values.length >= 0.85 && !/(id|sku|ean|upc|zip|postal|phone|code)/i.test(lowerKey)) {
5407
+ return "number";
5408
+ }
5409
+ if (uniqueCount <= Math.max(20, Math.ceil(rowCount * 0.7)) && !/(id|sku|ean|upc|description|content|url|image|thumbnail)/i.test(lowerKey)) {
5410
+ return "category";
5411
+ }
5412
+ return "text";
4502
5413
  }
4503
- static extractProductInfo(item, config, trainedSchema) {
4504
- const meta = item.metadata || {};
4505
- const name = this.getDynamicVal(meta, "name", config, trainedSchema);
4506
- const price = this.getDynamicVal(meta, "price", config, trainedSchema);
4507
- const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
4508
- if (name || this.isProductData(item)) {
4509
- let finalName = name ? String(name) : void 0;
4510
- if (!finalName) {
4511
- const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
4512
- finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
4513
- }
4514
- let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
4515
- if (!finalPrice) {
4516
- const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
4517
- if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
4518
- }
4519
- const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
4520
- return {
4521
- id: item.id,
4522
- name: finalName,
4523
- price: finalPrice,
4524
- image: typeof imageValue === "string" ? imageValue : void 0,
4525
- brand: brand ? String(brand) : void 0,
4526
- description: item.content,
4527
- inStock: this.determineStockStatus(item)
4528
- };
5414
+ static isDateLike(value) {
5415
+ if (typeof value === "number") return value > 1900 && value < 3e3;
5416
+ const text = String(value).trim();
5417
+ return text.length > 3 && !Number.isNaN(Date.parse(text));
5418
+ }
5419
+ static chooseAutomaticVisualization(data, profile, query) {
5420
+ if (profile.records.length === 0) return null;
5421
+ if (profile.dateFields.length > 0 && profile.numericFields.length > 0) {
5422
+ return this.transformToLineChart(profile);
5423
+ }
5424
+ if (profile.categoricalFields.length > 0 && profile.numericFields.length > 0) {
5425
+ return this.transformToBarChart(data, profile, query);
5426
+ }
5427
+ if (profile.categoricalFields.length > 0) {
5428
+ return this.transformToPieChart(data, profile, query);
5429
+ }
5430
+ if (profile.numericFields.length >= 2) {
5431
+ return this.transformToScatterPlot(profile, query);
5432
+ }
5433
+ if (profile.numericFields.length === 1) {
5434
+ return this.transformToMetricCard(profile, query);
4529
5435
  }
4530
5436
  return null;
4531
5437
  }
4532
- /**
4533
- * Helper: Detect categories in data
4534
- */
5438
+ static selectDimensionField(profile, query) {
5439
+ var _a, _b;
5440
+ const productCategory = profile.categoricalFields.find(
5441
+ (field) => /category|department|collection|type|group|segment|region|country|state|city/i.test(field.key)
5442
+ );
5443
+ const ranked = this.rankFieldsByQuery(profile.categoricalFields, query);
5444
+ return (_b = (_a = ranked[0]) != null ? _a : productCategory) != null ? _b : profile.categoricalFields[0];
5445
+ }
5446
+ static selectNumericField(profile, query) {
5447
+ var _a;
5448
+ return (_a = this.rankFieldsByQuery(profile.numericFields, query)[0]) != null ? _a : profile.numericFields[0];
5449
+ }
5450
+ static rankFieldsByQuery(fields, query) {
5451
+ const q = query.toLowerCase();
5452
+ return [...fields].sort((a, b) => this.fieldScore(b, q) - this.fieldScore(a, q));
5453
+ }
5454
+ static fieldScore(field, query) {
5455
+ const key = field.key.toLowerCase();
5456
+ const label = field.label.toLowerCase();
5457
+ let score = 0;
5458
+ if (query.includes(key)) score += 4;
5459
+ if (query.includes(label)) score += 4;
5460
+ key.split(/[_\s-]+/).forEach((part) => {
5461
+ if (part && query.includes(part)) score += 1;
5462
+ });
5463
+ if (/category|department|collection|type|group|segment/.test(key)) score += 2;
5464
+ if (/count|total|quantity|stock|inventory|sales|revenue|amount|price|value|score/.test(key)) score += 2;
5465
+ if (/id|sku|ean|upc|code/.test(key)) score -= 5;
5466
+ return score;
5467
+ }
5468
+ static normalizeComparableField(field) {
5469
+ return field.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "").trim();
5470
+ }
5471
+ static fieldTokens(field) {
5472
+ return field.toLowerCase().replace(/[_-]+/g, " ").split(/\s+/).map((token) => token.replace(/[^a-z0-9]/g, "")).filter(Boolean);
5473
+ }
5474
+ static aggregateProfileByDimension(profile, dimensionKey, measureKey) {
5475
+ const result = {};
5476
+ profile.records.forEach((record) => {
5477
+ var _a, _b, _c;
5478
+ const category = String((_a = record.fields[dimensionKey]) != null ? _a : "Other").trim() || "Other";
5479
+ const value = measureKey ? (_b = this.toFiniteNumber(record.fields[measureKey])) != null ? _b : 0 : 1;
5480
+ result[category] = ((_c = result[category]) != null ? _c : 0) + value;
5481
+ });
5482
+ return result;
5483
+ }
5484
+ static getRecordLabel(record) {
5485
+ const labelKeys = ["name", "title", "label", "product", "item", "brand"];
5486
+ const key = Object.keys(record.fields).find(
5487
+ (fieldKey) => labelKeys.some((labelKey) => fieldKey.toLowerCase().includes(labelKey))
5488
+ );
5489
+ return key ? record.fields[key] : void 0;
5490
+ }
5491
+ static detectAggregationOperation(query) {
5492
+ const q = query.toLowerCase();
5493
+ if (/\b(avg|average|mean)\b/.test(q)) return "average";
5494
+ if (/\b(count|how many|number of)\b/.test(q)) return "count";
5495
+ if (/\b(min|minimum|lowest)\b/.test(q)) return "min";
5496
+ if (/\b(max|maximum|highest)\b/.test(q)) return "max";
5497
+ if (/\bmedian\b/.test(q)) return "median";
5498
+ return "sum";
5499
+ }
5500
+ static calculateAggregate(values, operation) {
5501
+ if (values.length === 0) return 0;
5502
+ switch (operation) {
5503
+ case "average":
5504
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
5505
+ case "count":
5506
+ return values.length;
5507
+ case "min":
5508
+ return Math.min(...values);
5509
+ case "max":
5510
+ return Math.max(...values);
5511
+ case "median": {
5512
+ const sorted = [...values].sort((a, b) => a - b);
5513
+ const middle = Math.floor(sorted.length / 2);
5514
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
5515
+ }
5516
+ case "sum":
5517
+ default:
5518
+ return values.reduce((sum, value) => sum + value, 0);
5519
+ }
5520
+ }
5521
+ static humanizeFieldName(key) {
5522
+ return key.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
5523
+ }
5524
+ static formatNumber(value) {
5525
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
5526
+ }
4535
5527
  static detectCategories(data) {
4536
5528
  const categories = /* @__PURE__ */ new Set();
4537
5529
  data.forEach((item) => {
4538
- const meta = item.metadata || {};
4539
- if (meta.category) {
4540
- categories.add(String(meta.category));
4541
- }
4542
- if (meta.type) {
4543
- categories.add(String(meta.type));
4544
- }
4545
- if (meta.tag) {
4546
- const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
4547
- tags.forEach((t) => categories.add(String(t)));
4548
- }
4549
- const contentCategories = Array.from(new Set(
4550
- Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
4551
- ));
4552
- contentCategories.forEach((category) => categories.add(category));
4553
- const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
4554
- if (categoryMatch) {
4555
- categories.add(categoryMatch[1].trim());
4556
- }
5530
+ const category = this.getProductCategory(item);
5531
+ if (category) categories.add(category);
4557
5532
  });
4558
5533
  return Array.from(categories);
4559
5534
  }
4560
- /**
4561
- * Helper: Aggregate data by category
4562
- */
5535
+ static getProductCategory(item) {
5536
+ const meta = item.metadata || {};
5537
+ const metadataCategory = resolveMetadataValue(meta, "category");
5538
+ if (this.isUsableCategory(metadataCategory)) return String(metadataCategory).trim();
5539
+ const content = item.content || "";
5540
+ const explicitCategoryMatch = content.match(
5541
+ /(?:^|\n)\s*(?:product\s*)?(?:category|department|collection)\s*[:\-–]\s*([^\n]+)/i
5542
+ );
5543
+ if (explicitCategoryMatch && this.isUsableCategory(explicitCategoryMatch[1])) {
5544
+ return explicitCategoryMatch[1].trim();
5545
+ }
5546
+ return null;
5547
+ }
5548
+ static isUsableCategory(value) {
5549
+ if (value === null || value === void 0) return false;
5550
+ const category = String(value).trim();
5551
+ if (!category) return false;
5552
+ return !/^(category|type|tag|price|cost|stock|availability|description|product|item|name|brand|sku|id|ean|currency|color|size|index|internal id|other|\[?type\]?|\[?products?_?\d*\]?)$/i.test(category);
5553
+ }
4563
5554
  static aggregateByCategory(data, categories) {
4564
- const result = {};
4565
- categories.forEach((cat) => {
4566
- result[cat] = 0;
4567
- });
5555
+ const result = Object.fromEntries(categories.map((c) => [c, 0]));
4568
5556
  data.forEach((item) => {
4569
- const meta = item.metadata || {};
4570
- const itemCategory = meta.category || meta.type || "Other";
4571
- if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
4572
- result[itemCategory]++;
4573
- } else {
4574
- result["Other"] = (result["Other"] || 0) + 1;
4575
- }
5557
+ var _a;
5558
+ const cat = (_a = this.getProductCategory(item)) != null ? _a : "Other";
5559
+ if (Object.prototype.hasOwnProperty.call(result, cat)) result[cat]++;
5560
+ else result["Other"] = (result["Other"] || 0) + 1;
4576
5561
  });
4577
5562
  return result;
4578
5563
  }
4579
- /**
4580
- * Helper: Extract time series data
4581
- */
4582
5564
  static extractTimeSeriesData(data) {
4583
5565
  return data.map((item) => {
5566
+ var _a, _b, _c, _d, _e;
4584
5567
  const meta = item.metadata || {};
4585
5568
  return {
4586
- timestamp: meta.timestamp || meta.date || (/* @__PURE__ */ new Date()).toISOString(),
4587
- value: meta.value || item.score || 0,
4588
- label: meta.label || item.content.substring(0, 50)
5569
+ timestamp: (_b = (_a = meta.timestamp) != null ? _a : meta.date) != null ? _b : (/* @__PURE__ */ new Date()).toISOString(),
5570
+ value: (_d = (_c = meta.value) != null ? _c : item.score) != null ? _d : 0,
5571
+ label: (_e = meta.label) != null ? _e : item.content.substring(0, 50)
4589
5572
  };
4590
5573
  });
4591
5574
  }
4592
- /**
4593
- * Helper: Extract table columns
4594
- */
4595
- static extractTableColumns(data) {
4596
- const columnSet = /* @__PURE__ */ new Set();
4597
- columnSet.add("Content");
4598
- data.forEach((item) => {
4599
- Object.keys(item.metadata || {}).forEach((key) => {
4600
- columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
4601
- });
4602
- });
4603
- return Array.from(columnSet);
5575
+ static extractNumericValue(meta) {
5576
+ var _a;
5577
+ const preferredKeys = [
5578
+ "value",
5579
+ "count",
5580
+ "total",
5581
+ "average",
5582
+ "avg",
5583
+ "amount",
5584
+ "sales",
5585
+ "revenue",
5586
+ "score",
5587
+ "quantity",
5588
+ "price"
5589
+ ];
5590
+ for (const key of preferredKeys) {
5591
+ const raw = (_a = resolveMetadataValue(meta, key)) != null ? _a : meta[key];
5592
+ const value = typeof raw === "number" ? raw : Number(String(raw != null ? raw : "").replace(/[$,% ,]/g, ""));
5593
+ if (Number.isFinite(value)) return value;
5594
+ }
5595
+ for (const value of Object.values(meta)) {
5596
+ const numeric = typeof value === "number" ? value : Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
5597
+ if (Number.isFinite(numeric)) return numeric;
5598
+ }
5599
+ return null;
5600
+ }
5601
+ static extractTableColumns(data, query = "") {
5602
+ const q = query.toLowerCase();
5603
+ const availableFields = this.extractAvailableTableFields(data);
5604
+ if (/\b(organization|organisations?|organizations?|company|companies)\b/.test(q) && /\b(employee|employees|staff|headcount|workforce)\b/.test(q)) {
5605
+ const nameField = this.pickTableField(availableFields, [
5606
+ "company name",
5607
+ "organization name",
5608
+ "organisation name",
5609
+ "name",
5610
+ "company",
5611
+ "organization",
5612
+ "organisation"
5613
+ ]);
5614
+ const employeeField = this.pickTableField(availableFields, [
5615
+ "number of employees",
5616
+ "employee count",
5617
+ "employees",
5618
+ "employee_count",
5619
+ "number_employees",
5620
+ "num employees",
5621
+ "staff count",
5622
+ "headcount",
5623
+ "workforce"
5624
+ ]);
5625
+ const columns = [nameField, employeeField].filter((field) => Boolean(field));
5626
+ if (columns.length > 0) return columns;
5627
+ }
5628
+ const requestedFields = availableFields.map((field) => ({
5629
+ field,
5630
+ score: this.tableFieldQueryScore(field, q)
5631
+ })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).map((item) => item.field);
5632
+ if (requestedFields.length > 0) {
5633
+ return requestedFields.slice(0, Math.min(6, requestedFields.length));
5634
+ }
5635
+ const metadataFields = Array.from(new Set(
5636
+ data.flatMap((item) => Object.keys(item.metadata || {}).map((k) => this.humanizeFieldName(k)))
5637
+ ));
5638
+ return metadataFields.length > 0 ? metadataFields : ["Content"];
4604
5639
  }
4605
- /**
4606
- * Helper: Extract table row
4607
- */
4608
5640
  static extractTableRow(item, columns) {
4609
- const meta = item.metadata || {};
4610
- return columns.map((col) => {
4611
- if (col === "Content") {
4612
- return item.content.substring(0, 100);
5641
+ return columns.map((col) => this.resolveTableCellValue(item, col));
5642
+ }
5643
+ static extractAvailableTableFields(data) {
5644
+ const fields = /* @__PURE__ */ new Map();
5645
+ const addField = (field) => {
5646
+ const clean = this.humanizeFieldName(field);
5647
+ if (!clean || /^(content|\[?type\]?)$/i.test(clean)) return;
5648
+ const normalized = this.normalizeComparableField(clean);
5649
+ if (!fields.has(normalized)) fields.set(normalized, clean);
5650
+ };
5651
+ data.forEach((item) => {
5652
+ Object.keys(item.metadata || {}).forEach(addField);
5653
+ for (const match of (item.content || "").matchAll(/(?:^|\n)\s*([A-Za-z][A-Za-z0-9_ /-]{1,50})\s*[:\-–]\s*([^\n]+)/g)) {
5654
+ addField(match[1]);
4613
5655
  }
4614
- const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
4615
- const value = meta[metaKey];
4616
- return value !== void 0 ? String(value) : "";
4617
5656
  });
5657
+ return Array.from(fields.values());
5658
+ }
5659
+ static pickTableField(fields, aliases) {
5660
+ const normalizedAliases = aliases.map((alias) => this.normalizeComparableField(alias));
5661
+ for (const alias of normalizedAliases) {
5662
+ const exact = fields.find((field) => this.normalizeComparableField(field) === alias);
5663
+ if (exact) return exact;
5664
+ }
5665
+ for (const alias of normalizedAliases) {
5666
+ const fuzzy = fields.find((field) => {
5667
+ const normalizedField = this.normalizeComparableField(field);
5668
+ if (/(id|code|uuid)$/.test(normalizedField) && !/(id|code|uuid)$/.test(alias)) return false;
5669
+ return normalizedField.includes(alias) || alias.includes(normalizedField);
5670
+ });
5671
+ if (fuzzy) return fuzzy;
5672
+ }
5673
+ return void 0;
5674
+ }
5675
+ static tableFieldQueryScore(field, query) {
5676
+ const normalizedField = this.normalizeComparableField(field);
5677
+ if (!normalizedField) return 0;
5678
+ const fieldTokens = this.fieldTokens(field);
5679
+ return fieldTokens.reduce((score, token) => {
5680
+ if (token.length < 3) return score;
5681
+ return query.includes(token) ? score + 1 : score;
5682
+ }, query.includes(normalizedField) ? 3 : 0);
5683
+ }
5684
+ static resolveTableCellValue(item, column) {
5685
+ if (column === "Content") return item.content.substring(0, 100);
5686
+ const meta = item.metadata || {};
5687
+ const normalizedColumn = this.normalizeComparableField(column);
5688
+ const exactMetadata = Object.entries(meta).find(
5689
+ ([key]) => this.normalizeComparableField(key) === normalizedColumn || this.normalizeComparableField(this.humanizeFieldName(key)) === normalizedColumn
5690
+ );
5691
+ if (exactMetadata && exactMetadata[1] !== void 0 && exactMetadata[1] !== null) {
5692
+ return this.toDisplayValue(exactMetadata[1]);
5693
+ }
5694
+ const aliasValue = this.resolveAliasedTableCell(meta, column);
5695
+ if (aliasValue !== void 0) return this.toDisplayValue(aliasValue);
5696
+ const contentValue = this.extractContentFieldValue(item.content, column);
5697
+ if (contentValue !== null) return contentValue;
5698
+ const aliasContentValue = this.extractAliasedContentFieldValue(item.content, column);
5699
+ return aliasContentValue != null ? aliasContentValue : "";
5700
+ }
5701
+ static resolveAliasedTableCell(meta, column) {
5702
+ const normalizedColumn = this.normalizeComparableField(column);
5703
+ const aliases = normalizedColumn.includes("employee") ? ["number of employees", "employee count", "employees", "headcount", "staff count", "workforce"] : normalizedColumn.includes("name") ? ["company name", "organization name", "organisation name", "name", "company", "organization", "organisation"] : [];
5704
+ for (const alias of aliases) {
5705
+ const normalizedAlias = this.normalizeComparableField(alias);
5706
+ const match = Object.entries(meta).find(([key]) => {
5707
+ const normalizedKey = this.normalizeComparableField(key);
5708
+ return normalizedKey === normalizedAlias || normalizedKey.includes(normalizedAlias) || normalizedAlias.includes(normalizedKey);
5709
+ });
5710
+ if (match && match[1] !== void 0 && match[1] !== null) return match[1];
5711
+ }
5712
+ return void 0;
5713
+ }
5714
+ static extractContentFieldValue(content, column) {
5715
+ const escaped = column.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "[\\s_ -]+");
5716
+ const pattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\s*[:\\-\u2013]\\s*([^\\n]+)`, "i");
5717
+ const match = content.match(pattern);
5718
+ return match ? match[1].trim() : null;
5719
+ }
5720
+ static extractAliasedContentFieldValue(content, column) {
5721
+ const normalizedColumn = this.normalizeComparableField(column);
5722
+ const aliases = normalizedColumn.includes("employee") ? ["Number Of Employees", "Number of Employees", "Employee Count", "Employees", "Headcount", "Staff Count", "Workforce"] : normalizedColumn.includes("name") ? ["Company Name", "Organization Name", "Organisation Name", "Name", "Company", "Organization", "Organisation"] : [];
5723
+ for (const alias of aliases) {
5724
+ const value = this.extractContentFieldValue(content, alias);
5725
+ if (value !== null) return value;
5726
+ }
5727
+ return null;
5728
+ }
5729
+ static toDisplayValue(value) {
5730
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
5731
+ return String(value != null ? value : "");
4618
5732
  }
4619
- /**
4620
- * Helper: Calculate stock counts by category
4621
- */
4622
5733
  static calculateStockCounts(category, data) {
4623
5734
  let inStock = 0;
4624
5735
  let outOfStock = 0;
4625
5736
  data.forEach((d) => {
4626
- const meta = d.metadata || {};
4627
- const itemCategory = meta.category || meta.type || "Other";
4628
- if (itemCategory === category) {
4629
- if (this.determineStockStatus(d)) {
4630
- inStock++;
4631
- } else {
4632
- outOfStock++;
4633
- }
5737
+ var _a, _b;
5738
+ const cat = (_a = this.getProductCategory(d)) != null ? _a : "Other";
5739
+ if (cat === category) {
5740
+ const quantity = (_b = this.extractStockQuantity(d)) != null ? _b : 1;
5741
+ if (this.determineStockStatus(d)) inStock += quantity;
5742
+ else outOfStock += quantity;
4634
5743
  }
4635
5744
  });
4636
5745
  return { inStockCount: inStock, outOfStockCount: outOfStock };
4637
5746
  }
4638
- /**
4639
- * Helper: Determine if item is in stock
4640
- */
4641
5747
  static determineStockStatus(item) {
4642
5748
  const meta = item.metadata || {};
4643
- if (meta.inStock !== void 0) {
4644
- return Boolean(meta.inStock);
4645
- }
4646
- if (meta.stock !== void 0) {
4647
- return Boolean(meta.stock);
4648
- }
4649
- if (meta.available !== void 0) {
4650
- return Boolean(meta.available);
4651
- }
5749
+ const stockValue = resolveMetadataValue(meta, "stock");
5750
+ if (stockValue !== void 0) {
5751
+ const normalized = String(stockValue).toLowerCase();
5752
+ if (/out[_\s-]?of[_\s-]?stock|unavailable|false|no|sold out|0/.test(normalized)) return false;
5753
+ if (/in[_\s-]?stock|available|true|yes/.test(normalized)) return true;
5754
+ const numeric = this.toFiniteNumber(stockValue);
5755
+ if (numeric !== null) return numeric > 0;
5756
+ }
5757
+ if (meta.inStock !== void 0) return Boolean(meta.inStock);
5758
+ if (meta.stock !== void 0) return Boolean(meta.stock);
5759
+ if (meta.available !== void 0) return Boolean(meta.available);
4652
5760
  const content = (item.content || "").toLowerCase();
4653
- if (content.includes("out of stock") || content.includes("unavailable")) {
4654
- return false;
5761
+ if (/out[_\s-]?of[_\s-]?stock|unavailable|sold out/.test(content)) return false;
5762
+ if (/in[_\s-]?stock|available/.test(content)) return true;
5763
+ return true;
5764
+ }
5765
+ static extractStockQuantity(item) {
5766
+ const meta = item.metadata || {};
5767
+ const stockValue = resolveMetadataValue(meta, "stock");
5768
+ const numericStock = this.toFiniteNumber(stockValue);
5769
+ if (numericStock !== null) return numericStock;
5770
+ const content = item.content || "";
5771
+ const stockMatch = content.match(/\b(?:stock|inventory|quantity|count)\s*[:\-–]?\s*([\d,]+)/i);
5772
+ if (!stockMatch) return null;
5773
+ return this.toFiniteNumber(stockMatch[1]);
5774
+ }
5775
+ static toFiniteNumber(value) {
5776
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
5777
+ const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
5778
+ return Number.isFinite(numeric) ? numeric : null;
5779
+ }
5780
+ // ─── Product Extraction ───────────────────────────────────────────────────
5781
+ static getDynamicVal(meta, uiKey, config, trainedSchema) {
5782
+ var _a;
5783
+ if (!meta) return void 0;
5784
+ const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
5785
+ if ((mapping == null ? void 0 : mapping[uiKey]) && meta[mapping[uiKey]] !== void 0) return meta[mapping[uiKey]];
5786
+ if (trainedSchema && typeof trainedSchema === "object") {
5787
+ const trainedKey = trainedSchema[uiKey];
5788
+ if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
4655
5789
  }
4656
- if (content.includes("in stock") || content.includes("available")) {
4657
- return true;
5790
+ return resolveMetadataValue(meta, uiKey);
5791
+ }
5792
+ static extractProductInfo(item, config, trainedSchema) {
5793
+ var _a;
5794
+ const meta = item.metadata || {};
5795
+ const name = this.getDynamicVal(meta, "name", config, trainedSchema);
5796
+ const price = this.getDynamicVal(meta, "price", config, trainedSchema);
5797
+ const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
5798
+ const description = this.cleanProductDescription(
5799
+ (_a = this.extractProductDescriptionFromContent(item.content)) != null ? _a : this.getProductDescriptionValue(meta, config, trainedSchema)
5800
+ );
5801
+ if (name || this.isProductData(item)) {
5802
+ let finalName = name ? String(name) : void 0;
5803
+ if (!finalName) {
5804
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
5805
+ finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
5806
+ }
5807
+ let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
5808
+ if (!finalPrice) {
5809
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
5810
+ if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
5811
+ }
5812
+ const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
5813
+ return {
5814
+ id: item.id,
5815
+ name: finalName,
5816
+ price: finalPrice,
5817
+ image: typeof imageValue === "string" ? imageValue : void 0,
5818
+ brand: brand ? String(brand) : void 0,
5819
+ description,
5820
+ inStock: this.determineStockStatus(item)
5821
+ };
4658
5822
  }
4659
- return true;
5823
+ return null;
4660
5824
  }
4661
- /**
4662
- * Helper: Check if data has multiple fields
4663
- */
4664
- static hasMultipleFields(data) {
4665
- const fieldCount = /* @__PURE__ */ new Set();
4666
- data.forEach((item) => {
4667
- Object.keys(item.metadata || {}).forEach((key) => {
4668
- fieldCount.add(key);
4669
- });
4670
- });
4671
- return fieldCount.size > 2;
5825
+ static extractProductDescriptionFromContent(content) {
5826
+ const bodyMatch = content.match(
5827
+ /\bBody\s*\(HTML\)\s*:\s*([\s\S]*?)(?=\s*[,.]?\s+\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\b\s*:|$)/i
5828
+ );
5829
+ if (bodyMatch == null ? void 0 : bodyMatch[1]) return bodyMatch[1];
5830
+ const descriptionMatch = content.match(
5831
+ /\b(?:Description|Summary|Details|Body|Content)\s*:\s*([\s\S]*?)(?=\s*[,.]?\s+\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\b\s*:|$)/i
5832
+ );
5833
+ if (descriptionMatch == null ? void 0 : descriptionMatch[1]) return descriptionMatch[1];
5834
+ return null;
4672
5835
  }
4673
- // ─── LLM-Driven Visualization Decision ────────────────────────────────────
4674
- /**
4675
- * analyzeAndDecide sends user question + RAG data to the LLM with a
4676
- * structured system prompt and parses the JSON response into a
4677
- * UITransformationResponse.
4678
- *
4679
- * This is the recommended entry point for production use. The heuristic
4680
- * `transform()` method is used as a fallback if the LLM call fails.
4681
- *
4682
- * System prompt instructs the LLM to:
4683
- * - Analyze the question and retrieved data
4684
- * - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
4685
- * - Return a strict JSON object — no prose, no markdown fences
4686
- *
4687
- * @param query - the original user question
4688
- * @param sources - vector DB matches returned by RAG retrieval
4689
- * @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
4690
- * @returns - a validated UITransformationResponse (type + title + description + data)
4691
- */
4692
- static async analyzeAndDecide(query, sources, llm) {
4693
- try {
4694
- const context = this.buildContextSummary(sources);
4695
- const systemPrompt = this.buildVisualizationSystemPrompt();
4696
- const userPrompt = [
4697
- `USER QUESTION: ${query}`,
4698
- "",
4699
- "RETRIEVED DATA (JSON):",
4700
- context
4701
- ].join("\n");
4702
- const rawResponse = await llm.chat(
4703
- [{ role: "user", content: userPrompt }],
4704
- "",
4705
- { systemPrompt, temperature: 0 }
5836
+ static getProductDescriptionValue(meta, config, trainedSchema) {
5837
+ const mapped = this.getDynamicVal(meta, "description", config, trainedSchema);
5838
+ if (mapped !== void 0 && mapped !== meta.content && mapped !== meta.text) return mapped;
5839
+ const preferredKeys = [
5840
+ "body_html",
5841
+ "body html",
5842
+ "bodyHtml",
5843
+ "description",
5844
+ "summary",
5845
+ "details",
5846
+ "body"
5847
+ ];
5848
+ for (const key of preferredKeys) {
5849
+ const match = Object.keys(meta).find(
5850
+ (candidate) => this.normalizeComparableField(candidate) === this.normalizeComparableField(key)
4706
5851
  );
4707
- const parsed = this.parseTransformationResponse(rawResponse);
4708
- if (parsed) {
4709
- console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
4710
- return parsed;
4711
- }
4712
- console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
4713
- } catch (err) {
4714
- console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
5852
+ if (match && meta[match] !== void 0 && meta[match] !== null) return meta[match];
4715
5853
  }
4716
- return this.transform(query, sources);
5854
+ return void 0;
4717
5855
  }
4718
- /**
4719
- * Build the system prompt that instructs the LLM to return a visualization JSON.
4720
- */
5856
+ static cleanProductDescription(raw) {
5857
+ if (raw === null || raw === void 0) return void 0;
5858
+ const extracted = this.extractProductDescriptionFromContent(String(raw));
5859
+ if (extracted && extracted !== String(raw)) return this.cleanProductDescription(extracted);
5860
+ const text = String(raw).replace(/<[^>]*>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/\[[^\]]*TYPE[^\]]*\]/gi, " ").replace(/\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\s*:\s*[^,.\n:]+[,.]?/gi, " ").replace(/\bBody\s*\(HTML\)\s*:\s*/gi, " ").replace(/\b(?:Description|Summary|Details|Body|Content)\s*:\s*/gi, " ").replace(/\s+/g, " ").trim();
5861
+ return text || void 0;
5862
+ }
5863
+ // ─── Visualization Prompt ─────────────────────────────────────────────────
4721
5864
  static buildVisualizationSystemPrompt() {
4722
5865
  return `You are a data visualization expert embedded in a RAG chat system.
4723
- You will receive a user question and structured data retrieved from a vector database.
4724
- Your ONLY job is to analyze this information and return a single JSON object that tells
4725
- the frontend how to visualize it.
5866
+ You will receive a user question, a pre-computed INTENT object, and structured data from a vector database.
5867
+ Your ONLY job is to return a single JSON object describing how to visualize the data.
5868
+ The INTENT object is authoritative \u2014 honor it. For example:
5869
+ - If intent.wantsExplicitTable is false, never return type "table".
5870
+ - If intent.isTemporal is true, prefer line_chart.
5871
+ - If intent.visualizationHint is "comparison" or "ranking", prefer bar_chart.
5872
+ - If intent.visualizationHint is "composition", prefer pie_chart.
5873
+ - If intent.recommendedChart is unsupported by the response schema, use the nearest supported type.
5874
+ - Always respond in the language specified by intent.language.
4726
5875
 
4727
- Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
5876
+ Return ONLY a valid JSON object \u2014 no markdown fences, no prose.
4728
5877
 
4729
- The JSON must have this exact shape:
4730
5878
  {
4731
- "type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
4732
- "title": "A concise, descriptive title for the visualization",
4733
- "description": "One sentence describing what the visualization shows",
4734
- "data": <structured data \u2014 see rules below>
5879
+ "type": "bar_chart" | "horizontal_bar" | "line_chart" | "pie_chart" | "histogram" | "scatter_plot" | "radar_chart" | "metric_card" | "geo_map" | "table" | "text",
5880
+ "title": "concise descriptive title",
5881
+ "description": "one sentence describing the visualization",
5882
+ "data": <structured data per type below>
4735
5883
  }
4736
5884
 
4737
- DATA SHAPE per type:
4738
- - bar_chart: array of { "category": string, "value": number }
4739
- - line_chart: array of { "timestamp": string, "value": number, "label": string }
4740
- - pie_chart: array of { "label": string, "value": number }
4741
- - radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
4742
- Example for radar_chart:
4743
- [
4744
- { "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
4745
- { "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
4746
- { "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
4747
- ]
4748
- - table: { "columns": string[], "rows": (string|number)[][] }
4749
- - text: { "content": "<prose answer>" }
5885
+ DATA SHAPES:
5886
+ - bar_chart: [{ "category": string, "value": number }]
5887
+ - horizontal_bar: [{ "category": string, "value": number }]
5888
+ - histogram: [{ "category": string, "value": number }]
5889
+ - line_chart: [{ "timestamp": string, "value": number, "label": string }]
5890
+ - pie_chart: [{ "label": string, "value": number }]
5891
+ - scatter_plot:[{ "x": number, "y": number, "label": string }]
5892
+ - radar_chart: [{ "attribute": string, "<series1>": number, "<series2>": number, ... }]
5893
+ - metric_card: { "label": string, "value": number, "operation": "sum"|"average"|"count"|"min"|"max"|"median" }
5894
+ - geo_map: [{ "category": string, "value": number }]
5895
+ - table: { "columns": string[], "rows": (string|number)[][] }
5896
+ - text: { "content": "A concise plain-language answer." }
4750
5897
 
4751
- DECISION RULES (follow strictly):
4752
- 1. bar_chart \u2192 comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
4753
- 2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
4754
- 3. pie_chart \u2192 proportional breakdown or percentage distribution
4755
- 4. radar_chart \u2192 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.
4756
- 5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
4757
- 6. text \u2192 conversational or free-form answers where no chart adds value
4758
-
4759
- IMPORTANT:
4760
- - Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
4761
- - Ensure all "value" fields are numbers, not strings.
4762
- - For bar/line/pie, keep at most 12 data points for readability.
4763
- - Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
5898
+ RULES:
5899
+ 1. Aggregate values \u2014 never pass raw nested objects in chart arrays.
5900
+ 2. All "value" fields must be numbers.
5901
+ 3. Cap bar/line/pie at 12 data points.
5902
+ 4. Use dollar signs only for monetary prices, not for stock quantities or years.
5903
+ 5. If no relevant data is found, return text: "I cannot answer this question based on the available information."`;
4764
5904
  }
4765
- /**
4766
- * Serialize retrieved vector matches into a compact JSON context string.
4767
- * Limits the total character count to avoid exceeding LLM context windows.
4768
- */
4769
5905
  static buildContextSummary(sources, maxChars = 6e3) {
4770
5906
  const items = sources.map((s, i) => {
4771
5907
  var _a, _b, _c, _d;
@@ -4809,14 +5945,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
4809
5945
  Identify which keys best correspond to these standard UI properties:
4810
5946
  ${propertyList}
4811
5947
 
4812
- Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
5948
+ 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.
4813
5949
  If no good match is found for a property, omit it.
4814
5950
 
4815
5951
  Example:
4816
5952
  {
4817
- "name": "Product_Title",
4818
- "price": "MSRP_USD",
4819
- "brand": "VendorName"
5953
+ "name": "Title",
5954
+ "price": "Variant Price",
5955
+ "brand": "Vendor",
5956
+ "image": "Image Src",
5957
+ "stock": "Variant Inventory Qty"
4820
5958
  }
4821
5959
  `;
4822
5960
  try {
@@ -4986,27 +6124,38 @@ var Pipeline = class {
4986
6124
  async initialize() {
4987
6125
  var _a;
4988
6126
  if (this.initialised) return;
4989
- const chartInstruction = `You are a helpful product assistant. Use the provided context to answer questions accurately.
6127
+ const chartInstruction = `You are a helpful assistant. Use the provided context to answer questions accurately.
4990
6128
 
4991
- ### UI STYLE RULES (CRITICAL):
6129
+ ### CRITICAL RULES:
6130
+ - ONLY answer the user's specific question. Do NOT suggest or recommend unrelated products unless specifically asked.
4992
6131
  - NEVER generate markdown tables. If you do, the UI will break.
4993
6132
  - NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
4994
6133
  - NEVER generate text-based charts or graphs.
6134
+ - NEVER say you cannot display, render, draw, or create a chart when the user asks for a visualization. The UI handles visual rendering separately.
4995
6135
  - ONLY use plain text and bullet points.
6136
+ - NEVER use the plus sign (+) as a separator between names, categories, or products. Use commas or bullet points.
6137
+ - ONLY answer the question if the sources contain relevant information to answer it, else say that you cannot answer the question.
6138
+ - If answer cannot be found in the sources, say that you cannot answer the question and do not hallucinate.
6139
+ - Do not use information from previous turns to answer the question.
6140
+ - You CAN use numbers for years and counts (e.g., 2006, 5800). But NEVER put a dollar sign ($) before non-monetary numbers like Stock quantities, EAN numbers, or years. Only use dollar signs for actual monetary prices.
4996
6141
 
4997
6142
  ### PRODUCT DISPLAY:
4998
- - When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
6143
+ - Recommended Products should only be displayed when the user explicitly asks for products.
6144
+ - When recommending products (ONLY when asked), simply list their names, prices, and features in a friendly, conversational manner.
4999
6145
  - The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
6146
+ - For product descriptions, summarize customer-facing details only. Do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels.
5000
6147
  - Do NOT try to format product lists as tables.
5001
6148
  `;
5002
6149
  this.config.llm.systemPrompt = chartInstruction;
5003
6150
  this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
5004
- const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
6151
+ this.llmRouter = new LLMRouter(this.config);
6152
+ const { llmProvider: resolvedLLM, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
5005
6153
  this.config.llm,
5006
6154
  this.config.embedding
5007
6155
  );
5008
- this.llmProvider = llmProvider;
5009
6156
  this.embeddingProvider = embeddingProvider;
6157
+ await this.llmRouter.initialize(resolvedLLM);
6158
+ this.llmProvider = this.llmRouter.get("default");
5010
6159
  if (this.config.graphDb) {
5011
6160
  this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
5012
6161
  await this.graphDB.initialize();
@@ -5096,7 +6245,10 @@ var Pipeline = class {
5096
6245
  upsertBatchOptions
5097
6246
  );
5098
6247
  if (upsertResult.errors.length > 0) {
5099
- console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed`);
6248
+ console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
6249
+ upsertResult.errors.forEach((err, idx) => {
6250
+ console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
6251
+ });
5100
6252
  }
5101
6253
  return upsertResult.totalProcessed;
5102
6254
  }
@@ -5163,10 +6315,16 @@ var Pipeline = class {
5163
6315
  /**
5164
6316
  * High-performance streaming RAG flow.
5165
6317
  * Yields text chunks first, then the retrieval metadata + observability trace at the end.
6318
+ *
6319
+ * Latency optimizations:
6320
+ * - Strategy classification runs in parallel with query embedding (saves ~400ms)
6321
+ * - Hallucination scoring is fire-and-forget (doesn't block metadata yield)
6322
+ * - UITransformation is computed after text streaming and emitted with metadata
6323
+ * - SchemaMapper.train runs while answer generation streams
5166
6324
  */
5167
6325
  askStream(_0) {
5168
6326
  return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
5169
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
6327
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
5170
6328
  yield new __await(this.initialize());
5171
6329
  const ns = namespace != null ? namespace : this.config.projectId;
5172
6330
  const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
@@ -5182,27 +6340,61 @@ var Pipeline = class {
5182
6340
  }
5183
6341
  const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
5184
6342
  const filter = QueryProcessor.buildQueryFilter(question, hints);
6343
+ const numericPredicates = QueryProcessor.extractNumericPredicates(question, (_g = this.config.rag) == null ? void 0 : _g.filterableFields);
6344
+ if (numericPredicates.length > 0) {
6345
+ filter.__numericPredicates = numericPredicates;
6346
+ }
5185
6347
  const embedStart = performance.now();
5186
- const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
5187
- namespace: ns,
5188
- topK: topK * 2,
5189
- filter
5190
- }));
6348
+ const cacheKey = `${ns}::${searchQuery}`;
6349
+ const cachedVector = this.embeddingCache.get(cacheKey);
6350
+ const [strategyResult, embeddedVector] = yield new __await(Promise.all([
6351
+ QueryProcessor.determineRetrievalStrategy(
6352
+ searchQuery,
6353
+ this.llmRouter.get("fast"),
6354
+ (_h = this.config.rag) == null ? void 0 : _h.graphKeywords,
6355
+ (_i = this.config.rag) == null ? void 0 : _i.vectorKeywords
6356
+ ),
6357
+ // Embed immediately regardless of strategy — costs nothing if cached.
6358
+ // If strategy turns out to be 'graph'-only we just won't use the vector.
6359
+ cachedVector ? Promise.resolve(cachedVector) : this.embeddingProvider.embed(searchQuery, { taskType: "query" })
6360
+ ]));
6361
+ const queryVector = embeddedVector;
6362
+ if (!cachedVector && queryVector.length > 0) {
6363
+ this.embeddingCache.set(cacheKey, queryVector);
6364
+ }
6365
+ 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;
6366
+ const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
6367
+ const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
6368
+ const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
6369
+ const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
6370
+ const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
5191
6371
  const retrieveEnd = performance.now();
5192
6372
  const embedMs = retrieveEnd - embedStart;
5193
6373
  const retrieveMs = retrieveEnd - embedStart;
5194
6374
  const rerankStart = performance.now();
5195
- let sources = rawSources.filter((m) => m.score >= scoreThreshold);
5196
- if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
5197
- sources = yield new __await(this.reranker.rerank(sources, question, topK));
5198
- } else {
5199
- sources = sources.slice(0, topK);
6375
+ const structuredSources = this.applyStructuredFilters(rawSources, filter);
6376
+ let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6377
+ const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
6378
+ if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
6379
+ fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
6380
+ } else if (!wantsExhaustiveList) {
6381
+ fullSources = fullSources.slice(0, topK);
5200
6382
  }
5201
6383
  const rerankMs = performance.now() - rerankStart;
5202
- let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
6384
+ let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
5203
6385
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
6386
+ let displayCount = 15;
6387
+ if (hasMetadataFilter) {
6388
+ displayCount = fullSources.length;
6389
+ } else {
6390
+ const highlyRelevant = fullSources.filter((m) => m.score >= 0.4);
6391
+ displayCount = Math.max(highlyRelevant.length, topK);
6392
+ if (displayCount > 15) {
6393
+ displayCount = 15;
6394
+ }
6395
+ }
6396
+ const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
5204
6397
  if (graphData && graphData.nodes.length > 0) {
5205
- console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
5206
6398
  const graphContext = graphData.nodes.map(
5207
6399
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
5208
6400
  ).join("\n");
@@ -5213,20 +6405,29 @@ VECTOR CONTEXT:
5213
6405
  ${context}`;
5214
6406
  }
5215
6407
  const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
5216
- const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
5217
- const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
6408
+ const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
6409
+ const uiTransformationPromise = trainedSchemaPromise.then(
6410
+ (trainedSchema) => this.generateUiTransformation(
6411
+ question,
6412
+ sources,
6413
+ trainedSchema,
6414
+ hasNumericPredicates
6415
+ )
6416
+ ).catch((uiError) => {
6417
+ console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
6418
+ return UITransformer.transform(question, sources, this.config);
6419
+ });
6420
+ 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.)";
5218
6421
  const hardenedHistory = [...history];
5219
6422
  const userQuestion = { role: "user", content: question + restrictionSuffix };
5220
6423
  const messages = [...hardenedHistory, userQuestion];
5221
- const systemPrompt = (_h = this.config.llm.systemPrompt) != null ? _h : "";
6424
+ const systemPrompt = (_l = this.config.llm.systemPrompt) != null ? _l : "";
5222
6425
  const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
5223
6426
  let fullReply = "";
5224
6427
  const generateStart = performance.now();
5225
6428
  if (this.llmProvider.chatStream) {
5226
6429
  const stream = this.llmProvider.chatStream(messages, context);
5227
- if (!stream) {
5228
- throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
5229
- }
6430
+ if (!stream) throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
5230
6431
  try {
5231
6432
  for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
5232
6433
  const chunk = temp.value;
@@ -5253,7 +6454,7 @@ ${context}`;
5253
6454
  const latency = {
5254
6455
  embedMs: Math.round(embedMs),
5255
6456
  retrieveMs: Math.round(retrieveMs),
5256
- rerankMs: ((_i = this.config.rag) == null ? void 0 : _i.useReranking) ? Math.round(rerankMs) : void 0,
6457
+ rerankMs: ((_m = this.config.rag) == null ? void 0 : _m.useReranking) ? Math.round(rerankMs) : void 0,
5257
6458
  generateMs: Math.round(generateMs),
5258
6459
  totalMs: Math.round(totalMs)
5259
6460
  };
@@ -5266,9 +6467,6 @@ ${context}`;
5266
6467
  totalTokens: promptTokens + completionTokens,
5267
6468
  estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
5268
6469
  };
5269
- const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
5270
- const trainedSchema = yield new __await(trainingPromise);
5271
- const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, trainedSchema));
5272
6470
  const trace = {
5273
6471
  requestId,
5274
6472
  query: question,
@@ -5287,10 +6485,9 @@ ${context}`;
5287
6485
  }),
5288
6486
  latency,
5289
6487
  tokens,
5290
- hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
5291
- hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
5292
6488
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5293
6489
  };
6490
+ const uiTransformation = yield new __await(uiTransformationPromise);
5294
6491
  yield {
5295
6492
  reply: "",
5296
6493
  sources,
@@ -5298,6 +6495,13 @@ ${context}`;
5298
6495
  ui_transformation: uiTransformation,
5299
6496
  trace
5300
6497
  };
6498
+ scoreHallucination(this.llmProvider, fullReply, context).then((hallucinationResult) => {
6499
+ if (hallucinationResult) {
6500
+ trace.hallucinationScore = hallucinationResult.score;
6501
+ trace.hallucinationReason = hallucinationResult.reason;
6502
+ }
6503
+ }).catch(() => {
6504
+ });
5301
6505
  } catch (error2) {
5302
6506
  throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
5303
6507
  }
@@ -5307,24 +6511,107 @@ ${context}`;
5307
6511
  * Universal retrieval method combining all enabled providers.
5308
6512
  * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
5309
6513
  */
5310
- async generateUiTransformation(question, sources, trainedSchema) {
6514
+ async generateUiTransformation(question, sources, trainedSchema, forceDeterministic = false) {
5311
6515
  if (!sources || sources.length === 0) {
5312
6516
  return UITransformer.transform(question, sources, this.config, trainedSchema);
5313
6517
  }
6518
+ if (forceDeterministic) {
6519
+ return UITransformer.transform(question, sources, this.config, trainedSchema);
6520
+ }
5314
6521
  try {
5315
- return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
6522
+ return await UITransformer.analyzeAndDecide(question, sources, this.llmRouter.get("fast"));
5316
6523
  } catch (err) {
5317
6524
  console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
5318
6525
  return UITransformer.transform(question, sources, this.config, trainedSchema);
5319
6526
  }
5320
6527
  }
6528
+ applyStructuredFilters(sources, filter) {
6529
+ const predicates = Array.isArray(filter.__numericPredicates) ? filter.__numericPredicates : [];
6530
+ if (predicates.length === 0) return sources;
6531
+ return sources.filter((source) => predicates.every((predicate) => {
6532
+ const value = this.resolveNumericPredicateValue(source, predicate);
6533
+ return value !== null && this.matchesNumericPredicate(value, predicate);
6534
+ })).sort((a, b) => {
6535
+ var _a, _b;
6536
+ const primary = predicates[0];
6537
+ const aValue = (_a = this.resolveNumericPredicateValue(a, primary)) != null ? _a : 0;
6538
+ const bValue = (_b = this.resolveNumericPredicateValue(b, primary)) != null ? _b : 0;
6539
+ return primary.operator === "lt" || primary.operator === "lte" ? aValue - bValue : bValue - aValue;
6540
+ });
6541
+ }
6542
+ resolveNumericPredicateValue(source, predicate) {
6543
+ const meta = source.metadata || {};
6544
+ const field = predicate.field;
6545
+ const entries = Object.entries(meta).filter(([, value]) => value !== null && value !== void 0 && typeof value !== "object");
6546
+ if (field) {
6547
+ const normalizedField = this.normalizeComparableField(field);
6548
+ const exact = entries.find(([key]) => this.normalizeComparableField(key) === normalizedField);
6549
+ const fuzzy = exact != null ? exact : entries.map(([key, value]) => ({ key, value, score: this.fieldSimilarityScore(key, field) })).filter((candidate) => candidate.score > 0).sort((a, b) => b.score - a.score)[0];
6550
+ if (fuzzy) {
6551
+ const value = this.toFiniteNumber(Array.isArray(fuzzy) ? fuzzy[1] : fuzzy.value);
6552
+ if (value !== null) return value;
6553
+ }
6554
+ const contentValue = this.extractNumericValueFromContent(source.content, field);
6555
+ if (contentValue !== null) return contentValue;
6556
+ }
6557
+ for (const [key, value] of entries) {
6558
+ if (/(id|sku|ean|upc|phone|zip|postal|code)/i.test(key)) continue;
6559
+ const numeric = this.toFiniteNumber(value);
6560
+ if (numeric !== null) return numeric;
6561
+ }
6562
+ return null;
6563
+ }
6564
+ extractNumericValueFromContent(content, field) {
6565
+ const escapedWords = field.split(/\s+|_+|-+/).filter(Boolean).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
6566
+ if (escapedWords.length === 0) return null;
6567
+ const pattern = new RegExp(`(?:${escapedWords.join("[\\\\s_:-]*")})\\s*[:\\-\u2013]?\\s*([\\d,]+(?:\\.\\d+)?)`, "i");
6568
+ const match = content.match(pattern);
6569
+ return match ? this.toFiniteNumber(match[1]) : null;
6570
+ }
6571
+ matchesNumericPredicate(value, predicate) {
6572
+ switch (predicate.operator) {
6573
+ case "gt":
6574
+ return value > predicate.value;
6575
+ case "gte":
6576
+ return value >= predicate.value;
6577
+ case "lt":
6578
+ return value < predicate.value;
6579
+ case "lte":
6580
+ return value <= predicate.value;
6581
+ case "eq":
6582
+ default:
6583
+ return value === predicate.value;
6584
+ }
6585
+ }
6586
+ normalizeComparableField(value) {
6587
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
6588
+ }
6589
+ fieldSimilarityScore(candidate, requested) {
6590
+ const normalizedCandidate = this.normalizeComparableField(candidate);
6591
+ const normalizedRequested = this.normalizeComparableField(requested);
6592
+ if (normalizedCandidate === normalizedRequested) return 10;
6593
+ if (normalizedCandidate.includes(normalizedRequested) || normalizedRequested.includes(normalizedCandidate)) return 8;
6594
+ const candidateTokens = this.fieldTokens(candidate);
6595
+ const requestedTokens = this.fieldTokens(requested);
6596
+ const overlap = requestedTokens.filter((token) => candidateTokens.includes(token)).length;
6597
+ if (overlap === 0) return 0;
6598
+ return overlap / Math.max(requestedTokens.length, candidateTokens.length);
6599
+ }
6600
+ fieldTokens(value) {
6601
+ return value.toLowerCase().split(/[^a-z0-9]+/).map((token) => token.replace(/ies$/, "y").replace(/s$/, "")).filter((token) => token.length > 1);
6602
+ }
6603
+ toFiniteNumber(value) {
6604
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
6605
+ const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
6606
+ return Number.isFinite(numeric) ? numeric : null;
6607
+ }
5321
6608
  async retrieve(query, options) {
5322
- var _a, _b, _c;
6609
+ var _a, _b, _c, _d, _e, _f;
5323
6610
  const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
5324
6611
  const topK = (_b = options.topK) != null ? _b : 5;
5325
6612
  const cacheKey = `${ns}::${query}`;
5326
6613
  let queryVector = this.embeddingCache.get(cacheKey);
5327
- const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmProvider);
6614
+ const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmRouter.get("fast"));
5328
6615
  console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
5329
6616
  const [retrievedVector, graphData] = await Promise.all([
5330
6617
  // Only embed if we need vector search (strategy is 'vector' or 'both')
@@ -5336,8 +6623,27 @@ ${context}`;
5336
6623
  this.embeddingCache.set(cacheKey, retrievedVector);
5337
6624
  queryVector = retrievedVector;
5338
6625
  }
5339
- const sources = (strategy === "vector" || strategy === "both") && queryVector && queryVector.length > 0 ? await this.vectorDB.query(queryVector, topK, ns, options.filter) : [];
5340
- return { sources, graphData };
6626
+ const baseFilter = __spreadProps(__spreadValues({}, (_d = options.filter) != null ? _d : {}), { queryText: query });
6627
+ const numericPredicates = QueryProcessor.extractNumericPredicates(query, (_e = this.config.rag) == null ? void 0 : _e.filterableFields);
6628
+ if (numericPredicates.length > 0) {
6629
+ baseFilter.__numericPredicates = numericPredicates;
6630
+ }
6631
+ const retrievalLimit = numericPredicates.length > 0 ? Math.max(topK * 20, 100) : topK;
6632
+ const sources = (strategy === "vector" || strategy === "both") && queryVector && queryVector.length > 0 ? this.applyStructuredFilters(
6633
+ await this.vectorDB.query(queryVector, retrievalLimit, ns, baseFilter),
6634
+ baseFilter
6635
+ ) : [];
6636
+ const resolvedSources = [];
6637
+ for (const source of sources) {
6638
+ const parentId = (_f = source.metadata) == null ? void 0 : _f.parent_id;
6639
+ if (parentId) {
6640
+ console.log(`[Pipeline] Multi-Vector: Found child chunk. Parent ID: ${parentId}`);
6641
+ resolvedSources.push(source);
6642
+ } else {
6643
+ resolvedSources.push(source);
6644
+ }
6645
+ }
6646
+ return { sources: resolvedSources, graphData };
5341
6647
  }
5342
6648
  /** Rewrite the user query for better retrieval performance. */
5343
6649
  async rewriteQuery(question, history) {
@@ -5701,15 +7007,24 @@ function createStreamHandler(configOrPlugin) {
5701
7007
  });
5702
7008
  }
5703
7009
  const encoder = new TextEncoder();
7010
+ let isActive = true;
5704
7011
  const stream = new ReadableStream({
5705
7012
  async start(controller) {
5706
- var _a, _b;
5707
- const enqueue = (text) => controller.enqueue(encoder.encode(text));
7013
+ var _a;
7014
+ const enqueue = (text) => {
7015
+ if (!isActive) return;
7016
+ try {
7017
+ controller.enqueue(encoder.encode(text));
7018
+ } catch (err) {
7019
+ console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
7020
+ }
7021
+ };
5708
7022
  try {
5709
7023
  const pipelineStream = plugin.chatStream(message, history, namespace);
5710
7024
  try {
5711
7025
  for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
5712
7026
  const chunk = temp.value;
7027
+ if (!isActive) break;
5713
7028
  if (typeof chunk === "string") {
5714
7029
  enqueue(sseTextFrame(chunk));
5715
7030
  } else {
@@ -5721,8 +7036,7 @@ function createStreamHandler(configOrPlugin) {
5721
7036
  }
5722
7037
  if (sources.length > 0) {
5723
7038
  try {
5724
- const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
5725
- const uiTransformation = (_b = responseChunk == null ? void 0 : responseChunk.ui_transformation) != null ? _b : llmProvider ? await UITransformer.analyzeAndDecide(message, sources, llmProvider) : UITransformer.transform(message, sources, plugin.getConfig());
7039
+ const uiTransformation = (_a = responseChunk == null ? void 0 : responseChunk.ui_transformation) != null ? _a : UITransformer.transform(message, sources, plugin.getConfig());
5726
7040
  if (uiTransformation) {
5727
7041
  enqueue(sseUIFrame(uiTransformation));
5728
7042
  }
@@ -5748,12 +7062,27 @@ function createStreamHandler(configOrPlugin) {
5748
7062
  }
5749
7063
  }
5750
7064
  } catch (streamError) {
5751
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
5752
- console.error("[createStreamHandler] Stream error:", streamError);
5753
- enqueue(sseErrorFrame(errorMessage));
7065
+ if (isActive) {
7066
+ const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
7067
+ console.error("[createStreamHandler] Stream error:", streamError);
7068
+ try {
7069
+ enqueue(sseErrorFrame(errorMessage));
7070
+ } catch (e) {
7071
+ }
7072
+ }
5754
7073
  } finally {
5755
- controller.close();
7074
+ if (isActive) {
7075
+ isActive = false;
7076
+ try {
7077
+ controller.close();
7078
+ } catch (e) {
7079
+ }
7080
+ }
5756
7081
  }
7082
+ },
7083
+ cancel(reason) {
7084
+ isActive = false;
7085
+ console.log("[createStreamHandler] Stream connection closed by client:", reason);
5757
7086
  }
5758
7087
  });
5759
7088
  return new Response(stream, { headers: SSE_HEADERS });
@@ -5814,6 +7143,7 @@ function createUploadHandler(configOrPlugin) {
5814
7143
  if (parsed.data && parsed.data.length > 0) {
5815
7144
  let i = 0;
5816
7145
  let lastRowData = null;
7146
+ const csvCols = Object.keys(parsed.data[0]);
5817
7147
  for (const row of parsed.data) {
5818
7148
  i++;
5819
7149
  const rowData = row;
@@ -5835,12 +7165,14 @@ function createUploadHandler(configOrPlugin) {
5835
7165
  documents.push({
5836
7166
  docId: `${file.name}-row-${i}`,
5837
7167
  content: contentParts.join(", "),
5838
- metadata: __spreadValues(__spreadValues({
7168
+ metadata: __spreadValues(__spreadProps(__spreadValues({
5839
7169
  fileName: file.name,
5840
7170
  fileSize: file.size,
5841
7171
  fileType: file.type,
5842
7172
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
5843
- }, dimension ? { dimension } : {}), rowData)
7173
+ }, dimension ? { dimension } : {}), {
7174
+ csvHeaders: csvCols
7175
+ }), rowData)
5844
7176
  });
5845
7177
  }
5846
7178
  }