@retrivora-ai/rag-engine 1.9.0 → 1.9.1

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 (44) hide show
  1. package/dist/{ILLMProvider-BOJFz3Na.d.mts → ILLMProvider-BQyKZLnd.d.mts} +10 -0
  2. package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-BQyKZLnd.d.ts} +10 -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 +1719 -467
  6. package/dist/handlers/index.mjs +1718 -466
  7. package/dist/{index-BwpcaziY.d.ts → index-A0GqPsaG.d.ts} +1 -1
  8. package/dist/{index-D3V9Et2M.d.mts → index-CDftK3qs.d.mts} +1 -1
  9. package/dist/index.css +26 -0
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +246 -76
  13. package/dist/index.mjs +248 -76
  14. package/dist/server.d.mts +32 -5
  15. package/dist/server.d.ts +32 -5
  16. package/dist/server.js +1726 -671
  17. package/dist/server.mjs +1724 -669
  18. package/package.json +1 -1
  19. package/src/components/MarkdownComponents.tsx +3 -3
  20. package/src/components/MessageBubble.tsx +49 -2
  21. package/src/components/ProductCard.tsx +33 -6
  22. package/src/components/UIDispatcher.tsx +1 -0
  23. package/src/components/VisualizationRenderer.tsx +143 -11
  24. package/src/config/EmbeddingStrategy.ts +5 -4
  25. package/src/config/RagConfig.ts +10 -0
  26. package/src/config/serverConfig.ts +16 -1
  27. package/src/core/LLMRouter.ts +79 -0
  28. package/src/core/Pipeline.ts +262 -45
  29. package/src/core/ProviderRegistry.ts +6 -0
  30. package/src/core/QueryProcessor.ts +98 -0
  31. package/src/handlers/index.ts +8 -5
  32. package/src/hooks/useRagChat.ts +53 -17
  33. package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
  34. package/src/providers/vectordb/ChromaDBProvider.ts +13 -2
  35. package/src/providers/vectordb/MilvusProvider.ts +18 -2
  36. package/src/providers/vectordb/MultiTablePostgresProvider.ts +12 -12
  37. package/src/providers/vectordb/PostgreSQLProvider.ts +1 -1
  38. package/src/providers/vectordb/QdrantProvider.ts +1 -1
  39. package/src/providers/vectordb/RedisProvider.ts +3 -4
  40. package/src/providers/vectordb/WeaviateProvider.ts +41 -3
  41. package/src/types/index.ts +26 -0
  42. package/src/utils/ProductExtractor.ts +5 -3
  43. package/src/utils/UITransformer.ts +1348 -489
  44. package/src/utils/synonyms.ts +2 -0
package/dist/server.js CHANGED
@@ -334,16 +334,222 @@ 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
+
337
543
  // src/providers/vectordb/MultiTablePostgresProvider.ts
338
544
  var MultiTablePostgresProvider_exports = {};
339
545
  __export(MultiTablePostgresProvider_exports, {
340
546
  MultiTablePostgresProvider: () => MultiTablePostgresProvider
341
547
  });
342
- var import_pg, MultiTablePostgresProvider;
548
+ var import_pg2, MultiTablePostgresProvider;
343
549
  var init_MultiTablePostgresProvider = __esm({
344
550
  "src/providers/vectordb/MultiTablePostgresProvider.ts"() {
345
551
  "use strict";
346
- import_pg = require("pg");
552
+ import_pg2 = require("pg");
347
553
  init_BaseVectorProvider();
348
554
  MultiTablePostgresProvider = class extends BaseVectorProvider {
349
555
  constructor(config) {
@@ -361,7 +567,7 @@ var init_MultiTablePostgresProvider = __esm({
361
567
  this.uploadTable = opts.uploadTable || "document_chunks";
362
568
  }
363
569
  async initialize() {
364
- this.pool = new import_pg.Pool({ connectionString: this.connectionString });
570
+ this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
365
571
  const client = await this.pool.connect();
366
572
  try {
367
573
  await client.query("CREATE EXTENSION IF NOT EXISTS vector");
@@ -505,26 +711,25 @@ var init_MultiTablePostgresProvider = __esm({
505
711
  const allResults = [];
506
712
  console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
507
713
  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) : [];
714
+ const entityHints = Array.isArray(_filter == null ? void 0 : _filter.keywords) ? _filter.keywords.map((k) => k.trim().toLowerCase()).filter(Boolean) : [];
511
715
  console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
512
716
  console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
513
717
  const getDynamicKeywordQuery = () => {
514
718
  if (entityHints.length > 0) {
515
- return entityHints.map((h) => h.includes(" ") ? `"${h}"` : h).join(" & ");
719
+ return entityHints.map((h) => h.replace(/\s+/g, " & ")).join(" & ");
516
720
  }
517
721
  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, " & ");
722
+ 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
723
  }
520
724
  return "";
521
725
  };
522
726
  const dynamicKeywordQuery = getDynamicKeywordQuery();
727
+ const tableLimit = Math.max(topK, 50);
523
728
  const queryPromises = this.tables.map(async (table) => {
524
729
  try {
525
730
  let sqlQuery = "";
526
731
  let params = [];
527
- if (queryText) {
732
+ if (queryText && dynamicKeywordQuery) {
528
733
  const hasEntityHints = entityHints.length > 0;
529
734
  const exactNameScoreExpr = hasEntityHints ? `+ (
530
735
  SELECT COALESCE(MAX(
@@ -541,16 +746,16 @@ var init_MultiTablePostgresProvider = __esm({
541
746
  ((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
747
  FROM "${table}" t
543
748
  ORDER BY hybrid_score DESC
544
- LIMIT 50
749
+ LIMIT ${tableLimit}
545
750
  `;
546
751
  params = [vectorLiteral, dynamicKeywordQuery];
547
752
  } else {
548
753
  sqlQuery = `
549
754
  SELECT *,
550
- (1 - (embedding <=> $1::vector)) AS hybrid_score
755
+ (1 - (embedding <=> $1::vector)) AS hybrid_score
551
756
  FROM "${table}" t
552
757
  ORDER BY hybrid_score DESC
553
- LIMIT 50
758
+ LIMIT ${tableLimit}
554
759
  `;
555
760
  params = [vectorLiteral];
556
761
  }
@@ -875,13 +1080,25 @@ var init_MilvusProvider = __esm({
875
1080
  };
876
1081
  await this.http.post("/v1/vector/upsert", payload);
877
1082
  }
878
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
879
1083
  async query(vector, topK, namespace, _filter) {
1084
+ const sanitizedFilter = this.sanitizeFilter(_filter);
1085
+ const filterParts = [];
1086
+ if (namespace) {
1087
+ filterParts.push(`namespace == "${namespace}"`);
1088
+ }
1089
+ for (const [key, value] of Object.entries(sanitizedFilter)) {
1090
+ if (key === "queryText" || key === "keywords") continue;
1091
+ if (typeof value === "string") {
1092
+ filterParts.push(`metadata["${key}"] == "${value.replace(/"/g, '\\"')}"`);
1093
+ } else if (typeof value === "number") {
1094
+ filterParts.push(`metadata["${key}"] == ${value}`);
1095
+ }
1096
+ }
880
1097
  const payload = {
881
1098
  collectionName: this.indexName,
882
1099
  vector,
883
1100
  limit: topK,
884
- filter: namespace ? `namespace == "${namespace}"` : void 0,
1101
+ filter: filterParts.length > 0 ? filterParts.join(" && ") : void 0,
885
1102
  outputFields: ["content", "metadata"],
886
1103
  searchParams: {
887
1104
  nprobe: this.config.options.nprobe || 16,
@@ -1080,6 +1297,7 @@ var init_QdrantProvider = __esm({
1080
1297
  await this.http.put(`/collections/${this.indexName}/points`, payload);
1081
1298
  }
1082
1299
  async query(vector, topK, namespace, _filter) {
1300
+ var _a;
1083
1301
  const must = [];
1084
1302
  if (namespace) {
1085
1303
  must.push({ key: "namespace", match: { value: namespace } });
@@ -1101,7 +1319,7 @@ var init_QdrantProvider = __esm({
1101
1319
  limit: topK,
1102
1320
  with_payload: true,
1103
1321
  params: {
1104
- hnsw_ef: this.config.options.efSearch || Math.max(topK * 20, 128),
1322
+ hnsw_ef: ((_a = this.config.options) == null ? void 0 : _a.efSearch) || Math.max(topK * 20, 128),
1105
1323
  exact: false
1106
1324
  },
1107
1325
  filter: must.length > 0 ? { must } : void 0
@@ -1226,12 +1444,20 @@ var init_ChromaDBProvider = __esm({
1226
1444
  };
1227
1445
  await this.http.post(`/api/v1/collections/${this.collectionId}/add`, payload);
1228
1446
  }
1229
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1230
1447
  async query(vector, topK, namespace, _filter) {
1448
+ const sanitizedFilter = this.sanitizeFilter(_filter);
1449
+ const whereClauses = [];
1450
+ if (namespace) whereClauses.push({ namespace: { $eq: namespace } });
1451
+ Object.entries(sanitizedFilter).forEach(([key, value]) => {
1452
+ if (key === "namespace") return;
1453
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1454
+ whereClauses.push({ [key]: { $eq: value } });
1455
+ }
1456
+ });
1231
1457
  const payload = {
1232
1458
  query_embeddings: [vector],
1233
1459
  n_results: topK,
1234
- where: namespace ? { namespace: { $eq: namespace } } : void 0
1460
+ where: whereClauses.length > 1 ? { $and: whereClauses } : whereClauses[0]
1235
1461
  };
1236
1462
  const { data } = await this.http.post(`/api/v1/collections/${this.collectionId}/query`, payload);
1237
1463
  const matches = [];
@@ -1336,15 +1562,15 @@ var init_RedisProvider = __esm({
1336
1562
  console.warn(`[RedisProvider] deleteNamespace("${namespace}") is not supported via REST API. Use Redis CLI: SCAN + DEL`);
1337
1563
  }
1338
1564
  /**
1339
- * Redis is TCP-based and has no HTTP health endpoint.
1340
- * Returns true; actual connectivity is validated on the first operation.
1565
+ * Check reachability via a PING command.
1566
+ * Returns false on connection failure so health checks surface real problems.
1341
1567
  */
1342
1568
  async ping() {
1343
1569
  try {
1344
1570
  await this.http.post("/", ["PING"]);
1345
1571
  return true;
1346
1572
  } catch (e) {
1347
- return true;
1573
+ return false;
1348
1574
  }
1349
1575
  }
1350
1576
  async disconnect() {
@@ -1384,15 +1610,17 @@ var init_WeaviateProvider = __esm({
1384
1610
  await this.ping();
1385
1611
  }
1386
1612
  async upsert(doc, namespace) {
1613
+ const primitiveMetadata = this.extractPrimitiveMetadata(doc.metadata);
1387
1614
  const payload = {
1388
1615
  class: this.indexName,
1389
1616
  id: doc.id,
1390
1617
  vector: doc.vector,
1391
- properties: {
1618
+ properties: __spreadProps(__spreadValues({
1392
1619
  content: doc.content,
1393
- metadata: JSON.stringify(doc.metadata || {}),
1620
+ metadata: JSON.stringify(doc.metadata || {})
1621
+ }, primitiveMetadata), {
1394
1622
  namespace: namespace || ""
1395
- }
1623
+ })
1396
1624
  };
1397
1625
  await this.http.post("/v1/objects", payload);
1398
1626
  }
@@ -1402,20 +1630,22 @@ var init_WeaviateProvider = __esm({
1402
1630
  class: this.indexName,
1403
1631
  id: doc.id,
1404
1632
  vector: doc.vector,
1405
- properties: {
1633
+ properties: __spreadProps(__spreadValues({
1406
1634
  content: doc.content,
1407
- metadata: JSON.stringify(doc.metadata || {}),
1635
+ metadata: JSON.stringify(doc.metadata || {})
1636
+ }, this.extractPrimitiveMetadata(doc.metadata)), {
1408
1637
  namespace: namespace || ""
1409
- }
1638
+ })
1410
1639
  }))
1411
1640
  };
1412
1641
  await this.http.post("/v1/batch/objects", payload);
1413
1642
  }
1414
1643
  async query(vector, topK, namespace, _filter) {
1415
1644
  var _a, _b;
1645
+ const queryText = _filter == null ? void 0 : _filter.queryText;
1416
1646
  const sanitizedFilter = this.sanitizeFilter(_filter);
1417
- const queryText = sanitizedFilter.queryText;
1418
1647
  const searchParams = queryText ? `hybrid: { query: ${JSON.stringify(queryText)}, alpha: 0.5 }` : `nearVector: { vector: ${JSON.stringify(vector)} }`;
1648
+ const where = this.buildWhereFilter(namespace, sanitizedFilter);
1419
1649
  const graphqlQuery = {
1420
1650
  query: `
1421
1651
  {
@@ -1423,7 +1653,7 @@ var init_WeaviateProvider = __esm({
1423
1653
  ${this.indexName}(
1424
1654
  ${searchParams}
1425
1655
  limit: ${topK}
1426
- ${namespace ? `where: { path: ["namespace"], operator: Equal, valueString: "${namespace}" }` : ""}
1656
+ ${where ? `where: ${where}` : ""}
1427
1657
  ) {
1428
1658
  content
1429
1659
  metadata
@@ -1467,6 +1697,34 @@ var init_WeaviateProvider = __esm({
1467
1697
  }
1468
1698
  async disconnect() {
1469
1699
  }
1700
+ extractPrimitiveMetadata(metadata) {
1701
+ const result = {};
1702
+ Object.entries(metadata || {}).forEach(([key, value]) => {
1703
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1704
+ result[key] = value;
1705
+ }
1706
+ });
1707
+ return result;
1708
+ }
1709
+ buildWhereFilter(namespace, filter) {
1710
+ const operands = [];
1711
+ if (namespace) operands.push(this.weaviateOperand("namespace", namespace));
1712
+ Object.entries(filter || {}).forEach(([key, value]) => {
1713
+ if (key === "namespace") return;
1714
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1715
+ operands.push(this.weaviateOperand(key, value));
1716
+ }
1717
+ });
1718
+ if (operands.length === 0) return void 0;
1719
+ if (operands.length === 1) return operands[0];
1720
+ return `{ operator: And, operands: [${operands.join(", ")}] }`;
1721
+ }
1722
+ weaviateOperand(key, value) {
1723
+ const path = `path: [${JSON.stringify(key)}], operator: Equal`;
1724
+ if (typeof value === "number") return `{ ${path}, valueNumber: ${value} }`;
1725
+ if (typeof value === "boolean") return `{ ${path}, valueBoolean: ${value} }`;
1726
+ return `{ ${path}, valueString: ${JSON.stringify(value)} }`;
1727
+ }
1470
1728
  };
1471
1729
  }
1472
1730
  });
@@ -1820,7 +2078,7 @@ function getRagConfig(baseConfig, env = process.env) {
1820
2078
  return getEnvConfig(env, baseConfig);
1821
2079
  }
1822
2080
  function getEnvConfig(env = process.env, base) {
1823
- 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;
2081
+ 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;
1824
2082
  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__";
1825
2083
  const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
1826
2084
  const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
@@ -1874,11 +2132,22 @@ function getEnvConfig(env = process.env, base) {
1874
2132
  };
1875
2133
  const embeddingApiKeyByProvider = {
1876
2134
  openai: readString(env, "OPENAI_API_KEY"),
2135
+ anthropic: readString(env, "ANTHROPIC_API_KEY"),
2136
+ // Anthropic needs a separate embedding provider; key kept for completeness
1877
2137
  gemini: readString(env, "GEMINI_API_KEY"),
1878
2138
  ollama: void 0,
1879
2139
  universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
1880
2140
  custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
1881
2141
  };
2142
+ const DEFAULT_MODEL_BY_PROVIDER = {
2143
+ openai: "gpt-4o",
2144
+ anthropic: "claude-3-5-sonnet-20241022",
2145
+ gemini: "gemini-2.0-flash",
2146
+ ollama: "llama3",
2147
+ universal_rest: "default",
2148
+ rest: "default",
2149
+ custom: "default"
2150
+ };
1882
2151
  return {
1883
2152
  projectId,
1884
2153
  vectorDb: {
@@ -1888,8 +2157,8 @@ function getEnvConfig(env = process.env, base) {
1888
2157
  },
1889
2158
  llm: {
1890
2159
  provider: llmProvider,
1891
- model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
1892
- apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
2160
+ model: (_ja = (_ia = readString(env, "LLM_MODEL")) != null ? _ia : DEFAULT_MODEL_BY_PROVIDER[llmProvider]) != null ? _ja : "gpt-4o",
2161
+ apiKey: (_ka = llmApiKeyByProvider[llmProvider]) != null ? _ka : "",
1893
2162
  baseUrl: readString(env, "LLM_BASE_URL"),
1894
2163
  systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
1895
2164
  maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
@@ -1900,7 +2169,7 @@ function getEnvConfig(env = process.env, base) {
1900
2169
  },
1901
2170
  embedding: {
1902
2171
  provider: embeddingProvider,
1903
- model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
2172
+ model: (_la = readString(env, "EMBEDDING_MODEL")) != null ? _la : "text-embedding-3-small",
1904
2173
  apiKey: embeddingApiKeyByProvider[embeddingProvider],
1905
2174
  baseUrl: readString(env, "EMBEDDING_BASE_URL"),
1906
2175
  dimensions: embeddingDimensions,
@@ -1911,17 +2180,17 @@ function getEnvConfig(env = process.env, base) {
1911
2180
  }
1912
2181
  },
1913
2182
  ui: {
1914
- title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
1915
- subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
1916
- primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
1917
- accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
1918
- logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
1919
- placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
1920
- showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
1921
- 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.",
1922
- visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
1923
- borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
1924
- allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
2183
+ title: (_na = (_ma = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _ma : readString(env, "UI_TITLE")) != null ? _na : "AI Assistant",
2184
+ subtitle: (_pa = (_oa = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _oa : readString(env, "UI_SUBTITLE")) != null ? _pa : "Powered by RAG",
2185
+ primaryColor: (_ra = (_qa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _qa : readString(env, "UI_PRIMARY_COLOR")) != null ? _ra : "#10b981",
2186
+ accentColor: (_ta = (_sa = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _sa : readString(env, "UI_ACCENT_COLOR")) != null ? _ta : "#3b82f6",
2187
+ logoUrl: (_ua = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ua : readString(env, "UI_LOGO_URL"),
2188
+ placeholder: (_wa = (_va = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _va : readString(env, "UI_PLACEHOLDER")) != null ? _wa : "Ask me anything\u2026",
2189
+ showSources: ((_ya = (_xa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _xa : readString(env, "UI_SHOW_SOURCES")) != null ? _ya : "true") !== "false",
2190
+ 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.",
2191
+ visualStyle: (_Ca = (_Ba = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Ba : readString(env, "UI_VISUAL_STYLE")) != null ? _Ca : "glass",
2192
+ borderRadius: (_Ea = (_Da = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Da : readString(env, "UI_BORDER_RADIUS")) != null ? _Ea : "xl",
2193
+ allowUpload: ((_Ga = (_Fa = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Fa : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Ga : "false") === "true"
1925
2194
  },
1926
2195
  rag: {
1927
2196
  topK: readNumber(env, "RAG_TOP_K", 5),
@@ -2822,7 +3091,7 @@ var VECTOR_PROFILES = {
2822
3091
  // src/llm/providers/UniversalLLMAdapter.ts
2823
3092
  var UniversalLLMAdapter = class {
2824
3093
  constructor(config) {
2825
- var _a, _b, _c, _d, _e, _f, _g;
3094
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2826
3095
  this.model = config.model;
2827
3096
  const llmConfig = config;
2828
3097
  const options = (_a = llmConfig.options) != null ? _a : {};
@@ -2836,16 +3105,18 @@ var UniversalLLMAdapter = class {
2836
3105
  this.systemPrompt = (_c = llmConfig.systemPrompt) != null ? _c : "You are a helpful AI assistant. Use the provided context to answer the user.";
2837
3106
  this.maxTokens = (_d = llmConfig.maxTokens) != null ? _d : 1024;
2838
3107
  this.temperature = (_e = llmConfig.temperature) != null ? _e : 0;
2839
- const baseUrl = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl;
2840
- if (!baseUrl) {
3108
+ this.apiKey = config.apiKey;
3109
+ this.baseUrl = (_g = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl) != null ? _g : "";
3110
+ if (!this.baseUrl) {
2841
3111
  throw new Error("[UniversalLLMAdapter] baseUrl is required in config or config.options");
2842
3112
  }
3113
+ this.resolvedHeaders = __spreadValues(__spreadValues({
3114
+ "Content-Type": "application/json"
3115
+ }, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {});
2843
3116
  this.http = import_axios2.default.create({
2844
- baseURL: baseUrl,
2845
- headers: __spreadValues(__spreadValues({
2846
- "Content-Type": "application/json"
2847
- }, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {}),
2848
- timeout: (_g = this.opts.timeout) != null ? _g : 6e4
3117
+ baseURL: this.baseUrl,
3118
+ headers: this.resolvedHeaders,
3119
+ timeout: (_h = this.opts.timeout) != null ? _h : 6e4
2849
3120
  });
2850
3121
  }
2851
3122
  async chat(messages, context) {
@@ -2882,6 +3153,92 @@ ${context != null ? context : "None"}` },
2882
3153
  }
2883
3154
  return String(result);
2884
3155
  }
3156
+ /**
3157
+ * Streaming chat using native fetch + ReadableStream.
3158
+ * Parses OpenAI-compatible SSE frames: `data: {...}\n\n`
3159
+ * Works with vLLM, LMStudio, Together AI, Fireworks, and any OpenAI-compatible API.
3160
+ */
3161
+ chatStream(messages, context) {
3162
+ return __asyncGenerator(this, null, function* () {
3163
+ var _a, _b, _c;
3164
+ const path = (_a = this.opts.chatPath) != null ? _a : "/chat/completions";
3165
+ const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
3166
+ const extractPath = ((_b = this.opts.responseExtractPath) != null ? _b : "choices[0].message.content").replace("message.content", "delta.content");
3167
+ const formattedMessages = [
3168
+ { role: "system", content: `${this.systemPrompt}
3169
+
3170
+ Context:
3171
+ ${context != null ? context : "None"}` },
3172
+ ...messages
3173
+ ];
3174
+ let payload;
3175
+ if (this.opts.chatPayloadTemplate) {
3176
+ payload = buildPayload(this.opts.chatPayloadTemplate, {
3177
+ model: this.model,
3178
+ messages: formattedMessages,
3179
+ maxTokens: this.maxTokens,
3180
+ temperature: this.temperature
3181
+ });
3182
+ if (typeof payload === "object" && payload !== null) {
3183
+ payload.stream = true;
3184
+ }
3185
+ } else {
3186
+ payload = {
3187
+ model: this.model,
3188
+ messages: formattedMessages,
3189
+ max_tokens: this.maxTokens,
3190
+ temperature: this.temperature,
3191
+ stream: true
3192
+ };
3193
+ }
3194
+ const response = yield new __await(fetch(url, {
3195
+ method: "POST",
3196
+ headers: this.resolvedHeaders,
3197
+ body: JSON.stringify(payload)
3198
+ }));
3199
+ if (!response.ok) {
3200
+ const errorText = yield new __await(response.text().catch(() => response.statusText));
3201
+ throw new Error(`[UniversalLLMAdapter] Streaming request failed (${response.status}): ${errorText}`);
3202
+ }
3203
+ if (!response.body) {
3204
+ throw new Error("[UniversalLLMAdapter] Response body is null \u2014 server did not send a streaming response.");
3205
+ }
3206
+ const reader = response.body.getReader();
3207
+ const decoder = new TextDecoder("utf-8");
3208
+ let buffer = "";
3209
+ try {
3210
+ while (true) {
3211
+ const { done, value } = yield new __await(reader.read());
3212
+ if (done) break;
3213
+ buffer += decoder.decode(value, { stream: true });
3214
+ const lines = buffer.split("\n");
3215
+ buffer = (_c = lines.pop()) != null ? _c : "";
3216
+ for (const line of lines) {
3217
+ const trimmed = line.trim();
3218
+ if (!trimmed || trimmed === "data: [DONE]") continue;
3219
+ if (!trimmed.startsWith("data:")) continue;
3220
+ try {
3221
+ const json = JSON.parse(trimmed.slice(5).trim());
3222
+ const text = resolvePath(json, extractPath);
3223
+ if (text && typeof text === "string") yield text;
3224
+ } catch (e) {
3225
+ }
3226
+ }
3227
+ }
3228
+ if (buffer.trim() && buffer.trim() !== "data: [DONE]") {
3229
+ const jsonStr = buffer.replace(/^data:\s*/, "").trim();
3230
+ try {
3231
+ const json = JSON.parse(jsonStr);
3232
+ const text = resolvePath(json, extractPath);
3233
+ if (text && typeof text === "string") yield text;
3234
+ } catch (e) {
3235
+ }
3236
+ }
3237
+ } finally {
3238
+ reader.releaseLock();
3239
+ }
3240
+ });
3241
+ }
2885
3242
  async embed(text) {
2886
3243
  var _a, _b;
2887
3244
  const path = (_a = this.opts.embedPath) != null ? _a : "/embeddings";
@@ -3078,6 +3435,7 @@ var ProviderRegistry = class {
3078
3435
  return null;
3079
3436
  }
3080
3437
  static async loadVectorProviderClass(provider) {
3438
+ var _a;
3081
3439
  if (this.vectorProviders[provider]) return this.vectorProviders[provider];
3082
3440
  switch (provider) {
3083
3441
  case "pinecone": {
@@ -3086,6 +3444,11 @@ var ProviderRegistry = class {
3086
3444
  }
3087
3445
  case "pgvector":
3088
3446
  case "postgresql": {
3447
+ const postgresMode = ((_a = process.env.POSTGRES_MODE) != null ? _a : "multi").toLowerCase();
3448
+ if (postgresMode === "single") {
3449
+ const { PostgreSQLProvider: PostgreSQLProvider2 } = await Promise.resolve().then(() => (init_PostgreSQLProvider(), PostgreSQLProvider_exports));
3450
+ return PostgreSQLProvider2;
3451
+ }
3089
3452
  const { MultiTablePostgresProvider: MultiTablePostgresProvider2 } = await Promise.resolve().then(() => (init_MultiTablePostgresProvider(), MultiTablePostgresProvider_exports));
3090
3453
  return MultiTablePostgresProvider2;
3091
3454
  }
@@ -4085,6 +4448,76 @@ var QueryProcessor = class {
4085
4448
  }
4086
4449
  return [...hints.values()];
4087
4450
  }
4451
+ static extractNumericPredicates(question, validFields = []) {
4452
+ const predicates = [];
4453
+ const seen = /* @__PURE__ */ new Set();
4454
+ const comparatorPattern = [
4455
+ "greater than or equal to",
4456
+ "more than or equal to",
4457
+ "less than or equal to",
4458
+ "greater than",
4459
+ "more than",
4460
+ "less than",
4461
+ "equal to",
4462
+ "at least",
4463
+ "at most",
4464
+ "above",
4465
+ "over",
4466
+ "below",
4467
+ "under",
4468
+ "equals?",
4469
+ ">=",
4470
+ "<=",
4471
+ ">",
4472
+ "<",
4473
+ "="
4474
+ ].join("|");
4475
+ const addPredicate = (rawField, rawOperator, rawValue) => {
4476
+ const value = Number(rawValue.replace(/,/g, ""));
4477
+ if (!Number.isFinite(value)) return;
4478
+ const operator = this.normalizeNumericOperator(rawOperator);
4479
+ const field = rawField ? this.normalizePredicateField(rawField, validFields) : void 0;
4480
+ const key = `${field != null ? field : "*"}::${operator}::${value}`;
4481
+ if (seen.has(key)) return;
4482
+ seen.add(key);
4483
+ predicates.push(__spreadProps(__spreadValues({}, field ? { field } : {}), { operator, value }));
4484
+ };
4485
+ const scopedPatterns = [
4486
+ 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"),
4487
+ new RegExp(`\\b([a-zA-Z][a-zA-Z0-9_\\s\\-/]{1,80}?)\\s+(?:is|are|was|were)?\\s*(?:${comparatorPattern})\\s+([\\d,]+(?:\\.\\d+)?)`, "gi")
4488
+ ];
4489
+ for (const pattern of scopedPatterns) {
4490
+ for (const match of question.matchAll(pattern)) {
4491
+ const full = match[0];
4492
+ const operatorMatch = full.match(new RegExp(`(${comparatorPattern})`, "i"));
4493
+ if (!operatorMatch) continue;
4494
+ addPredicate(match[1], operatorMatch[1], match[2]);
4495
+ }
4496
+ }
4497
+ for (const match of question.matchAll(/\b([a-zA-Z][a-zA-Z0-9_\s\-/]{1,80}?)\s*(>=|<=|>|<|=)\s*([\d,]+(?:\.\d+)?)/g)) {
4498
+ addPredicate(match[1], match[2], match[3]);
4499
+ }
4500
+ return predicates;
4501
+ }
4502
+ static normalizeNumericOperator(operator) {
4503
+ const op = operator.toLowerCase().trim();
4504
+ if (op === ">" || /\b(greater than|more than|above|over)\b/.test(op)) return "gt";
4505
+ if (op === ">=" || /\b(greater than or equal to|more than or equal to|at least)\b/.test(op)) return "gte";
4506
+ if (op === "<" || /\b(less than|below|under)\b/.test(op)) return "lt";
4507
+ if (op === "<=" || /\b(less than or equal to|at most)\b/.test(op)) return "lte";
4508
+ return "eq";
4509
+ }
4510
+ static normalizePredicateField(field, validFields) {
4511
+ 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();
4512
+ if (validFields.length === 0) return cleaned;
4513
+ const comparable = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
4514
+ const cleanedComparable = comparable(cleaned);
4515
+ const matchedField = validFields.find((fieldName) => {
4516
+ const candidate = comparable(fieldName);
4517
+ return candidate === cleanedComparable || candidate.includes(cleanedComparable) || cleanedComparable.includes(candidate);
4518
+ });
4519
+ return matchedField != null ? matchedField : cleaned;
4520
+ }
4088
4521
  /**
4089
4522
  * Constructs a QueryFilter object from extracted hints.
4090
4523
  *
@@ -4105,6 +4538,10 @@ var QueryProcessor = class {
4105
4538
  }
4106
4539
  if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
4107
4540
  if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
4541
+ const numericPredicates = this.extractNumericPredicates(question);
4542
+ if (numericPredicates.length > 0) {
4543
+ filter.__numericPredicates = numericPredicates;
4544
+ }
4108
4545
  return filter;
4109
4546
  }
4110
4547
  /**
@@ -4172,6 +4609,61 @@ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
4172
4609
  }
4173
4610
  };
4174
4611
 
4612
+ // src/core/LLMRouter.ts
4613
+ var FAST_MODEL_DEFAULTS = {
4614
+ openai: "gpt-4o-mini",
4615
+ gemini: "gemini-2.0-flash",
4616
+ anthropic: "claude-3-haiku-20240307",
4617
+ ollama: "",
4618
+ // Ollama has no universal lightweight default — reuse main model
4619
+ rest: "",
4620
+ universal_rest: "",
4621
+ custom: ""
4622
+ };
4623
+ var LLMRouter = class {
4624
+ constructor(config) {
4625
+ this.config = config;
4626
+ this.models = /* @__PURE__ */ new Map();
4627
+ }
4628
+ /**
4629
+ * Initialize all LLM roles.
4630
+ *
4631
+ * @param prebuiltDefault - optional pre-built provider (from EmbeddingStrategyResolver).
4632
+ * When provided it is used directly as the 'default' role without re-constructing.
4633
+ */
4634
+ async initialize(prebuiltDefault) {
4635
+ var _a;
4636
+ const defaultModel = prebuiltDefault != null ? prebuiltDefault : LLMFactory.create(this.config.llm, this.config.embedding);
4637
+ this.models.set("default", defaultModel);
4638
+ const envFastModel = process.env.FAST_LLM_MODEL;
4639
+ const providerFastDefault = (_a = FAST_MODEL_DEFAULTS[this.config.llm.provider]) != null ? _a : "";
4640
+ const fastModelName = envFastModel || providerFastDefault;
4641
+ if (fastModelName && fastModelName !== this.config.llm.model) {
4642
+ console.log(`[LLMRouter] Fast role \u2192 provider="${this.config.llm.provider}" model="${fastModelName}"`);
4643
+ const fastConfig = __spreadProps(__spreadValues({}, this.config.llm), {
4644
+ model: fastModelName
4645
+ });
4646
+ this.models.set("fast", LLMFactory.create(fastConfig, this.config.embedding));
4647
+ } else {
4648
+ console.log(`[LLMRouter] Fast role \u2192 reusing default model (no lightweight alternative configured).`);
4649
+ this.models.set("fast", defaultModel);
4650
+ }
4651
+ this.models.set("powerful", defaultModel);
4652
+ }
4653
+ /**
4654
+ * Retrieve a model provider by its task role.
4655
+ * Falls back to 'default' if the requested role is not registered.
4656
+ */
4657
+ get(role) {
4658
+ var _a;
4659
+ const provider = (_a = this.models.get(role)) != null ? _a : this.models.get("default");
4660
+ if (!provider) {
4661
+ throw new Error(`[LLMRouter] No provider registered for role: "${role}". Did you call initialize()?`);
4662
+ }
4663
+ return provider;
4664
+ }
4665
+ };
4666
+
4175
4667
  // src/utils/synonyms.ts
4176
4668
  var FIELD_SYNONYMS = {
4177
4669
  name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
@@ -4201,6 +4693,14 @@ var FIELD_SYNONYMS = {
4201
4693
  "in stock",
4202
4694
  "status"
4203
4695
  ],
4696
+ category: [
4697
+ "product_category",
4698
+ "product category",
4699
+ "category_name",
4700
+ "category name",
4701
+ "department",
4702
+ "collection"
4703
+ ],
4204
4704
  description: ["summary", "content", "body", "text", "info", "details"],
4205
4705
  link: ["url", "href", "product_url", "page_url", "link"]
4206
4706
  };
@@ -4232,98 +4732,457 @@ function resolveMetadataValue(meta, uiKey) {
4232
4732
 
4233
4733
  // src/utils/UITransformer.ts
4234
4734
  var UITransformer = class {
4735
+ // ─── Public Entry Points ─────────────────────────────────────────────────
4235
4736
  /**
4236
- * Main transformation method
4237
- * Analyzes user query and retrieved data to determine if a product carousel is needed.
4737
+ * Heuristic-only transform (no LLM required).
4738
+ * Uses the lightweight heuristic intent detector as a fallback.
4739
+ * Prefer `analyzeAndDecide()` in production.
4238
4740
  */
4239
- static transform(userQuery, retrievedData, config, trainedSchema) {
4741
+ static transform(userQuery, retrievedData, config, trainedSchema, intent) {
4742
+ var _a, _b, _c;
4240
4743
  if (!retrievedData || retrievedData.length === 0) {
4241
4744
  return this.createTextResponse("No data available", "No relevant data found for your query.");
4242
4745
  }
4243
- const isStockRequest = this.isStockQuery(userQuery);
4244
- const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
4245
- const categories = this.detectCategories(filteredData);
4746
+ const resolvedIntent = intent != null ? intent : this.detectIntentHeuristic(userQuery);
4747
+ const filteredData = resolvedIntent.filterInStockOnly ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
4748
+ const profile = this.profileData(filteredData);
4246
4749
  const hasProducts = filteredData.some((item) => this.isProductData(item));
4247
- const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
4248
- const isTrendQuery = this.isTrendQuery(userQuery);
4249
- if (isTrendQuery && isTimeSeries) {
4250
- return this.transformToLineChart(filteredData);
4750
+ const wantsPieLikeChart = ["pie_chart", "donut_chart"].includes(resolvedIntent.recommendedChart);
4751
+ if (resolvedIntent.visualizationHint === "trend" && profile.dateFields.length > 0 && profile.numericFields.length > 0) {
4752
+ return this.transformToLineChart(profile);
4251
4753
  }
4252
- if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
4253
- return this.transformToProductCarousel(filteredData, config, trainedSchema);
4754
+ if (wantsPieLikeChart || ["composition", "category_breakdown"].includes(resolvedIntent.visualizationHint)) {
4755
+ const pieChart = this.transformToPieChart(filteredData, profile, userQuery);
4756
+ if (pieChart) return pieChart;
4254
4757
  }
4255
- if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
4256
- return this.transformToPieChart(filteredData);
4758
+ if (["comparison", "ranking"].includes(resolvedIntent.visualizationHint)) {
4759
+ return this.transformToBarChart(filteredData, profile, userQuery, resolvedIntent.visualizationHint === "ranking");
4257
4760
  }
4258
- if (hasProducts) {
4259
- return this.transformToProductCarousel(filteredData, config, trainedSchema);
4761
+ if (resolvedIntent.visualizationHint === "distribution") {
4762
+ return (_a = this.transformToHistogram(profile, userQuery)) != null ? _a : this.transformToBarChart(filteredData, profile, userQuery);
4763
+ }
4764
+ if (resolvedIntent.visualizationHint === "correlation") {
4765
+ return (_b = this.transformToScatterPlot(profile, userQuery)) != null ? _b : this.transformToBarChart(filteredData, profile, userQuery);
4766
+ }
4767
+ if (resolvedIntent.visualizationHint === "geographic") {
4768
+ return this.transformToBarChart(filteredData, profile, userQuery);
4260
4769
  }
4261
- if (this.hasMultipleFields(filteredData)) {
4262
- return this.transformToTable(filteredData);
4770
+ if (this.isStructuredListQuery(userQuery)) {
4771
+ return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
4772
+ }
4773
+ if (resolvedIntent.visualizationHint === "kpi") {
4774
+ return (_c = this.transformToMetricCard(profile, userQuery)) != null ? _c : this.transformToText(filteredData);
4775
+ }
4776
+ if (["tabular", "table"].includes(resolvedIntent.visualizationHint) || resolvedIntent.wantsExplicitTable) {
4777
+ return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
4778
+ }
4779
+ if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === "product_browse") {
4780
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
4263
4781
  }
4782
+ const automatic = this.chooseAutomaticVisualization(filteredData, profile, userQuery);
4783
+ if (automatic) return automatic;
4264
4784
  return this.transformToText(filteredData);
4265
4785
  }
4266
4786
  /**
4267
- * Transform data to product carousel format
4268
- */
4269
- static transformToProductCarousel(data, config, trainedSchema) {
4270
- const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
4271
- return {
4272
- type: "product_carousel",
4273
- title: "Recommended Products",
4274
- description: `Found ${products.length} relevant products`,
4275
- data: products
4276
- };
4277
- }
4278
- /**
4279
- * Transform data to pie chart format
4787
+ * LLM-driven entry point (recommended for production).
4788
+ *
4789
+ * Step 1 — Detect intent via a dedicated, lightweight LLM call.
4790
+ * Step 2 Pass the intent + data to the visualization-selection prompt.
4791
+ * Step 3 — Fall back to the heuristic `transform()` if either LLM call fails.
4280
4792
  */
4281
- static transformToPieChart(data) {
4282
- const categories = this.detectCategories(data);
4283
- const categoryData = this.aggregateByCategory(data, categories);
4284
- const pieData = Object.entries(categoryData).map(([label, count]) => {
4285
- const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
4286
- return {
4287
- label,
4288
- value: count,
4289
- inStockCount,
4290
- outOfStockCount
4291
- };
4292
- });
4793
+ static async analyzeAndDecide(query, sources, llm) {
4794
+ let intent;
4795
+ try {
4796
+ intent = await this.detectIntent(query, llm);
4797
+ console.debug("[UITransformer] Detected intent:", intent);
4798
+ } catch (err) {
4799
+ console.warn("[UITransformer] Intent detection failed; using heuristic.", err);
4800
+ intent = this.detectIntentHeuristic(query);
4801
+ }
4802
+ if (this.isProductQuery(query) && ["text", "product_browse"].includes(intent.visualizationHint)) {
4803
+ return this.transform(
4804
+ query,
4805
+ sources,
4806
+ void 0,
4807
+ void 0,
4808
+ __spreadProps(__spreadValues({}, intent), { visualizationHint: "product_browse", recommendedChart: "text" })
4809
+ );
4810
+ }
4811
+ try {
4812
+ const context = this.buildContextSummary(sources);
4813
+ const systemPrompt = this.buildVisualizationSystemPrompt();
4814
+ const userPrompt = [
4815
+ `USER QUESTION: ${query}`,
4816
+ "",
4817
+ `DETECTED INTENT (JSON): ${JSON.stringify(intent)}`,
4818
+ "",
4819
+ "RETRIEVED DATA (JSON):",
4820
+ context
4821
+ ].join("\n");
4822
+ const rawResponse = await llm.chat(
4823
+ [{ role: "user", content: userPrompt }],
4824
+ "",
4825
+ { systemPrompt, temperature: 0 }
4826
+ );
4827
+ const parsed = this.parseTransformationResponse(rawResponse);
4828
+ if (parsed) {
4829
+ const intentAllowsTable = intent.wantsExplicitTable || ["tabular", "table", "geographic"].includes(intent.visualizationHint);
4830
+ const intentWantsPieLikeChart = ["pie_chart", "donut_chart"].includes(intent.recommendedChart) || ["composition", "category_breakdown"].includes(intent.visualizationHint);
4831
+ if (parsed.type === "table" && !intentAllowsTable) {
4832
+ console.debug("[UITransformer] LLM chose table but intent says no. Falling back to text.");
4833
+ return this.transform(query, sources, void 0, void 0, intent);
4834
+ }
4835
+ if (intentWantsPieLikeChart && parsed.type !== "pie_chart" && this.detectCategories(sources).length > 1) {
4836
+ console.debug("[UITransformer] LLM ignored pie/composition intent. Using deterministic pie chart.");
4837
+ return this.transform(query, sources, void 0, void 0, intent);
4838
+ }
4839
+ console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
4840
+ return parsed;
4841
+ }
4842
+ console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
4843
+ } catch (err) {
4844
+ console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
4845
+ }
4846
+ return this.transform(query, sources, void 0, void 0, intent);
4847
+ }
4848
+ // ─── Dynamic Intent Detection ─────────────────────────────────────────────
4849
+ /**
4850
+ * Calls the LLM with a compact, focused prompt to extract a structured
4851
+ * `QueryIntent` from the user's query.
4852
+ *
4853
+ * Keeping this as a *separate* call from visualization selection means:
4854
+ * - The prompt is shorter and more reliable.
4855
+ * - The intent object can be reused across both the heuristic and LLM paths.
4856
+ * - It is easy to unit-test intent detection in isolation.
4857
+ */
4858
+ static async detectIntent(query, llm) {
4859
+ const systemPrompt = `You are an intent classifier for a product-search RAG system.
4860
+ Given a user query, return ONLY a valid JSON object with this exact shape \u2014 no prose, no markdown:
4861
+
4862
+ {
4863
+ "visualizationHint": "trend" | "comparison" | "distribution" | "composition" | "correlation" | "ranking" | "kpi" | "tabular" | "geographic" | "product_browse" | "table" | "text",
4864
+ "recommendedChart": "line_chart" | "bar_chart" | "histogram" | "pie_chart" | "donut_chart" | "scatter_plot" | "horizontal_bar" | "metric_card" | "table" | "geo_map" | "text",
4865
+ "filterInStockOnly": boolean,
4866
+ "wantsExplicitTable": boolean,
4867
+ "isTemporal": boolean,
4868
+ "isComparison": boolean,
4869
+ "language": "<BCP-47 language tag, e.g. en, de, hi, ja>",
4870
+ "reasoning": "<one short sentence \u2014 why you chose these values>"
4871
+ }
4872
+
4873
+ RULES:
4874
+ - visualizationHint and recommendedChart
4875
+ "trend" \u2192 keywords: trend, growth, over time; recommendedChart "line_chart"
4876
+ "comparison" \u2192 keywords: compare, versus, top; recommendedChart "bar_chart"
4877
+ "distribution" \u2192 keywords: distribution, spread; recommendedChart "histogram"
4878
+ "composition" \u2192 keywords: share, percentage, breakup, breakdown; recommendedChart "pie_chart" or "donut_chart"
4879
+ "correlation" \u2192 keywords: relation, correlation; recommendedChart "scatter_plot"
4880
+ "ranking" \u2192 keywords: highest, lowest, top 10, ranking; recommendedChart "horizontal_bar"
4881
+ "kpi" \u2192 keywords: total, average, count; recommendedChart "metric_card"
4882
+ "tabular" \u2192 keywords: detailed records, table, grid, spreadsheet; recommendedChart "table"
4883
+ "geographic" \u2192 keywords: region, country, map; recommendedChart "geo_map"
4884
+ "product_browse" \u2192 user is browsing, searching, describing, viewing details for, or asking about one or more products without analytical visualization intent
4885
+ "table" \u2192 legacy alias for "tabular"; use only if the query literally says table
4886
+ "text" \u2192 conversational, factual, or other intent
4887
+ - If the user explicitly asks for a chart type, honor that chart type in recommendedChart.
4888
+ - If a query says "distribution ... in a pie chart", use visualizationHint "composition" and recommendedChart "pie_chart".
4889
+ - filterInStockOnly: true only if user mentions stock, availability, in stock, etc.
4890
+ - wantsExplicitTable: true if the user asks for a table, list, grid, spreadsheet, or detailed records.
4891
+ - isTemporal: true if time words appear (trend, historical, over time, last year, monthly, etc.)
4892
+ - isComparison: true if user compares, ranks, or contrasts entities or categories.
4893
+ - language: detect from the query text itself; default "en" if uncertain.`;
4894
+ const rawResponse = await llm.chat(
4895
+ [{ role: "user", content: `QUERY: ${query}` }],
4896
+ "",
4897
+ { systemPrompt, temperature: 0 }
4898
+ );
4899
+ const parsed = this.parseIntentResponse(rawResponse);
4900
+ if (!parsed) {
4901
+ throw new Error(`Could not parse intent JSON from LLM response: ${rawResponse}`);
4902
+ }
4903
+ return parsed;
4904
+ }
4905
+ /**
4906
+ * Parse and validate the raw LLM response into a `QueryIntent`.
4907
+ */
4908
+ static parseIntentResponse(raw) {
4909
+ const jsonStr = this.extractJsonCandidate(raw);
4910
+ if (!jsonStr) return null;
4911
+ try {
4912
+ const obj = JSON.parse(jsonStr);
4913
+ const validHints = [
4914
+ "trend",
4915
+ "comparison",
4916
+ "distribution",
4917
+ "composition",
4918
+ "correlation",
4919
+ "ranking",
4920
+ "kpi",
4921
+ "tabular",
4922
+ "geographic",
4923
+ "category_breakdown",
4924
+ "product_browse",
4925
+ "table",
4926
+ "text"
4927
+ ];
4928
+ const validCharts = [
4929
+ "line_chart",
4930
+ "bar_chart",
4931
+ "histogram",
4932
+ "pie_chart",
4933
+ "donut_chart",
4934
+ "scatter_plot",
4935
+ "horizontal_bar",
4936
+ "metric_card",
4937
+ "table",
4938
+ "geo_map",
4939
+ "text"
4940
+ ];
4941
+ const hint = obj.visualizationHint;
4942
+ if (!hint || !validHints.includes(hint)) return null;
4943
+ const normalizedHint = hint === "category_breakdown" ? "composition" : hint;
4944
+ const recommendedChart = typeof obj.recommendedChart === "string" && validCharts.includes(obj.recommendedChart) ? obj.recommendedChart : this.getRecommendedChartForIntent(normalizedHint);
4945
+ return {
4946
+ visualizationHint: normalizedHint,
4947
+ recommendedChart,
4948
+ filterInStockOnly: Boolean(obj.filterInStockOnly),
4949
+ wantsExplicitTable: Boolean(obj.wantsExplicitTable),
4950
+ isTemporal: Boolean(obj.isTemporal),
4951
+ isComparison: Boolean(obj.isComparison),
4952
+ language: typeof obj.language === "string" && obj.language ? obj.language : "en",
4953
+ reasoning: typeof obj.reasoning === "string" ? obj.reasoning : void 0
4954
+ };
4955
+ } catch (e) {
4956
+ return null;
4957
+ }
4958
+ }
4959
+ /**
4960
+ * Heuristic intent detector — used when the LLM is unavailable.
4961
+ * Intentionally minimal: it should only catch the most obvious signals.
4962
+ * The LLM path handles everything subtle.
4963
+ */
4964
+ static detectIntentHeuristic(query) {
4965
+ const q = query.toLowerCase();
4966
+ const isTemporal = /\b(trend|trends|over time|historical|history|growth|decline|monthly|yearly|weekly|daily|last (year|month|week)|timeline|forecast)\b/.test(q);
4967
+ const isRanking = /\b(highest|lowest|top\s*\d+|bottom\s*\d+|rank(?:ing)?|best|worst|leading)\b/.test(q);
4968
+ const isComparison = /\b(compare|comparison|vs\.?|versus|against|differ|difference|contrast|top)\b/.test(q) || isRanking;
4969
+ const isDistribution = /\b(distribution|spread|histogram|frequency|variance|range)\b/.test(q);
4970
+ const wantsPieLikeChart = /\b(pie|donut|doughnut)(?:\s+chart)?\b/.test(q);
4971
+ const isComposition = wantsPieLikeChart || /\b(share|percentage|percent|breakup|breakdown|composition|split|segmentation|by category|by type|proportion)\b/.test(q);
4972
+ const isCorrelation = /\b(relation|relationship|correlation|correlate|scatter|association|impact of|depend(?:s|ence)? on)\b/.test(q);
4973
+ const isKpi = /\b(total|average|avg|count|sum|median|minimum|maximum|metric|kpi|how many|number of)\b/.test(q);
4974
+ const isGeographic = /\b(region|country|countries|state|city|location|map|geo|geographic|territory)\b/.test(q);
4975
+ const filterInStockOnly = /\b(in[- ]?stock|available|availability|inventory|stock status)\b/.test(q);
4976
+ const wantsExplicitTable = /\b(table|spreadsheet|grid|detailed records|record details|list all|compare all)\b/.test(q);
4977
+ let visualizationHint = "text";
4978
+ if (wantsExplicitTable) visualizationHint = "tabular";
4979
+ else if (isTemporal) visualizationHint = "trend";
4980
+ else if (wantsPieLikeChart) visualizationHint = "composition";
4981
+ else if (isRanking) visualizationHint = "ranking";
4982
+ else if (isComparison) visualizationHint = "comparison";
4983
+ else if (isDistribution) visualizationHint = "distribution";
4984
+ else if (isComposition) visualizationHint = "composition";
4985
+ else if (isCorrelation) visualizationHint = "correlation";
4986
+ else if (isGeographic) visualizationHint = "geographic";
4987
+ else if (isKpi) visualizationHint = "kpi";
4988
+ else if (this.isProductQuery(query)) visualizationHint = "product_browse";
4989
+ return {
4990
+ visualizationHint,
4991
+ recommendedChart: this.getRecommendedChartForIntent(visualizationHint),
4992
+ filterInStockOnly,
4993
+ wantsExplicitTable,
4994
+ isTemporal,
4995
+ isComparison,
4996
+ language: "en"
4997
+ // heuristic cannot reliably detect language
4998
+ };
4999
+ }
5000
+ static getRecommendedChartForIntent(intent) {
5001
+ switch (intent) {
5002
+ case "trend":
5003
+ return "line_chart";
5004
+ case "comparison":
5005
+ return "bar_chart";
5006
+ case "distribution":
5007
+ return "histogram";
5008
+ case "composition":
5009
+ case "category_breakdown":
5010
+ return "pie_chart";
5011
+ case "correlation":
5012
+ return "scatter_plot";
5013
+ case "ranking":
5014
+ return "horizontal_bar";
5015
+ case "kpi":
5016
+ return "metric_card";
5017
+ case "tabular":
5018
+ case "table":
5019
+ return "table";
5020
+ case "geographic":
5021
+ return "geo_map";
5022
+ case "product_browse":
5023
+ case "text":
5024
+ default:
5025
+ return "text";
5026
+ }
5027
+ }
5028
+ // ─── Transform Helpers ────────────────────────────────────────────────────
5029
+ static transformToProductCarousel(data, config, trainedSchema) {
5030
+ const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
5031
+ return {
5032
+ type: "product_carousel",
5033
+ title: "Recommended Products",
5034
+ description: `Found ${products.length} relevant products`,
5035
+ data: products
5036
+ };
5037
+ }
5038
+ static transformToPieChart(data, profile, query = "") {
5039
+ var _a;
5040
+ const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
5041
+ const categories = dimension ? Array.from(new Set(profile.records.map((record) => {
5042
+ var _a2;
5043
+ return String((_a2 = record.fields[dimension.key]) != null ? _a2 : "");
5044
+ }).filter(Boolean))) : this.detectCategories(data);
5045
+ if (categories.length === 0) return null;
5046
+ const categoryData = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key) : this.aggregateByCategory(data, categories);
5047
+ const pieData = Object.entries(categoryData).map(([label, count]) => {
5048
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
5049
+ return { label, value: count, inStockCount, outOfStockCount };
5050
+ });
4293
5051
  return {
4294
5052
  type: "pie_chart",
4295
- title: "Distribution by Category",
4296
- description: `Showing breakdown across ${categories.length} categories`,
5053
+ title: `Distribution by ${(_a = dimension == null ? void 0 : dimension.label) != null ? _a : "Category"}`,
5054
+ description: `Showing breakdown across ${pieData.length} categories`,
4297
5055
  data: pieData
4298
5056
  };
4299
5057
  }
4300
- /**
4301
- * Transform data to line chart format
4302
- */
4303
- static transformToLineChart(data) {
4304
- const timePoints = this.extractTimeSeriesData(data);
4305
- const lineData = timePoints.map((point) => ({
4306
- timestamp: point.timestamp,
4307
- value: point.value,
4308
- label: point.label
4309
- }));
5058
+ static transformToLineChart(profile) {
5059
+ const dateField = profile.dateFields[0];
5060
+ const valueField = profile.numericFields[0];
5061
+ const buckets = /* @__PURE__ */ new Map();
5062
+ profile.records.forEach((record) => {
5063
+ var _a, _b, _c;
5064
+ const timestamp = String((_a = record.fields[dateField.key]) != null ? _a : "");
5065
+ const value = (_b = this.toFiniteNumber(record.fields[valueField.key])) != null ? _b : 0;
5066
+ buckets.set(timestamp, ((_c = buckets.get(timestamp)) != null ? _c : 0) + value);
5067
+ });
5068
+ 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 }));
4310
5069
  return {
4311
5070
  type: "line_chart",
4312
- title: "Trend Over Time",
5071
+ title: `${valueField.label} Over Time`,
4313
5072
  description: `Showing ${lineData.length} data points`,
4314
5073
  data: lineData
4315
5074
  };
4316
5075
  }
4317
- /**
4318
- * Transform data to table format
4319
- */
4320
- static transformToTable(data) {
4321
- const columns = this.extractTableColumns(data);
4322
- const rows = data.map((item) => this.extractTableRow(item, columns));
4323
- const tableData = {
4324
- columns,
4325
- rows
5076
+ static transformToBarChart(data, profile, query = "", horizontal = false) {
5077
+ var _a;
5078
+ const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
5079
+ const measure = profile ? this.selectNumericField(profile, query) : void 0;
5080
+ const aggregate = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key, measure == null ? void 0 : measure.key) : this.aggregateByCategory(data, this.detectCategories(data));
5081
+ const barData = Object.entries(aggregate).map(([category, value]) => ({ category, value: Number(value) })).sort((a, b) => horizontal ? b.value - a.value : 0).slice(0, 12);
5082
+ const fallbackData = barData.length > 0 ? barData : data.slice(0, 12).map((item, index) => {
5083
+ var _a2, _b, _c, _d, _e;
5084
+ const meta = item.metadata || {};
5085
+ const label = String(
5086
+ (_c = (_b = (_a2 = this.getDynamicVal(meta, "name")) != null ? _a2 : meta.label) != null ? _b : meta.title) != null ? _c : `Result ${index + 1}`
5087
+ );
5088
+ const value = (_e = (_d = this.extractNumericValue(meta)) != null ? _d : item.score) != null ? _e : 0;
5089
+ return { category: label, value: Number(value) };
5090
+ });
5091
+ return {
5092
+ type: horizontal ? "horizontal_bar" : "bar_chart",
5093
+ title: dimension ? `${(_a = measure == null ? void 0 : measure.label) != null ? _a : "Count"} by ${dimension.label}` : "Comparison",
5094
+ description: `Showing ${fallbackData.length} comparable values`,
5095
+ data: fallbackData
4326
5096
  };
5097
+ }
5098
+ static transformToHistogram(profile, query = "") {
5099
+ const field = this.selectNumericField(profile, query);
5100
+ if (!field) return null;
5101
+ const values = profile.records.map((record) => this.toFiniteNumber(record.fields[field.key])).filter((value) => value !== null).sort((a, b) => a - b);
5102
+ if (values.length === 0) return null;
5103
+ const min = values[0];
5104
+ const max = values[values.length - 1];
5105
+ const bucketCount = Math.min(10, Math.max(3, Math.ceil(Math.sqrt(values.length))));
5106
+ const width = max === min ? 1 : (max - min) / bucketCount;
5107
+ const buckets = Array.from({ length: bucketCount }, (_, index) => {
5108
+ const start = min + index * width;
5109
+ const end = index === bucketCount - 1 ? max : start + width;
5110
+ return { category: `${this.formatNumber(start)}-${this.formatNumber(end)}`, value: 0 };
5111
+ });
5112
+ values.forEach((value) => {
5113
+ const bucketIndex = max === min ? 0 : Math.min(bucketCount - 1, Math.floor((value - min) / width));
5114
+ buckets[bucketIndex].value += 1;
5115
+ });
5116
+ return {
5117
+ type: "histogram",
5118
+ title: `${field.label} Distribution`,
5119
+ description: `Showing ${values.length} values across ${bucketCount} buckets`,
5120
+ data: buckets
5121
+ };
5122
+ }
5123
+ static transformToScatterPlot(profile, query = "") {
5124
+ const fields = this.rankFieldsByQuery(profile.numericFields, query).slice(0, 2);
5125
+ if (fields.length < 2) return null;
5126
+ const [xField, yField] = fields;
5127
+ const points = profile.records.map((record) => {
5128
+ var _a;
5129
+ const x = this.toFiniteNumber(record.fields[xField.key]);
5130
+ const y = this.toFiniteNumber(record.fields[yField.key]);
5131
+ if (x === null || y === null) return null;
5132
+ return { x, y, label: String((_a = this.getRecordLabel(record)) != null ? _a : record.id) };
5133
+ }).filter((point) => point !== null).slice(0, 100);
5134
+ if (points.length === 0) return null;
5135
+ return {
5136
+ type: "scatter_plot",
5137
+ title: `${xField.label} vs ${yField.label}`,
5138
+ description: `Showing ${points.length} paired values`,
5139
+ data: points
5140
+ };
5141
+ }
5142
+ static transformToMetricCard(profile, query = "") {
5143
+ const operation = this.detectAggregationOperation(query);
5144
+ const numericField = this.selectNumericField(profile, query);
5145
+ const values = numericField ? profile.records.map((record) => this.toFiniteNumber(record.fields[numericField.key])).filter((value2) => value2 !== null) : [];
5146
+ const value = operation === "count" || values.length === 0 ? profile.records.length : this.calculateAggregate(values, operation);
5147
+ const metric = {
5148
+ label: numericField ? `${operation} ${numericField.label}` : "Count",
5149
+ value,
5150
+ operation
5151
+ };
5152
+ return {
5153
+ type: "metric_card",
5154
+ title: metric.label,
5155
+ description: `Calculated from ${profile.records.length} result${profile.records.length === 1 ? "" : "s"}`,
5156
+ data: metric
5157
+ };
5158
+ }
5159
+ static transformToRadarChart(data) {
5160
+ const attributeMap = {};
5161
+ data.forEach((item) => {
5162
+ var _a, _b, _c;
5163
+ const meta = item.metadata || {};
5164
+ const seriesName = String((_c = (_b = (_a = meta.name) != null ? _a : meta.product) != null ? _b : item.id) != null ? _c : "Item");
5165
+ Object.entries(meta).forEach(([key, val]) => {
5166
+ if (typeof val === "number" && !["price", "id"].includes(key.toLowerCase())) {
5167
+ if (!attributeMap[key]) attributeMap[key] = {};
5168
+ attributeMap[key][seriesName] = val;
5169
+ }
5170
+ });
5171
+ });
5172
+ const radarData = Object.entries(attributeMap).map(([attribute, series]) => __spreadValues({
5173
+ attribute
5174
+ }, series));
5175
+ return {
5176
+ type: "radar_chart",
5177
+ title: "Product Comparison",
5178
+ description: `Comparing ${data.length} items across ${radarData.length} attributes`,
5179
+ data: radarData.length > 0 ? radarData : data.map((d) => ({ attribute: d.content.substring(0, 40) }))
5180
+ };
5181
+ }
5182
+ static transformToTable(data, query = "") {
5183
+ const columns = this.extractTableColumns(data, query);
5184
+ const rows = data.map((item) => this.extractTableRow(item, columns));
5185
+ const tableData = { columns, rows };
4327
5186
  return {
4328
5187
  type: "table",
4329
5188
  title: "Detailed Results",
@@ -4331,28 +5190,17 @@ var UITransformer = class {
4331
5190
  data: tableData
4332
5191
  };
4333
5192
  }
4334
- /**
4335
- * Transform data to text format (fallback)
4336
- */
4337
5193
  static transformToText(data) {
4338
- const textContent = data.map((item) => item.content).join("\n\n");
4339
5194
  return this.createTextResponse(
4340
5195
  "Search Results",
4341
- textContent,
5196
+ data.map((item) => item.content).join("\n\n"),
4342
5197
  `Found ${data.length} relevant results`
4343
5198
  );
4344
5199
  }
4345
- /**
4346
- * Helper: Create text response
4347
- */
4348
5200
  static createTextResponse(title, content, description) {
4349
- return {
4350
- type: "text",
4351
- title,
4352
- description,
4353
- data: { content }
4354
- };
5201
+ return { type: "text", title, description, data: { content } };
4355
5202
  }
5203
+ // ─── LLM Response Parsing ─────────────────────────────────────────────────
4356
5204
  static parseTransformationResponse(raw) {
4357
5205
  const payloadText = this.extractJsonCandidate(raw);
4358
5206
  if (!payloadText) return null;
@@ -4389,9 +5237,7 @@ var UITransformer = class {
4389
5237
  if (char === "{") depth += 1;
4390
5238
  if (char === "}") {
4391
5239
  depth -= 1;
4392
- if (depth === 0) {
4393
- return cleaned.slice(start, i + 1);
4394
- }
5240
+ if (depth === 0) return cleaned.slice(start, i + 1);
4395
5241
  }
4396
5242
  }
4397
5243
  return null;
@@ -4399,19 +5245,16 @@ var UITransformer = class {
4399
5245
  static normalizeTransformation(payload) {
4400
5246
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
4401
5247
  if (!payload || typeof payload !== "object") return null;
4402
- const payloadObj = payload;
4403
- const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
5248
+ const p = payload;
5249
+ const type = this.normalizeVisualizationType(
5250
+ String((_c = (_b = (_a = p.type) != null ? _a : p.view) != null ? _b : p.chartType) != null ? _c : "")
5251
+ );
4404
5252
  if (!type) return null;
4405
- const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
4406
- const description = payloadObj.description ? String(payloadObj.description) : void 0;
4407
- 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;
5253
+ const title = String((_e = (_d = p.title) != null ? _d : p.heading) != null ? _e : "Visualization");
5254
+ const description = p.description ? String(p.description) : void 0;
5255
+ 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;
4408
5256
  const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
4409
- const transformation = {
4410
- type,
4411
- title,
4412
- description,
4413
- data
4414
- };
5257
+ const transformation = { type, title, description, data };
4415
5258
  return this.validateTransformation(transformation) ? transformation : null;
4416
5259
  }
4417
5260
  static normalizeVisualizationType(type) {
@@ -4421,10 +5264,23 @@ var UITransformer = class {
4421
5264
  pie_chart: "pie_chart",
4422
5265
  bar: "bar_chart",
4423
5266
  bar_chart: "bar_chart",
5267
+ histogram: "histogram",
5268
+ horizontal_bar: "horizontal_bar",
5269
+ horizontalbar: "horizontal_bar",
4424
5270
  line: "line_chart",
4425
5271
  line_chart: "line_chart",
5272
+ scatter: "scatter_plot",
5273
+ scatter_plot: "scatter_plot",
5274
+ scatterplot: "scatter_plot",
4426
5275
  radar: "radar_chart",
4427
5276
  radar_chart: "radar_chart",
5277
+ metric: "metric_card",
5278
+ metric_card: "metric_card",
5279
+ card: "metric_card",
5280
+ kpi: "metric_card",
5281
+ geo: "geo_map",
5282
+ geo_map: "geo_map",
5283
+ map: "geo_map",
4428
5284
  table: "table",
4429
5285
  text: "text",
4430
5286
  product_carousel: "product_carousel",
@@ -4432,21 +5288,31 @@ var UITransformer = class {
4432
5288
  };
4433
5289
  return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
4434
5290
  }
4435
- static validateTransformation(transformation) {
4436
- const { type, data } = transformation;
5291
+ static validateTransformation(t) {
5292
+ const { type, data } = t;
4437
5293
  switch (type) {
4438
5294
  case "pie_chart":
4439
5295
  case "bar_chart":
5296
+ case "histogram":
5297
+ case "horizontal_bar":
4440
5298
  return Array.isArray(data) && data.every(
4441
- (item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
5299
+ (i) => i !== null && typeof i === "object" && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
5300
+ );
5301
+ case "scatter_plot":
5302
+ return Array.isArray(data) && data.every(
5303
+ (i) => i !== null && typeof i === "object" && (typeof i.x === "number" || !Number.isNaN(Number(i.x))) && (typeof i.y === "number" || !Number.isNaN(Number(i.y)))
4442
5304
  );
4443
5305
  case "line_chart":
4444
5306
  return Array.isArray(data) && data.every(
4445
- (item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
5307
+ (i) => i !== null && typeof i === "object" && "timestamp" in i && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
4446
5308
  );
5309
+ case "metric_card":
5310
+ return typeof data === "object" && data !== null && (typeof data.value === "number" || !Number.isNaN(Number(data.value)));
5311
+ case "geo_map":
5312
+ return Array.isArray(data) || typeof data === "object" && data !== null;
4447
5313
  case "radar_chart":
4448
5314
  return Array.isArray(data) && data.every(
4449
- (item) => item !== null && typeof item === "object" && "attribute" in item
5315
+ (i) => i !== null && typeof i === "object" && "attribute" in i
4450
5316
  );
4451
5317
  case "table":
4452
5318
  return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
@@ -4455,15 +5321,27 @@ var UITransformer = class {
4455
5321
  case "product_carousel":
4456
5322
  case "carousel":
4457
5323
  return Array.isArray(data) && data.every(
4458
- (item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
5324
+ (i) => i !== null && typeof i === "object" && ("id" in i || "name" in i)
4459
5325
  );
4460
5326
  default:
4461
5327
  return false;
4462
5328
  }
4463
5329
  }
4464
- /**
4465
- * Helper: Check if data item is product-related
4466
- */
5330
+ // ─── Data Inspection Helpers ──────────────────────────────────────────────
5331
+ static isStructuredListQuery(query) {
5332
+ const q = query.toLowerCase();
5333
+ const asksForRecords = /\b(list|provide|show|give|get|find|which|whose|records?|details?)\b/.test(q);
5334
+ 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);
5335
+ const hasEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
5336
+ return asksForRecords && (hasNumericPredicate || hasEntityTerms);
5337
+ }
5338
+ static isProductQuery(query) {
5339
+ const q = query.toLowerCase();
5340
+ 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);
5341
+ const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list)\b/.test(q);
5342
+ const nonProductEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
5343
+ return productTerms && productAction && !nonProductEntityTerms;
5344
+ }
4467
5345
  static isProductData(item) {
4468
5346
  const content = (item.content || "").toLowerCase();
4469
5347
  const productKeywords = [
@@ -4489,360 +5367,582 @@ var UITransformer = class {
4489
5367
  "fragrance"
4490
5368
  ];
4491
5369
  const hasKeywords = productKeywords.some((kw) => content.includes(kw));
4492
- const hasMetadataKey = Object.keys(item.metadata || {}).some(
4493
- (k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
4494
- );
4495
- const hasPricePattern = /\$\s*\d+/.test(content);
5370
+ const metadata = item.metadata || {};
5371
+ const hasMetadataKey = Object.keys(metadata).some((k) => {
5372
+ const val = metadata[k];
5373
+ return ["price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase()) && val !== null && val !== void 0 && val !== "";
5374
+ });
5375
+ const hasPricePattern = /\$\s*\d+\.\d{2}/.test(content);
4496
5376
  return hasKeywords || hasMetadataKey || hasPricePattern;
4497
5377
  }
4498
- /**
4499
- * Helper: Check if data contains time series
4500
- */
4501
5378
  static isTimeSeriesData(item) {
4502
- const content = (item.content || "").toLowerCase();
4503
- const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
4504
- const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
4505
5379
  const metadata = item.metadata || {};
4506
5380
  const maybeDateKeys = Object.keys(metadata).filter(
4507
5381
  (k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
4508
5382
  );
4509
- const hasValidDateValue = maybeDateKeys.some((key) => {
5383
+ return maybeDateKeys.some((key) => {
4510
5384
  const value = metadata[key];
4511
- if (typeof value === "string") {
4512
- return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
4513
- }
5385
+ if (typeof value === "string") return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
4514
5386
  return typeof value === "number";
4515
5387
  });
4516
- return hasTimeKeyword || hasValidDateValue;
4517
5388
  }
4518
- static shouldShowCategoryChart(query, categories) {
4519
- if (categories.length < 2) {
4520
- return false;
4521
- }
4522
- const normalized = query.toLowerCase();
4523
- const chartKeywords = [
4524
- "distribution",
4525
- "breakdown",
4526
- "by category",
4527
- "by type",
4528
- "compare",
4529
- "share",
4530
- "percentage",
4531
- "segmentation",
4532
- "split",
4533
- "category breakdown",
4534
- "category distribution"
4535
- ];
4536
- return chartKeywords.some((keyword) => normalized.includes(keyword));
5389
+ static hasMultipleFields(data) {
5390
+ const fieldCount = /* @__PURE__ */ new Set();
5391
+ data.forEach((item) => Object.keys(item.metadata || {}).forEach((k) => fieldCount.add(k)));
5392
+ return fieldCount.size > 2;
4537
5393
  }
4538
- static isTrendQuery(query) {
4539
- const normalized = query.toLowerCase();
4540
- const trendKeywords = [
4541
- "trend",
4542
- "over time",
4543
- "historical",
4544
- "growth",
4545
- "decline",
4546
- "increase",
4547
- "decrease",
4548
- "year",
4549
- "month",
4550
- "week",
4551
- "day",
4552
- "comparison",
4553
- "compare",
4554
- "changes",
4555
- "timeline"
4556
- ];
4557
- return trendKeywords.some((keyword) => normalized.includes(keyword));
5394
+ static profileData(data) {
5395
+ const records = data.map((item) => {
5396
+ const fields2 = {};
5397
+ Object.entries(item.metadata || {}).forEach(([key, value]) => {
5398
+ const primitive = this.toPrimitive(value);
5399
+ if (primitive !== null) fields2[key] = primitive;
5400
+ });
5401
+ if (!fields2.content && item.content) fields2.content = item.content.substring(0, 500);
5402
+ return {
5403
+ id: item.id,
5404
+ content: item.content,
5405
+ score: item.score,
5406
+ fields: fields2,
5407
+ source: item
5408
+ };
5409
+ });
5410
+ const keys = Array.from(new Set(records.flatMap((record) => Object.keys(record.fields))));
5411
+ const fields = keys.map((key) => {
5412
+ const values = records.map((record) => record.fields[key]).filter((value) => value !== void 0 && value !== null && String(value).trim() !== "");
5413
+ const uniqueCount = new Set(values.map((value) => String(value).toLowerCase())).size;
5414
+ return {
5415
+ key,
5416
+ label: this.humanizeFieldName(key),
5417
+ kind: this.inferFieldKind(key, values, records.length, uniqueCount),
5418
+ values,
5419
+ uniqueCount
5420
+ };
5421
+ });
5422
+ return {
5423
+ records,
5424
+ fields,
5425
+ numericFields: fields.filter((field) => field.kind === "number"),
5426
+ dateFields: fields.filter((field) => field.kind === "date"),
5427
+ categoricalFields: fields.filter((field) => field.kind === "category"),
5428
+ booleanFields: fields.filter((field) => field.kind === "boolean")
5429
+ };
4558
5430
  }
4559
- static isStockQuery(query) {
4560
- const normalized = query.toLowerCase();
4561
- return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
5431
+ static toPrimitive(value) {
5432
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
5433
+ if (value === null || value === void 0) return null;
5434
+ if (value instanceof Date) return value.toISOString();
5435
+ return null;
4562
5436
  }
4563
- /**
4564
- * Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
4565
- */
4566
- static getDynamicVal(meta, uiKey, config, trainedSchema) {
4567
- var _a;
4568
- if (!meta) return void 0;
4569
- const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
4570
- if (mapping && mapping[uiKey]) {
4571
- const mappedKey = mapping[uiKey];
4572
- if (meta[mappedKey] !== void 0) return meta[mappedKey];
5437
+ static inferFieldKind(key, values, rowCount, uniqueCount) {
5438
+ const lowerKey = key.toLowerCase();
5439
+ if (values.length === 0) return "text";
5440
+ if (values.every((value) => typeof value === "boolean" || /^(true|false|yes|no|in_stock|out_of_stock|available|unavailable)$/i.test(String(value)))) {
5441
+ return "boolean";
4573
5442
  }
4574
- if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
4575
- const trainedKey = trainedSchema[uiKey];
4576
- if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
5443
+ if (/(date|time|timestamp|created|updated|month|year|period)/i.test(lowerKey) && values.some((value) => this.isDateLike(value))) {
5444
+ return "date";
4577
5445
  }
4578
- return resolveMetadataValue(meta, uiKey);
5446
+ const numericCount = values.filter((value) => this.toFiniteNumber(value) !== null).length;
5447
+ if (numericCount / values.length >= 0.85 && !/(id|sku|ean|upc|zip|postal|phone|code)/i.test(lowerKey)) {
5448
+ return "number";
5449
+ }
5450
+ if (uniqueCount <= Math.max(20, Math.ceil(rowCount * 0.7)) && !/(id|sku|ean|upc|description|content|url|image|thumbnail)/i.test(lowerKey)) {
5451
+ return "category";
5452
+ }
5453
+ return "text";
4579
5454
  }
4580
- static extractProductInfo(item, config, trainedSchema) {
4581
- const meta = item.metadata || {};
4582
- const name = this.getDynamicVal(meta, "name", config, trainedSchema);
4583
- const price = this.getDynamicVal(meta, "price", config, trainedSchema);
4584
- const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
4585
- if (name || this.isProductData(item)) {
4586
- let finalName = name ? String(name) : void 0;
4587
- if (!finalName) {
4588
- const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
4589
- finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
4590
- }
4591
- let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
4592
- if (!finalPrice) {
4593
- const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
4594
- if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
4595
- }
4596
- const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
4597
- return {
4598
- id: item.id,
4599
- name: finalName,
4600
- price: finalPrice,
4601
- image: typeof imageValue === "string" ? imageValue : void 0,
4602
- brand: brand ? String(brand) : void 0,
4603
- description: item.content,
4604
- inStock: this.determineStockStatus(item)
4605
- };
5455
+ static isDateLike(value) {
5456
+ if (typeof value === "number") return value > 1900 && value < 3e3;
5457
+ const text = String(value).trim();
5458
+ return text.length > 3 && !Number.isNaN(Date.parse(text));
5459
+ }
5460
+ static chooseAutomaticVisualization(data, profile, query) {
5461
+ if (profile.records.length === 0) return null;
5462
+ if (profile.dateFields.length > 0 && profile.numericFields.length > 0) {
5463
+ return this.transformToLineChart(profile);
5464
+ }
5465
+ if (profile.categoricalFields.length > 0 && profile.numericFields.length > 0) {
5466
+ return this.transformToBarChart(data, profile, query);
5467
+ }
5468
+ if (profile.categoricalFields.length > 0) {
5469
+ return this.transformToPieChart(data, profile, query);
5470
+ }
5471
+ if (profile.numericFields.length >= 2) {
5472
+ return this.transformToScatterPlot(profile, query);
5473
+ }
5474
+ if (profile.numericFields.length === 1) {
5475
+ return this.transformToMetricCard(profile, query);
4606
5476
  }
4607
5477
  return null;
4608
5478
  }
4609
- /**
4610
- * Helper: Detect categories in data
4611
- */
5479
+ static selectDimensionField(profile, query) {
5480
+ var _a, _b;
5481
+ const productCategory = profile.categoricalFields.find(
5482
+ (field) => /category|department|collection|type|group|segment|region|country|state|city/i.test(field.key)
5483
+ );
5484
+ const ranked = this.rankFieldsByQuery(profile.categoricalFields, query);
5485
+ return (_b = (_a = ranked[0]) != null ? _a : productCategory) != null ? _b : profile.categoricalFields[0];
5486
+ }
5487
+ static selectNumericField(profile, query) {
5488
+ var _a;
5489
+ return (_a = this.rankFieldsByQuery(profile.numericFields, query)[0]) != null ? _a : profile.numericFields[0];
5490
+ }
5491
+ static rankFieldsByQuery(fields, query) {
5492
+ const q = query.toLowerCase();
5493
+ return [...fields].sort((a, b) => this.fieldScore(b, q) - this.fieldScore(a, q));
5494
+ }
5495
+ static fieldScore(field, query) {
5496
+ const key = field.key.toLowerCase();
5497
+ const label = field.label.toLowerCase();
5498
+ let score = 0;
5499
+ if (query.includes(key)) score += 4;
5500
+ if (query.includes(label)) score += 4;
5501
+ key.split(/[_\s-]+/).forEach((part) => {
5502
+ if (part && query.includes(part)) score += 1;
5503
+ });
5504
+ if (/category|department|collection|type|group|segment/.test(key)) score += 2;
5505
+ if (/count|total|quantity|stock|inventory|sales|revenue|amount|price|value|score/.test(key)) score += 2;
5506
+ if (/id|sku|ean|upc|code/.test(key)) score -= 5;
5507
+ return score;
5508
+ }
5509
+ static normalizeComparableField(field) {
5510
+ return field.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "").trim();
5511
+ }
5512
+ static fieldTokens(field) {
5513
+ return field.toLowerCase().replace(/[_-]+/g, " ").split(/\s+/).map((token) => token.replace(/[^a-z0-9]/g, "")).filter(Boolean);
5514
+ }
5515
+ static aggregateProfileByDimension(profile, dimensionKey, measureKey) {
5516
+ const result = {};
5517
+ profile.records.forEach((record) => {
5518
+ var _a, _b, _c;
5519
+ const category = String((_a = record.fields[dimensionKey]) != null ? _a : "Other").trim() || "Other";
5520
+ const value = measureKey ? (_b = this.toFiniteNumber(record.fields[measureKey])) != null ? _b : 0 : 1;
5521
+ result[category] = ((_c = result[category]) != null ? _c : 0) + value;
5522
+ });
5523
+ return result;
5524
+ }
5525
+ static getRecordLabel(record) {
5526
+ const labelKeys = ["name", "title", "label", "product", "item", "brand"];
5527
+ const key = Object.keys(record.fields).find(
5528
+ (fieldKey) => labelKeys.some((labelKey) => fieldKey.toLowerCase().includes(labelKey))
5529
+ );
5530
+ return key ? record.fields[key] : void 0;
5531
+ }
5532
+ static detectAggregationOperation(query) {
5533
+ const q = query.toLowerCase();
5534
+ if (/\b(avg|average|mean)\b/.test(q)) return "average";
5535
+ if (/\b(count|how many|number of)\b/.test(q)) return "count";
5536
+ if (/\b(min|minimum|lowest)\b/.test(q)) return "min";
5537
+ if (/\b(max|maximum|highest)\b/.test(q)) return "max";
5538
+ if (/\bmedian\b/.test(q)) return "median";
5539
+ return "sum";
5540
+ }
5541
+ static calculateAggregate(values, operation) {
5542
+ if (values.length === 0) return 0;
5543
+ switch (operation) {
5544
+ case "average":
5545
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
5546
+ case "count":
5547
+ return values.length;
5548
+ case "min":
5549
+ return Math.min(...values);
5550
+ case "max":
5551
+ return Math.max(...values);
5552
+ case "median": {
5553
+ const sorted = [...values].sort((a, b) => a - b);
5554
+ const middle = Math.floor(sorted.length / 2);
5555
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
5556
+ }
5557
+ case "sum":
5558
+ default:
5559
+ return values.reduce((sum, value) => sum + value, 0);
5560
+ }
5561
+ }
5562
+ static humanizeFieldName(key) {
5563
+ return key.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
5564
+ }
5565
+ static formatNumber(value) {
5566
+ return Number.isInteger(value) ? String(value) : value.toFixed(1);
5567
+ }
4612
5568
  static detectCategories(data) {
4613
5569
  const categories = /* @__PURE__ */ new Set();
4614
5570
  data.forEach((item) => {
4615
- const meta = item.metadata || {};
4616
- if (meta.category) {
4617
- categories.add(String(meta.category));
4618
- }
4619
- if (meta.type) {
4620
- categories.add(String(meta.type));
4621
- }
4622
- if (meta.tag) {
4623
- const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
4624
- tags.forEach((t) => categories.add(String(t)));
4625
- }
4626
- const contentCategories = Array.from(new Set(
4627
- Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
4628
- ));
4629
- contentCategories.forEach((category) => categories.add(category));
4630
- const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
4631
- if (categoryMatch) {
4632
- categories.add(categoryMatch[1].trim());
4633
- }
5571
+ const category = this.getProductCategory(item);
5572
+ if (category) categories.add(category);
4634
5573
  });
4635
5574
  return Array.from(categories);
4636
5575
  }
4637
- /**
4638
- * Helper: Aggregate data by category
4639
- */
5576
+ static getProductCategory(item) {
5577
+ const meta = item.metadata || {};
5578
+ const metadataCategory = resolveMetadataValue(meta, "category");
5579
+ if (this.isUsableCategory(metadataCategory)) return String(metadataCategory).trim();
5580
+ const content = item.content || "";
5581
+ const explicitCategoryMatch = content.match(
5582
+ /(?:^|\n)\s*(?:product\s*)?(?:category|department|collection)\s*[:\-–]\s*([^\n]+)/i
5583
+ );
5584
+ if (explicitCategoryMatch && this.isUsableCategory(explicitCategoryMatch[1])) {
5585
+ return explicitCategoryMatch[1].trim();
5586
+ }
5587
+ return null;
5588
+ }
5589
+ static isUsableCategory(value) {
5590
+ if (value === null || value === void 0) return false;
5591
+ const category = String(value).trim();
5592
+ if (!category) return false;
5593
+ 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);
5594
+ }
4640
5595
  static aggregateByCategory(data, categories) {
4641
- const result = {};
4642
- categories.forEach((cat) => {
4643
- result[cat] = 0;
4644
- });
5596
+ const result = Object.fromEntries(categories.map((c) => [c, 0]));
4645
5597
  data.forEach((item) => {
4646
- const meta = item.metadata || {};
4647
- const itemCategory = meta.category || meta.type || "Other";
4648
- if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
4649
- result[itemCategory]++;
4650
- } else {
4651
- result["Other"] = (result["Other"] || 0) + 1;
4652
- }
5598
+ var _a;
5599
+ const cat = (_a = this.getProductCategory(item)) != null ? _a : "Other";
5600
+ if (Object.prototype.hasOwnProperty.call(result, cat)) result[cat]++;
5601
+ else result["Other"] = (result["Other"] || 0) + 1;
4653
5602
  });
4654
5603
  return result;
4655
5604
  }
4656
- /**
4657
- * Helper: Extract time series data
4658
- */
4659
5605
  static extractTimeSeriesData(data) {
4660
5606
  return data.map((item) => {
5607
+ var _a, _b, _c, _d, _e;
4661
5608
  const meta = item.metadata || {};
4662
5609
  return {
4663
- timestamp: meta.timestamp || meta.date || (/* @__PURE__ */ new Date()).toISOString(),
4664
- value: meta.value || item.score || 0,
4665
- label: meta.label || item.content.substring(0, 50)
5610
+ timestamp: (_b = (_a = meta.timestamp) != null ? _a : meta.date) != null ? _b : (/* @__PURE__ */ new Date()).toISOString(),
5611
+ value: (_d = (_c = meta.value) != null ? _c : item.score) != null ? _d : 0,
5612
+ label: (_e = meta.label) != null ? _e : item.content.substring(0, 50)
4666
5613
  };
4667
5614
  });
4668
5615
  }
4669
- /**
4670
- * Helper: Extract table columns
4671
- */
4672
- static extractTableColumns(data) {
4673
- const columnSet = /* @__PURE__ */ new Set();
4674
- columnSet.add("Content");
4675
- data.forEach((item) => {
4676
- Object.keys(item.metadata || {}).forEach((key) => {
4677
- columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
4678
- });
4679
- });
4680
- return Array.from(columnSet);
5616
+ static extractNumericValue(meta) {
5617
+ var _a;
5618
+ const preferredKeys = [
5619
+ "value",
5620
+ "count",
5621
+ "total",
5622
+ "average",
5623
+ "avg",
5624
+ "amount",
5625
+ "sales",
5626
+ "revenue",
5627
+ "score",
5628
+ "quantity",
5629
+ "price"
5630
+ ];
5631
+ for (const key of preferredKeys) {
5632
+ const raw = (_a = resolveMetadataValue(meta, key)) != null ? _a : meta[key];
5633
+ const value = typeof raw === "number" ? raw : Number(String(raw != null ? raw : "").replace(/[$,% ,]/g, ""));
5634
+ if (Number.isFinite(value)) return value;
5635
+ }
5636
+ for (const value of Object.values(meta)) {
5637
+ const numeric = typeof value === "number" ? value : Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
5638
+ if (Number.isFinite(numeric)) return numeric;
5639
+ }
5640
+ return null;
5641
+ }
5642
+ static extractTableColumns(data, query = "") {
5643
+ const q = query.toLowerCase();
5644
+ const availableFields = this.extractAvailableTableFields(data);
5645
+ if (/\b(organization|organisations?|organizations?|company|companies)\b/.test(q) && /\b(employee|employees|staff|headcount|workforce)\b/.test(q)) {
5646
+ const nameField = this.pickTableField(availableFields, [
5647
+ "company name",
5648
+ "organization name",
5649
+ "organisation name",
5650
+ "name",
5651
+ "company",
5652
+ "organization",
5653
+ "organisation"
5654
+ ]);
5655
+ const employeeField = this.pickTableField(availableFields, [
5656
+ "number of employees",
5657
+ "employee count",
5658
+ "employees",
5659
+ "employee_count",
5660
+ "number_employees",
5661
+ "num employees",
5662
+ "staff count",
5663
+ "headcount",
5664
+ "workforce"
5665
+ ]);
5666
+ const columns = [nameField, employeeField].filter((field) => Boolean(field));
5667
+ if (columns.length > 0) return columns;
5668
+ }
5669
+ const requestedFields = availableFields.map((field) => ({
5670
+ field,
5671
+ score: this.tableFieldQueryScore(field, q)
5672
+ })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).map((item) => item.field);
5673
+ if (requestedFields.length > 0) {
5674
+ return requestedFields.slice(0, Math.min(6, requestedFields.length));
5675
+ }
5676
+ const metadataFields = Array.from(new Set(
5677
+ data.flatMap((item) => Object.keys(item.metadata || {}).map((k) => this.humanizeFieldName(k)))
5678
+ ));
5679
+ return metadataFields.length > 0 ? metadataFields : ["Content"];
4681
5680
  }
4682
- /**
4683
- * Helper: Extract table row
4684
- */
4685
5681
  static extractTableRow(item, columns) {
4686
- const meta = item.metadata || {};
4687
- return columns.map((col) => {
4688
- if (col === "Content") {
4689
- return item.content.substring(0, 100);
5682
+ return columns.map((col) => this.resolveTableCellValue(item, col));
5683
+ }
5684
+ static extractAvailableTableFields(data) {
5685
+ const fields = /* @__PURE__ */ new Map();
5686
+ const addField = (field) => {
5687
+ const clean = this.humanizeFieldName(field);
5688
+ if (!clean || /^(content|\[?type\]?)$/i.test(clean)) return;
5689
+ const normalized = this.normalizeComparableField(clean);
5690
+ if (!fields.has(normalized)) fields.set(normalized, clean);
5691
+ };
5692
+ data.forEach((item) => {
5693
+ Object.keys(item.metadata || {}).forEach(addField);
5694
+ for (const match of (item.content || "").matchAll(/(?:^|\n)\s*([A-Za-z][A-Za-z0-9_ /-]{1,50})\s*[:\-–]\s*([^\n]+)/g)) {
5695
+ addField(match[1]);
4690
5696
  }
4691
- const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
4692
- const value = meta[metaKey];
4693
- return value !== void 0 ? String(value) : "";
4694
5697
  });
5698
+ return Array.from(fields.values());
5699
+ }
5700
+ static pickTableField(fields, aliases) {
5701
+ const normalizedAliases = aliases.map((alias) => this.normalizeComparableField(alias));
5702
+ for (const alias of normalizedAliases) {
5703
+ const exact = fields.find((field) => this.normalizeComparableField(field) === alias);
5704
+ if (exact) return exact;
5705
+ }
5706
+ for (const alias of normalizedAliases) {
5707
+ const fuzzy = fields.find((field) => {
5708
+ const normalizedField = this.normalizeComparableField(field);
5709
+ if (/(id|code|uuid)$/.test(normalizedField) && !/(id|code|uuid)$/.test(alias)) return false;
5710
+ return normalizedField.includes(alias) || alias.includes(normalizedField);
5711
+ });
5712
+ if (fuzzy) return fuzzy;
5713
+ }
5714
+ return void 0;
5715
+ }
5716
+ static tableFieldQueryScore(field, query) {
5717
+ const normalizedField = this.normalizeComparableField(field);
5718
+ if (!normalizedField) return 0;
5719
+ const fieldTokens = this.fieldTokens(field);
5720
+ return fieldTokens.reduce((score, token) => {
5721
+ if (token.length < 3) return score;
5722
+ return query.includes(token) ? score + 1 : score;
5723
+ }, query.includes(normalizedField) ? 3 : 0);
5724
+ }
5725
+ static resolveTableCellValue(item, column) {
5726
+ if (column === "Content") return item.content.substring(0, 100);
5727
+ const meta = item.metadata || {};
5728
+ const normalizedColumn = this.normalizeComparableField(column);
5729
+ const exactMetadata = Object.entries(meta).find(
5730
+ ([key]) => this.normalizeComparableField(key) === normalizedColumn || this.normalizeComparableField(this.humanizeFieldName(key)) === normalizedColumn
5731
+ );
5732
+ if (exactMetadata && exactMetadata[1] !== void 0 && exactMetadata[1] !== null) {
5733
+ return this.toDisplayValue(exactMetadata[1]);
5734
+ }
5735
+ const aliasValue = this.resolveAliasedTableCell(meta, column);
5736
+ if (aliasValue !== void 0) return this.toDisplayValue(aliasValue);
5737
+ const contentValue = this.extractContentFieldValue(item.content, column);
5738
+ if (contentValue !== null) return contentValue;
5739
+ const aliasContentValue = this.extractAliasedContentFieldValue(item.content, column);
5740
+ return aliasContentValue != null ? aliasContentValue : "";
5741
+ }
5742
+ static resolveAliasedTableCell(meta, column) {
5743
+ const normalizedColumn = this.normalizeComparableField(column);
5744
+ 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"] : [];
5745
+ for (const alias of aliases) {
5746
+ const normalizedAlias = this.normalizeComparableField(alias);
5747
+ const match = Object.entries(meta).find(([key]) => {
5748
+ const normalizedKey = this.normalizeComparableField(key);
5749
+ return normalizedKey === normalizedAlias || normalizedKey.includes(normalizedAlias) || normalizedAlias.includes(normalizedKey);
5750
+ });
5751
+ if (match && match[1] !== void 0 && match[1] !== null) return match[1];
5752
+ }
5753
+ return void 0;
5754
+ }
5755
+ static extractContentFieldValue(content, column) {
5756
+ const escaped = column.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "[\\s_ -]+");
5757
+ const pattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\s*[:\\-\u2013]\\s*([^\\n]+)`, "i");
5758
+ const match = content.match(pattern);
5759
+ return match ? match[1].trim() : null;
5760
+ }
5761
+ static extractAliasedContentFieldValue(content, column) {
5762
+ const normalizedColumn = this.normalizeComparableField(column);
5763
+ 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"] : [];
5764
+ for (const alias of aliases) {
5765
+ const value = this.extractContentFieldValue(content, alias);
5766
+ if (value !== null) return value;
5767
+ }
5768
+ return null;
5769
+ }
5770
+ static toDisplayValue(value) {
5771
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
5772
+ return String(value != null ? value : "");
4695
5773
  }
4696
- /**
4697
- * Helper: Calculate stock counts by category
4698
- */
4699
5774
  static calculateStockCounts(category, data) {
4700
5775
  let inStock = 0;
4701
5776
  let outOfStock = 0;
4702
5777
  data.forEach((d) => {
4703
- const meta = d.metadata || {};
4704
- const itemCategory = meta.category || meta.type || "Other";
4705
- if (itemCategory === category) {
4706
- if (this.determineStockStatus(d)) {
4707
- inStock++;
4708
- } else {
4709
- outOfStock++;
4710
- }
5778
+ var _a, _b;
5779
+ const cat = (_a = this.getProductCategory(d)) != null ? _a : "Other";
5780
+ if (cat === category) {
5781
+ const quantity = (_b = this.extractStockQuantity(d)) != null ? _b : 1;
5782
+ if (this.determineStockStatus(d)) inStock += quantity;
5783
+ else outOfStock += quantity;
4711
5784
  }
4712
5785
  });
4713
5786
  return { inStockCount: inStock, outOfStockCount: outOfStock };
4714
5787
  }
4715
- /**
4716
- * Helper: Determine if item is in stock
4717
- */
4718
5788
  static determineStockStatus(item) {
4719
5789
  const meta = item.metadata || {};
4720
- if (meta.inStock !== void 0) {
4721
- return Boolean(meta.inStock);
4722
- }
4723
- if (meta.stock !== void 0) {
4724
- return Boolean(meta.stock);
4725
- }
4726
- if (meta.available !== void 0) {
4727
- return Boolean(meta.available);
4728
- }
5790
+ const stockValue = resolveMetadataValue(meta, "stock");
5791
+ if (stockValue !== void 0) {
5792
+ const normalized = String(stockValue).toLowerCase();
5793
+ if (/out[_\s-]?of[_\s-]?stock|unavailable|false|no|sold out|0/.test(normalized)) return false;
5794
+ if (/in[_\s-]?stock|available|true|yes/.test(normalized)) return true;
5795
+ const numeric = this.toFiniteNumber(stockValue);
5796
+ if (numeric !== null) return numeric > 0;
5797
+ }
5798
+ if (meta.inStock !== void 0) return Boolean(meta.inStock);
5799
+ if (meta.stock !== void 0) return Boolean(meta.stock);
5800
+ if (meta.available !== void 0) return Boolean(meta.available);
4729
5801
  const content = (item.content || "").toLowerCase();
4730
- if (content.includes("out of stock") || content.includes("unavailable")) {
4731
- return false;
5802
+ if (/out[_\s-]?of[_\s-]?stock|unavailable|sold out/.test(content)) return false;
5803
+ if (/in[_\s-]?stock|available/.test(content)) return true;
5804
+ return true;
5805
+ }
5806
+ static extractStockQuantity(item) {
5807
+ const meta = item.metadata || {};
5808
+ const stockValue = resolveMetadataValue(meta, "stock");
5809
+ const numericStock = this.toFiniteNumber(stockValue);
5810
+ if (numericStock !== null) return numericStock;
5811
+ const content = item.content || "";
5812
+ const stockMatch = content.match(/\b(?:stock|inventory|quantity|count)\s*[:\-–]?\s*([\d,]+)/i);
5813
+ if (!stockMatch) return null;
5814
+ return this.toFiniteNumber(stockMatch[1]);
5815
+ }
5816
+ static toFiniteNumber(value) {
5817
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
5818
+ const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
5819
+ return Number.isFinite(numeric) ? numeric : null;
5820
+ }
5821
+ // ─── Product Extraction ───────────────────────────────────────────────────
5822
+ static getDynamicVal(meta, uiKey, config, trainedSchema) {
5823
+ var _a;
5824
+ if (!meta) return void 0;
5825
+ const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
5826
+ if ((mapping == null ? void 0 : mapping[uiKey]) && meta[mapping[uiKey]] !== void 0) return meta[mapping[uiKey]];
5827
+ if (trainedSchema && typeof trainedSchema === "object") {
5828
+ const trainedKey = trainedSchema[uiKey];
5829
+ if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
4732
5830
  }
4733
- if (content.includes("in stock") || content.includes("available")) {
4734
- return true;
5831
+ return resolveMetadataValue(meta, uiKey);
5832
+ }
5833
+ static extractProductInfo(item, config, trainedSchema) {
5834
+ var _a;
5835
+ const meta = item.metadata || {};
5836
+ const name = this.getDynamicVal(meta, "name", config, trainedSchema);
5837
+ const price = this.getDynamicVal(meta, "price", config, trainedSchema);
5838
+ const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
5839
+ const description = this.cleanProductDescription(
5840
+ (_a = this.extractProductDescriptionFromContent(item.content)) != null ? _a : this.getProductDescriptionValue(meta, config, trainedSchema)
5841
+ );
5842
+ if (name || this.isProductData(item)) {
5843
+ let finalName = name ? String(name) : void 0;
5844
+ if (!finalName) {
5845
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
5846
+ finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
5847
+ }
5848
+ let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
5849
+ if (!finalPrice) {
5850
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
5851
+ if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
5852
+ }
5853
+ const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
5854
+ return {
5855
+ id: item.id,
5856
+ name: finalName,
5857
+ price: finalPrice,
5858
+ image: typeof imageValue === "string" ? imageValue : void 0,
5859
+ brand: brand ? String(brand) : void 0,
5860
+ description,
5861
+ inStock: this.determineStockStatus(item)
5862
+ };
4735
5863
  }
4736
- return true;
5864
+ return null;
4737
5865
  }
4738
- /**
4739
- * Helper: Check if data has multiple fields
4740
- */
4741
- static hasMultipleFields(data) {
4742
- const fieldCount = /* @__PURE__ */ new Set();
4743
- data.forEach((item) => {
4744
- Object.keys(item.metadata || {}).forEach((key) => {
4745
- fieldCount.add(key);
4746
- });
4747
- });
4748
- return fieldCount.size > 2;
5866
+ static extractProductDescriptionFromContent(content) {
5867
+ const bodyMatch = content.match(
5868
+ /\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
5869
+ );
5870
+ if (bodyMatch == null ? void 0 : bodyMatch[1]) return bodyMatch[1];
5871
+ const descriptionMatch = content.match(
5872
+ /\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
5873
+ );
5874
+ if (descriptionMatch == null ? void 0 : descriptionMatch[1]) return descriptionMatch[1];
5875
+ return null;
4749
5876
  }
4750
- // ─── LLM-Driven Visualization Decision ────────────────────────────────────
4751
- /**
4752
- * analyzeAndDecide sends user question + RAG data to the LLM with a
4753
- * structured system prompt and parses the JSON response into a
4754
- * UITransformationResponse.
4755
- *
4756
- * This is the recommended entry point for production use. The heuristic
4757
- * `transform()` method is used as a fallback if the LLM call fails.
4758
- *
4759
- * System prompt instructs the LLM to:
4760
- * - Analyze the question and retrieved data
4761
- * - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
4762
- * - Return a strict JSON object — no prose, no markdown fences
4763
- *
4764
- * @param query - the original user question
4765
- * @param sources - vector DB matches returned by RAG retrieval
4766
- * @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
4767
- * @returns - a validated UITransformationResponse (type + title + description + data)
4768
- */
4769
- static async analyzeAndDecide(query, sources, llm) {
4770
- try {
4771
- const context = this.buildContextSummary(sources);
4772
- const systemPrompt = this.buildVisualizationSystemPrompt();
4773
- const userPrompt = [
4774
- `USER QUESTION: ${query}`,
4775
- "",
4776
- "RETRIEVED DATA (JSON):",
4777
- context
4778
- ].join("\n");
4779
- const rawResponse = await llm.chat(
4780
- [{ role: "user", content: userPrompt }],
4781
- "",
4782
- { systemPrompt, temperature: 0 }
5877
+ static getProductDescriptionValue(meta, config, trainedSchema) {
5878
+ const mapped = this.getDynamicVal(meta, "description", config, trainedSchema);
5879
+ if (mapped !== void 0 && mapped !== meta.content && mapped !== meta.text) return mapped;
5880
+ const preferredKeys = [
5881
+ "body_html",
5882
+ "body html",
5883
+ "bodyHtml",
5884
+ "description",
5885
+ "summary",
5886
+ "details",
5887
+ "body"
5888
+ ];
5889
+ for (const key of preferredKeys) {
5890
+ const match = Object.keys(meta).find(
5891
+ (candidate) => this.normalizeComparableField(candidate) === this.normalizeComparableField(key)
4783
5892
  );
4784
- const parsed = this.parseTransformationResponse(rawResponse);
4785
- if (parsed) {
4786
- console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
4787
- return parsed;
4788
- }
4789
- console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
4790
- } catch (err) {
4791
- console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
5893
+ if (match && meta[match] !== void 0 && meta[match] !== null) return meta[match];
4792
5894
  }
4793
- return this.transform(query, sources);
5895
+ return void 0;
4794
5896
  }
4795
- /**
4796
- * Build the system prompt that instructs the LLM to return a visualization JSON.
4797
- */
5897
+ static cleanProductDescription(raw) {
5898
+ if (raw === null || raw === void 0) return void 0;
5899
+ const extracted = this.extractProductDescriptionFromContent(String(raw));
5900
+ if (extracted && extracted !== String(raw)) return this.cleanProductDescription(extracted);
5901
+ 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();
5902
+ return text || void 0;
5903
+ }
5904
+ // ─── Visualization Prompt ─────────────────────────────────────────────────
4798
5905
  static buildVisualizationSystemPrompt() {
4799
5906
  return `You are a data visualization expert embedded in a RAG chat system.
4800
- You will receive a user question and structured data retrieved from a vector database.
4801
- Your ONLY job is to analyze this information and return a single JSON object that tells
4802
- the frontend how to visualize it.
5907
+ You will receive a user question, a pre-computed INTENT object, and structured data from a vector database.
5908
+ Your ONLY job is to return a single JSON object describing how to visualize the data.
5909
+ The INTENT object is authoritative \u2014 honor it. For example:
5910
+ - If intent.wantsExplicitTable is false, never return type "table".
5911
+ - If intent.isTemporal is true, prefer line_chart.
5912
+ - If intent.visualizationHint is "comparison" or "ranking", prefer bar_chart.
5913
+ - If intent.visualizationHint is "composition", prefer pie_chart.
5914
+ - If intent.recommendedChart is unsupported by the response schema, use the nearest supported type.
5915
+ - Always respond in the language specified by intent.language.
4803
5916
 
4804
- Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
5917
+ Return ONLY a valid JSON object \u2014 no markdown fences, no prose.
4805
5918
 
4806
- The JSON must have this exact shape:
4807
5919
  {
4808
- "type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
4809
- "title": "A concise, descriptive title for the visualization",
4810
- "description": "One sentence describing what the visualization shows",
4811
- "data": <structured data \u2014 see rules below>
5920
+ "type": "bar_chart" | "horizontal_bar" | "line_chart" | "pie_chart" | "histogram" | "scatter_plot" | "radar_chart" | "metric_card" | "geo_map" | "table" | "text",
5921
+ "title": "concise descriptive title",
5922
+ "description": "one sentence describing the visualization",
5923
+ "data": <structured data per type below>
4812
5924
  }
4813
5925
 
4814
- DATA SHAPE per type:
4815
- - bar_chart: array of { "category": string, "value": number }
4816
- - line_chart: array of { "timestamp": string, "value": number, "label": string }
4817
- - pie_chart: array of { "label": string, "value": number }
4818
- - radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
4819
- Example for radar_chart:
4820
- [
4821
- { "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
4822
- { "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
4823
- { "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
4824
- ]
4825
- - table: { "columns": string[], "rows": (string|number)[][] }
4826
- - text: { "content": "<prose answer>" }
4827
-
4828
- DECISION RULES (follow strictly):
4829
- 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.
4830
- 2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
4831
- 3. pie_chart \u2192 proportional breakdown or percentage distribution
4832
- 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.
4833
- 5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
4834
- 6. text \u2192 conversational or free-form answers where no chart adds value
5926
+ DATA SHAPES:
5927
+ - bar_chart: [{ "category": string, "value": number }]
5928
+ - horizontal_bar: [{ "category": string, "value": number }]
5929
+ - histogram: [{ "category": string, "value": number }]
5930
+ - line_chart: [{ "timestamp": string, "value": number, "label": string }]
5931
+ - pie_chart: [{ "label": string, "value": number }]
5932
+ - scatter_plot:[{ "x": number, "y": number, "label": string }]
5933
+ - radar_chart: [{ "attribute": string, "<series1>": number, "<series2>": number, ... }]
5934
+ - metric_card: { "label": string, "value": number, "operation": "sum"|"average"|"count"|"min"|"max"|"median" }
5935
+ - geo_map: [{ "category": string, "value": number }]
5936
+ - table: { "columns": string[], "rows": (string|number)[][] }
5937
+ - text: { "content": "A concise plain-language answer." }
4835
5938
 
4836
- IMPORTANT:
4837
- - Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
4838
- - Ensure all "value" fields are numbers, not strings.
4839
- - For bar/line/pie, keep at most 12 data points for readability.
4840
- - Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
5939
+ RULES:
5940
+ 1. Aggregate values \u2014 never pass raw nested objects in chart arrays.
5941
+ 2. All "value" fields must be numbers.
5942
+ 3. Cap bar/line/pie at 12 data points.
5943
+ 4. Use dollar signs only for monetary prices, not for stock quantities or years.
5944
+ 5. If no relevant data is found, return text: "I cannot answer this question based on the available information."`;
4841
5945
  }
4842
- /**
4843
- * Serialize retrieved vector matches into a compact JSON context string.
4844
- * Limits the total character count to avoid exceeding LLM context windows.
4845
- */
4846
5946
  static buildContextSummary(sources, maxChars = 6e3) {
4847
5947
  const items = sources.map((s, i) => {
4848
5948
  var _a, _b, _c, _d;
@@ -5063,27 +6163,38 @@ var Pipeline = class {
5063
6163
  async initialize() {
5064
6164
  var _a;
5065
6165
  if (this.initialised) return;
5066
- const chartInstruction = `You are a helpful product assistant. Use the provided context to answer questions accurately.
6166
+ const chartInstruction = `You are a helpful assistant. Use the provided context to answer questions accurately.
5067
6167
 
5068
- ### UI STYLE RULES (CRITICAL):
6168
+ ### CRITICAL RULES:
6169
+ - ONLY answer the user's specific question. Do NOT suggest or recommend unrelated products unless specifically asked.
5069
6170
  - NEVER generate markdown tables. If you do, the UI will break.
5070
6171
  - NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
5071
6172
  - NEVER generate text-based charts or graphs.
6173
+ - NEVER say you cannot display, render, draw, or create a chart when the user asks for a visualization. The UI handles visual rendering separately.
5072
6174
  - ONLY use plain text and bullet points.
6175
+ - NEVER use the plus sign (+) as a separator between names, categories, or products. Use commas or bullet points.
6176
+ - ONLY answer the question if the sources contain relevant information to answer it, else say that you cannot answer the question.
6177
+ - If answer cannot be found in the sources, say that you cannot answer the question and do not hallucinate.
6178
+ - Do not use information from previous turns to answer the question.
6179
+ - 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.
5073
6180
 
5074
6181
  ### PRODUCT DISPLAY:
5075
- - When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
6182
+ - Recommended Products should only be displayed when the user explicitly asks for products.
6183
+ - When recommending products (ONLY when asked), simply list their names, prices, and features in a friendly, conversational manner.
5076
6184
  - The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
6185
+ - 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.
5077
6186
  - Do NOT try to format product lists as tables.
5078
6187
  `;
5079
6188
  this.config.llm.systemPrompt = chartInstruction;
5080
6189
  this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
5081
- const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
6190
+ this.llmRouter = new LLMRouter(this.config);
6191
+ const { llmProvider: resolvedLLM, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
5082
6192
  this.config.llm,
5083
6193
  this.config.embedding
5084
6194
  );
5085
- this.llmProvider = llmProvider;
5086
6195
  this.embeddingProvider = embeddingProvider;
6196
+ await this.llmRouter.initialize(resolvedLLM);
6197
+ this.llmProvider = this.llmRouter.get("default");
5087
6198
  if (this.config.graphDb) {
5088
6199
  this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
5089
6200
  await this.graphDB.initialize();
@@ -5240,10 +6351,16 @@ var Pipeline = class {
5240
6351
  /**
5241
6352
  * High-performance streaming RAG flow.
5242
6353
  * Yields text chunks first, then the retrieval metadata + observability trace at the end.
6354
+ *
6355
+ * Latency optimizations:
6356
+ * - Strategy classification runs in parallel with query embedding (saves ~400ms)
6357
+ * - Hallucination scoring is fire-and-forget (doesn't block metadata yield)
6358
+ * - UITransformation is computed after text streaming and emitted with metadata
6359
+ * - SchemaMapper.train runs while answer generation streams
5243
6360
  */
5244
6361
  askStream(_0) {
5245
6362
  return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
5246
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
6363
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
5247
6364
  yield new __await(this.initialize());
5248
6365
  const ns = namespace != null ? namespace : this.config.projectId;
5249
6366
  const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
@@ -5259,27 +6376,48 @@ var Pipeline = class {
5259
6376
  }
5260
6377
  const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
5261
6378
  const filter = QueryProcessor.buildQueryFilter(question, hints);
6379
+ const numericPredicates = QueryProcessor.extractNumericPredicates(question, (_g = this.config.rag) == null ? void 0 : _g.filterableFields);
6380
+ if (numericPredicates.length > 0) {
6381
+ filter.__numericPredicates = numericPredicates;
6382
+ }
5262
6383
  const embedStart = performance.now();
5263
- const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
5264
- namespace: ns,
5265
- topK: topK * 2,
5266
- filter
5267
- }));
6384
+ const cacheKey = `${ns}::${searchQuery}`;
6385
+ const cachedVector = this.embeddingCache.get(cacheKey);
6386
+ const [strategyResult, embeddedVector] = yield new __await(Promise.all([
6387
+ QueryProcessor.determineRetrievalStrategy(
6388
+ searchQuery,
6389
+ this.llmRouter.get("fast"),
6390
+ (_h = this.config.rag) == null ? void 0 : _h.graphKeywords,
6391
+ (_i = this.config.rag) == null ? void 0 : _i.vectorKeywords
6392
+ ),
6393
+ // Embed immediately regardless of strategy — costs nothing if cached.
6394
+ // If strategy turns out to be 'graph'-only we just won't use the vector.
6395
+ cachedVector ? Promise.resolve(cachedVector) : this.embeddingProvider.embed(searchQuery, { taskType: "query" })
6396
+ ]));
6397
+ const queryVector = embeddedVector;
6398
+ if (!cachedVector && queryVector.length > 0) {
6399
+ this.embeddingCache.set(cacheKey, queryVector);
6400
+ }
6401
+ 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;
6402
+ const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
6403
+ const wantsExhaustiveList = hasNumericPredicates || /\b(list|all|show|provide|give|get)\b/i.test(question);
6404
+ const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
6405
+ const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
5268
6406
  const retrieveEnd = performance.now();
5269
6407
  const embedMs = retrieveEnd - embedStart;
5270
6408
  const retrieveMs = retrieveEnd - embedStart;
5271
6409
  const rerankStart = performance.now();
5272
- let sources = rawSources.filter((m) => m.score >= scoreThreshold);
5273
- if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
6410
+ const structuredSources = this.applyStructuredFilters(rawSources, filter);
6411
+ let sources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
6412
+ if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
5274
6413
  sources = yield new __await(this.reranker.rerank(sources, question, topK));
5275
- } else {
6414
+ } else if (!wantsExhaustiveList) {
5276
6415
  sources = sources.slice(0, topK);
5277
6416
  }
5278
6417
  const rerankMs = performance.now() - rerankStart;
5279
6418
  let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
5280
6419
  ${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
5281
6420
  if (graphData && graphData.nodes.length > 0) {
5282
- console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
5283
6421
  const graphContext = graphData.nodes.map(
5284
6422
  (n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
5285
6423
  ).join("\n");
@@ -5290,20 +6428,18 @@ VECTOR CONTEXT:
5290
6428
  ${context}`;
5291
6429
  }
5292
6430
  const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
5293
- const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
5294
- const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
6431
+ const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
6432
+ const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. The UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization. If listing products, use simple bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)";
5295
6433
  const hardenedHistory = [...history];
5296
6434
  const userQuestion = { role: "user", content: question + restrictionSuffix };
5297
6435
  const messages = [...hardenedHistory, userQuestion];
5298
- const systemPrompt = (_h = this.config.llm.systemPrompt) != null ? _h : "";
6436
+ const systemPrompt = (_l = this.config.llm.systemPrompt) != null ? _l : "";
5299
6437
  const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
5300
6438
  let fullReply = "";
5301
6439
  const generateStart = performance.now();
5302
6440
  if (this.llmProvider.chatStream) {
5303
6441
  const stream = this.llmProvider.chatStream(messages, context);
5304
- if (!stream) {
5305
- throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
5306
- }
6442
+ if (!stream) throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
5307
6443
  try {
5308
6444
  for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
5309
6445
  const chunk = temp.value;
@@ -5330,7 +6466,7 @@ ${context}`;
5330
6466
  const latency = {
5331
6467
  embedMs: Math.round(embedMs),
5332
6468
  retrieveMs: Math.round(retrieveMs),
5333
- rerankMs: ((_i = this.config.rag) == null ? void 0 : _i.useReranking) ? Math.round(rerankMs) : void 0,
6469
+ rerankMs: ((_m = this.config.rag) == null ? void 0 : _m.useReranking) ? Math.round(rerankMs) : void 0,
5334
6470
  generateMs: Math.round(generateMs),
5335
6471
  totalMs: Math.round(totalMs)
5336
6472
  };
@@ -5343,9 +6479,6 @@ ${context}`;
5343
6479
  totalTokens: promptTokens + completionTokens,
5344
6480
  estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
5345
6481
  };
5346
- const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
5347
- const trainedSchema = yield new __await(trainingPromise);
5348
- const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, trainedSchema));
5349
6482
  const trace = {
5350
6483
  requestId,
5351
6484
  query: question,
@@ -5364,10 +6497,21 @@ ${context}`;
5364
6497
  }),
5365
6498
  latency,
5366
6499
  tokens,
5367
- hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
5368
- hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
5369
6500
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5370
6501
  };
6502
+ let uiTransformation;
6503
+ try {
6504
+ const trainedSchema = yield new __await(trainedSchemaPromise);
6505
+ uiTransformation = yield new __await(this.generateUiTransformation(
6506
+ question,
6507
+ sources,
6508
+ trainedSchema,
6509
+ hasNumericPredicates
6510
+ ));
6511
+ } catch (uiError) {
6512
+ console.warn("[Pipeline] UI transformation failed before metadata yield:", uiError);
6513
+ uiTransformation = UITransformer.transform(question, sources, this.config);
6514
+ }
5371
6515
  yield {
5372
6516
  reply: "",
5373
6517
  sources,
@@ -5375,6 +6519,13 @@ ${context}`;
5375
6519
  ui_transformation: uiTransformation,
5376
6520
  trace
5377
6521
  };
6522
+ scoreHallucination(this.llmProvider, fullReply, context).then((hallucinationResult) => {
6523
+ if (hallucinationResult) {
6524
+ trace.hallucinationScore = hallucinationResult.score;
6525
+ trace.hallucinationReason = hallucinationResult.reason;
6526
+ }
6527
+ }).catch(() => {
6528
+ });
5378
6529
  } catch (error2) {
5379
6530
  throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
5380
6531
  }
@@ -5384,24 +6535,107 @@ ${context}`;
5384
6535
  * Universal retrieval method combining all enabled providers.
5385
6536
  * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
5386
6537
  */
5387
- async generateUiTransformation(question, sources, trainedSchema) {
6538
+ async generateUiTransformation(question, sources, trainedSchema, forceDeterministic = false) {
5388
6539
  if (!sources || sources.length === 0) {
5389
6540
  return UITransformer.transform(question, sources, this.config, trainedSchema);
5390
6541
  }
6542
+ if (forceDeterministic) {
6543
+ return UITransformer.transform(question, sources, this.config, trainedSchema);
6544
+ }
5391
6545
  try {
5392
- return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
6546
+ return await UITransformer.analyzeAndDecide(question, sources, this.llmRouter.get("fast"));
5393
6547
  } catch (err) {
5394
6548
  console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
5395
6549
  return UITransformer.transform(question, sources, this.config, trainedSchema);
5396
6550
  }
5397
6551
  }
6552
+ applyStructuredFilters(sources, filter) {
6553
+ const predicates = Array.isArray(filter.__numericPredicates) ? filter.__numericPredicates : [];
6554
+ if (predicates.length === 0) return sources;
6555
+ return sources.filter((source) => predicates.every((predicate) => {
6556
+ const value = this.resolveNumericPredicateValue(source, predicate);
6557
+ return value !== null && this.matchesNumericPredicate(value, predicate);
6558
+ })).sort((a, b) => {
6559
+ var _a, _b;
6560
+ const primary = predicates[0];
6561
+ const aValue = (_a = this.resolveNumericPredicateValue(a, primary)) != null ? _a : 0;
6562
+ const bValue = (_b = this.resolveNumericPredicateValue(b, primary)) != null ? _b : 0;
6563
+ return primary.operator === "lt" || primary.operator === "lte" ? aValue - bValue : bValue - aValue;
6564
+ });
6565
+ }
6566
+ resolveNumericPredicateValue(source, predicate) {
6567
+ const meta = source.metadata || {};
6568
+ const field = predicate.field;
6569
+ const entries = Object.entries(meta).filter(([, value]) => value !== null && value !== void 0 && typeof value !== "object");
6570
+ if (field) {
6571
+ const normalizedField = this.normalizeComparableField(field);
6572
+ const exact = entries.find(([key]) => this.normalizeComparableField(key) === normalizedField);
6573
+ 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];
6574
+ if (fuzzy) {
6575
+ const value = this.toFiniteNumber(Array.isArray(fuzzy) ? fuzzy[1] : fuzzy.value);
6576
+ if (value !== null) return value;
6577
+ }
6578
+ const contentValue = this.extractNumericValueFromContent(source.content, field);
6579
+ if (contentValue !== null) return contentValue;
6580
+ }
6581
+ for (const [key, value] of entries) {
6582
+ if (/(id|sku|ean|upc|phone|zip|postal|code)/i.test(key)) continue;
6583
+ const numeric = this.toFiniteNumber(value);
6584
+ if (numeric !== null) return numeric;
6585
+ }
6586
+ return null;
6587
+ }
6588
+ extractNumericValueFromContent(content, field) {
6589
+ const escapedWords = field.split(/\s+|_+|-+/).filter(Boolean).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
6590
+ if (escapedWords.length === 0) return null;
6591
+ const pattern = new RegExp(`(?:${escapedWords.join("[\\\\s_:-]*")})\\s*[:\\-\u2013]?\\s*([\\d,]+(?:\\.\\d+)?)`, "i");
6592
+ const match = content.match(pattern);
6593
+ return match ? this.toFiniteNumber(match[1]) : null;
6594
+ }
6595
+ matchesNumericPredicate(value, predicate) {
6596
+ switch (predicate.operator) {
6597
+ case "gt":
6598
+ return value > predicate.value;
6599
+ case "gte":
6600
+ return value >= predicate.value;
6601
+ case "lt":
6602
+ return value < predicate.value;
6603
+ case "lte":
6604
+ return value <= predicate.value;
6605
+ case "eq":
6606
+ default:
6607
+ return value === predicate.value;
6608
+ }
6609
+ }
6610
+ normalizeComparableField(value) {
6611
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
6612
+ }
6613
+ fieldSimilarityScore(candidate, requested) {
6614
+ const normalizedCandidate = this.normalizeComparableField(candidate);
6615
+ const normalizedRequested = this.normalizeComparableField(requested);
6616
+ if (normalizedCandidate === normalizedRequested) return 10;
6617
+ if (normalizedCandidate.includes(normalizedRequested) || normalizedRequested.includes(normalizedCandidate)) return 8;
6618
+ const candidateTokens = this.fieldTokens(candidate);
6619
+ const requestedTokens = this.fieldTokens(requested);
6620
+ const overlap = requestedTokens.filter((token) => candidateTokens.includes(token)).length;
6621
+ if (overlap === 0) return 0;
6622
+ return overlap / Math.max(requestedTokens.length, candidateTokens.length);
6623
+ }
6624
+ fieldTokens(value) {
6625
+ return value.toLowerCase().split(/[^a-z0-9]+/).map((token) => token.replace(/ies$/, "y").replace(/s$/, "")).filter((token) => token.length > 1);
6626
+ }
6627
+ toFiniteNumber(value) {
6628
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
6629
+ const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
6630
+ return Number.isFinite(numeric) ? numeric : null;
6631
+ }
5398
6632
  async retrieve(query, options) {
5399
- var _a, _b, _c;
6633
+ var _a, _b, _c, _d, _e, _f;
5400
6634
  const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
5401
6635
  const topK = (_b = options.topK) != null ? _b : 5;
5402
6636
  const cacheKey = `${ns}::${query}`;
5403
6637
  let queryVector = this.embeddingCache.get(cacheKey);
5404
- const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmProvider);
6638
+ const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmRouter.get("fast"));
5405
6639
  console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
5406
6640
  const [retrievedVector, graphData] = await Promise.all([
5407
6641
  // Only embed if we need vector search (strategy is 'vector' or 'both')
@@ -5413,8 +6647,27 @@ ${context}`;
5413
6647
  this.embeddingCache.set(cacheKey, retrievedVector);
5414
6648
  queryVector = retrievedVector;
5415
6649
  }
5416
- const sources = (strategy === "vector" || strategy === "both") && queryVector && queryVector.length > 0 ? await this.vectorDB.query(queryVector, topK, ns, options.filter) : [];
5417
- return { sources, graphData };
6650
+ const baseFilter = __spreadProps(__spreadValues({}, (_d = options.filter) != null ? _d : {}), { queryText: query });
6651
+ const numericPredicates = QueryProcessor.extractNumericPredicates(query, (_e = this.config.rag) == null ? void 0 : _e.filterableFields);
6652
+ if (numericPredicates.length > 0) {
6653
+ baseFilter.__numericPredicates = numericPredicates;
6654
+ }
6655
+ const retrievalLimit = numericPredicates.length > 0 ? Math.max(topK * 20, 100) : topK;
6656
+ const sources = (strategy === "vector" || strategy === "both") && queryVector && queryVector.length > 0 ? this.applyStructuredFilters(
6657
+ await this.vectorDB.query(queryVector, retrievalLimit, ns, baseFilter),
6658
+ baseFilter
6659
+ ) : [];
6660
+ const resolvedSources = [];
6661
+ for (const source of sources) {
6662
+ const parentId = (_f = source.metadata) == null ? void 0 : _f.parent_id;
6663
+ if (parentId) {
6664
+ console.log(`[Pipeline] Multi-Vector: Found child chunk. Parent ID: ${parentId}`);
6665
+ resolvedSources.push(source);
6666
+ } else {
6667
+ resolvedSources.push(source);
6668
+ }
6669
+ }
6670
+ return { sources: resolvedSources, graphData };
5418
6671
  }
5419
6672
  /** Rewrite the user query for better retrieval performance. */
5420
6673
  async rewriteQuery(question, history) {
@@ -5906,204 +7159,7 @@ var DocumentParser = class {
5906
7159
  // src/server.ts
5907
7160
  init_BaseVectorProvider();
5908
7161
  init_PineconeProvider();
5909
-
5910
- // src/providers/vectordb/PostgreSQLProvider.ts
5911
- var import_pg2 = require("pg");
5912
- init_BaseVectorProvider();
5913
- var PostgreSQLProvider = class extends BaseVectorProvider {
5914
- constructor(config) {
5915
- var _a;
5916
- super(config);
5917
- this.tableName = this.indexName.replace(/[^a-z0-9_]/gi, "_");
5918
- const opts = config.options;
5919
- if (!opts.connectionString) throw new Error("[PostgreSQLProvider] options.connectionString is required");
5920
- this.connectionString = opts.connectionString;
5921
- this.dimensions = (_a = opts.dimensions) != null ? _a : 1536;
5922
- }
5923
- static getValidator() {
5924
- return {
5925
- validate(config) {
5926
- const errors = [];
5927
- const opts = config.options || {};
5928
- if (!opts.connectionString) {
5929
- errors.push({
5930
- field: "vectorDb.options.connectionString",
5931
- message: "PostgreSQL connection string is required",
5932
- suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
5933
- severity: "error"
5934
- });
5935
- }
5936
- if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
5937
- errors.push({
5938
- field: "vectorDb.options.tables",
5939
- message: "PostgreSQL tables must be a string or a string array",
5940
- severity: "error"
5941
- });
5942
- }
5943
- return errors;
5944
- }
5945
- };
5946
- }
5947
- static getHealthChecker() {
5948
- return {
5949
- async check(config) {
5950
- const opts = config.options || {};
5951
- const timestamp = Date.now();
5952
- try {
5953
- const { Client } = await import("pg");
5954
- const client = new Client({ connectionString: opts.connectionString });
5955
- await client.connect();
5956
- const result = await client.query(`
5957
- SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
5958
- `);
5959
- const hasVector = result.rows[0].exists;
5960
- await client.end();
5961
- return {
5962
- healthy: true,
5963
- provider: "postgresql",
5964
- capabilities: { pgvectorInstalled: hasVector },
5965
- timestamp
5966
- };
5967
- } catch (error) {
5968
- return {
5969
- healthy: false,
5970
- provider: "postgresql",
5971
- error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
5972
- timestamp
5973
- };
5974
- }
5975
- }
5976
- };
5977
- }
5978
- async initialize() {
5979
- this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
5980
- const client = await this.pool.connect();
5981
- try {
5982
- await client.query("CREATE EXTENSION IF NOT EXISTS vector");
5983
- await client.query(`
5984
- CREATE TABLE IF NOT EXISTS ${this.tableName} (
5985
- id TEXT PRIMARY KEY,
5986
- namespace TEXT NOT NULL DEFAULT '',
5987
- content TEXT NOT NULL,
5988
- metadata JSONB,
5989
- embedding VECTOR(${this.dimensions})
5990
- )
5991
- `);
5992
- await client.query(`
5993
- CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
5994
- ON ${this.tableName}
5995
- USING hnsw (embedding vector_cosine_ops)
5996
- `);
5997
- } finally {
5998
- client.release();
5999
- }
6000
- }
6001
- async upsert(doc, namespace = "") {
6002
- var _a;
6003
- const vectorLiteral = `[${doc.vector.join(",")}]`;
6004
- await this.pool.query(
6005
- `INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
6006
- VALUES ($1, $2, $3, $4, $5::vector)
6007
- ON CONFLICT (id) DO UPDATE
6008
- SET namespace = EXCLUDED.namespace,
6009
- content = EXCLUDED.content,
6010
- metadata = EXCLUDED.metadata,
6011
- embedding = EXCLUDED.embedding`,
6012
- [doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
6013
- );
6014
- }
6015
- async batchUpsert(docs, namespace = "") {
6016
- if (docs.length === 0) return;
6017
- const client = await this.pool.connect();
6018
- try {
6019
- await client.query("BEGIN");
6020
- const BATCH_SIZE = 50;
6021
- for (let i = 0; i < docs.length; i += BATCH_SIZE) {
6022
- const batch = docs.slice(i, i + BATCH_SIZE);
6023
- const values = [];
6024
- const valuePlaceholders = batch.map((doc, idx) => {
6025
- var _a;
6026
- const offset = idx * 5;
6027
- values.push(doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), `[${doc.vector.join(",")}]`);
6028
- return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}::vector)`;
6029
- }).join(", ");
6030
- const query = `
6031
- INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
6032
- VALUES ${valuePlaceholders}
6033
- ON CONFLICT (id) DO UPDATE
6034
- SET namespace = EXCLUDED.namespace,
6035
- content = EXCLUDED.content,
6036
- metadata = EXCLUDED.metadata,
6037
- embedding = EXCLUDED.embedding
6038
- `;
6039
- await client.query(query, values);
6040
- }
6041
- await client.query("COMMIT");
6042
- } catch (error) {
6043
- await client.query("ROLLBACK");
6044
- throw error;
6045
- } finally {
6046
- client.release();
6047
- }
6048
- }
6049
- async query(vector, topK, namespace, filter) {
6050
- const vectorLiteral = `[${vector.join(",")}]`;
6051
- let whereClause = namespace ? `WHERE namespace = $3` : "";
6052
- const params = [vectorLiteral, topK];
6053
- if (namespace) params.push(namespace);
6054
- const publicFilter = this.sanitizeFilter(filter);
6055
- if (Object.keys(publicFilter).length > 0) {
6056
- const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
6057
- const paramIdx = params.length + 1;
6058
- params.push(JSON.stringify(val));
6059
- return `metadata->>'${key}' = $${paramIdx}`;
6060
- }).join(" AND ");
6061
- whereClause = whereClause ? `${whereClause} AND ${filterConditions}` : `WHERE ${filterConditions}`;
6062
- }
6063
- const client = await this.pool.connect();
6064
- try {
6065
- const efSearch = this.config.options.efSearch || Math.max(topK * 10, 40);
6066
- await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
6067
- const result = await client.query(
6068
- `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
6069
- FROM ${this.tableName}
6070
- ${whereClause}
6071
- ORDER BY embedding <=> $1::vector
6072
- LIMIT $2`,
6073
- params
6074
- );
6075
- return result.rows.map((row) => ({
6076
- id: String(row["id"]),
6077
- score: parseFloat(String(row["score"])),
6078
- content: String(row["content"]),
6079
- metadata: row["metadata"]
6080
- }));
6081
- } finally {
6082
- client.release();
6083
- }
6084
- }
6085
- async delete(id, namespace) {
6086
- const where = namespace ? "WHERE id = $1 AND namespace = $2" : "WHERE id = $1";
6087
- const params = namespace ? [id, namespace] : [id];
6088
- await this.pool.query(`DELETE FROM ${this.tableName} ${where}`, params);
6089
- }
6090
- async deleteNamespace(namespace) {
6091
- await this.pool.query(`DELETE FROM ${this.tableName} WHERE namespace = $1`, [namespace]);
6092
- }
6093
- async ping() {
6094
- try {
6095
- await this.pool.query("SELECT 1");
6096
- return true;
6097
- } catch (e) {
6098
- return false;
6099
- }
6100
- }
6101
- async disconnect() {
6102
- await this.pool.end();
6103
- }
6104
- };
6105
-
6106
- // src/server.ts
7162
+ init_PostgreSQLProvider();
6107
7163
  init_MultiTablePostgresProvider();
6108
7164
  init_MongoDBProvider();
6109
7165
  init_MilvusProvider();
@@ -6191,7 +7247,7 @@ function createStreamHandler(configOrPlugin) {
6191
7247
  const encoder = new TextEncoder();
6192
7248
  const stream = new ReadableStream({
6193
7249
  async start(controller) {
6194
- var _a, _b;
7250
+ var _a;
6195
7251
  const enqueue = (text) => controller.enqueue(encoder.encode(text));
6196
7252
  try {
6197
7253
  const pipelineStream = plugin.chatStream(message, history, namespace);
@@ -6209,8 +7265,7 @@ function createStreamHandler(configOrPlugin) {
6209
7265
  }
6210
7266
  if (sources.length > 0) {
6211
7267
  try {
6212
- const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
6213
- 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());
7268
+ const uiTransformation = (_a = responseChunk == null ? void 0 : responseChunk.ui_transformation) != null ? _a : UITransformer.transform(message, sources, plugin.getConfig());
6214
7269
  if (uiTransformation) {
6215
7270
  enqueue(sseUIFrame(uiTransformation));
6216
7271
  }