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