@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.mjs
CHANGED
|
@@ -313,17 +313,300 @@ var init_PineconeProvider = __esm({
|
|
|
313
313
|
}
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
// src/providers/vectordb/PostgreSQLProvider.ts
|
|
317
|
+
var PostgreSQLProvider_exports = {};
|
|
318
|
+
__export(PostgreSQLProvider_exports, {
|
|
319
|
+
PostgreSQLProvider: () => PostgreSQLProvider
|
|
320
|
+
});
|
|
321
|
+
import { Pool } from "pg";
|
|
322
|
+
var PostgreSQLProvider;
|
|
323
|
+
var init_PostgreSQLProvider = __esm({
|
|
324
|
+
"src/providers/vectordb/PostgreSQLProvider.ts"() {
|
|
325
|
+
"use strict";
|
|
326
|
+
init_BaseVectorProvider();
|
|
327
|
+
PostgreSQLProvider = class extends BaseVectorProvider {
|
|
328
|
+
constructor(config) {
|
|
329
|
+
var _a;
|
|
330
|
+
super(config);
|
|
331
|
+
this.tableName = this.indexName.replace(/[^a-z0-9_]/gi, "_");
|
|
332
|
+
const opts = config.options;
|
|
333
|
+
if (!opts.connectionString) throw new Error("[PostgreSQLProvider] options.connectionString is required");
|
|
334
|
+
this.connectionString = opts.connectionString;
|
|
335
|
+
this.dimensions = (_a = opts.dimensions) != null ? _a : 1536;
|
|
336
|
+
}
|
|
337
|
+
static getValidator() {
|
|
338
|
+
return {
|
|
339
|
+
validate(config) {
|
|
340
|
+
const errors = [];
|
|
341
|
+
const opts = config.options || {};
|
|
342
|
+
if (!opts.connectionString) {
|
|
343
|
+
errors.push({
|
|
344
|
+
field: "vectorDb.options.connectionString",
|
|
345
|
+
message: "PostgreSQL connection string is required",
|
|
346
|
+
suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
|
|
347
|
+
severity: "error"
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
|
|
351
|
+
errors.push({
|
|
352
|
+
field: "vectorDb.options.tables",
|
|
353
|
+
message: "PostgreSQL tables must be a string or a string array",
|
|
354
|
+
severity: "error"
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return errors;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
static getHealthChecker() {
|
|
362
|
+
return {
|
|
363
|
+
async check(config) {
|
|
364
|
+
const opts = config.options || {};
|
|
365
|
+
const timestamp = Date.now();
|
|
366
|
+
try {
|
|
367
|
+
const { Client } = await import("pg");
|
|
368
|
+
const client = new Client({ connectionString: opts.connectionString });
|
|
369
|
+
await client.connect();
|
|
370
|
+
const result = await client.query(`
|
|
371
|
+
SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
|
|
372
|
+
`);
|
|
373
|
+
const hasVector = result.rows[0].exists;
|
|
374
|
+
await client.end();
|
|
375
|
+
return {
|
|
376
|
+
healthy: true,
|
|
377
|
+
provider: "postgresql",
|
|
378
|
+
capabilities: { pgvectorInstalled: hasVector },
|
|
379
|
+
timestamp
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
healthy: false,
|
|
384
|
+
provider: "postgresql",
|
|
385
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
386
|
+
timestamp
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async initialize() {
|
|
393
|
+
this.pool = new Pool({ connectionString: this.connectionString });
|
|
394
|
+
const client = await this.pool.connect();
|
|
395
|
+
try {
|
|
396
|
+
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
397
|
+
await client.query(`
|
|
398
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
399
|
+
id TEXT PRIMARY KEY,
|
|
400
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
401
|
+
content TEXT NOT NULL,
|
|
402
|
+
metadata JSONB,
|
|
403
|
+
embedding VECTOR(${this.dimensions})
|
|
404
|
+
)
|
|
405
|
+
`);
|
|
406
|
+
await client.query(`
|
|
407
|
+
CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
|
|
408
|
+
ON ${this.tableName}
|
|
409
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
410
|
+
`);
|
|
411
|
+
} finally {
|
|
412
|
+
client.release();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async upsert(doc, namespace = "") {
|
|
416
|
+
var _a;
|
|
417
|
+
const vectorLiteral = `[${doc.vector.join(",")}]`;
|
|
418
|
+
await this.pool.query(
|
|
419
|
+
`INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
420
|
+
VALUES ($1, $2, $3, $4, $5::vector)
|
|
421
|
+
ON CONFLICT (id) DO UPDATE
|
|
422
|
+
SET namespace = EXCLUDED.namespace,
|
|
423
|
+
content = EXCLUDED.content,
|
|
424
|
+
metadata = EXCLUDED.metadata,
|
|
425
|
+
embedding = EXCLUDED.embedding`,
|
|
426
|
+
[doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
async batchUpsert(docs, namespace = "") {
|
|
430
|
+
if (docs.length === 0) return;
|
|
431
|
+
const client = await this.pool.connect();
|
|
432
|
+
try {
|
|
433
|
+
await client.query("BEGIN");
|
|
434
|
+
const BATCH_SIZE = 50;
|
|
435
|
+
for (let i = 0; i < docs.length; i += BATCH_SIZE) {
|
|
436
|
+
const batch = docs.slice(i, i + BATCH_SIZE);
|
|
437
|
+
const values = [];
|
|
438
|
+
const valuePlaceholders = batch.map((doc, idx) => {
|
|
439
|
+
var _a;
|
|
440
|
+
const offset = idx * 5;
|
|
441
|
+
values.push(doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), `[${doc.vector.join(",")}]`);
|
|
442
|
+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}::vector)`;
|
|
443
|
+
}).join(", ");
|
|
444
|
+
const query = `
|
|
445
|
+
INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
446
|
+
VALUES ${valuePlaceholders}
|
|
447
|
+
ON CONFLICT (id) DO UPDATE
|
|
448
|
+
SET namespace = EXCLUDED.namespace,
|
|
449
|
+
content = EXCLUDED.content,
|
|
450
|
+
metadata = EXCLUDED.metadata,
|
|
451
|
+
embedding = EXCLUDED.embedding
|
|
452
|
+
`;
|
|
453
|
+
await client.query(query, values);
|
|
454
|
+
}
|
|
455
|
+
await client.query("COMMIT");
|
|
456
|
+
} catch (error) {
|
|
457
|
+
await client.query("ROLLBACK");
|
|
458
|
+
throw error;
|
|
459
|
+
} finally {
|
|
460
|
+
client.release();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async query(vector, topK, namespace, filter) {
|
|
464
|
+
const vectorLiteral = `[${vector.join(",")}]`;
|
|
465
|
+
let whereClause = namespace ? `WHERE namespace = $3` : "";
|
|
466
|
+
const params = [vectorLiteral, topK];
|
|
467
|
+
if (namespace) params.push(namespace);
|
|
468
|
+
const publicFilter = this.sanitizeFilter(filter);
|
|
469
|
+
if (Object.keys(publicFilter).length > 0) {
|
|
470
|
+
const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
|
|
471
|
+
const paramIdx = params.length + 1;
|
|
472
|
+
params.push(String(val));
|
|
473
|
+
return `metadata->>'${key}' = $${paramIdx}`;
|
|
474
|
+
}).join(" AND ");
|
|
475
|
+
whereClause = whereClause ? `${whereClause} AND ${filterConditions}` : `WHERE ${filterConditions}`;
|
|
476
|
+
}
|
|
477
|
+
const client = await this.pool.connect();
|
|
478
|
+
try {
|
|
479
|
+
const efSearch = this.config.options.efSearch || Math.max(topK * 10, 40);
|
|
480
|
+
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
481
|
+
const result = await client.query(
|
|
482
|
+
`SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
|
|
483
|
+
FROM ${this.tableName}
|
|
484
|
+
${whereClause}
|
|
485
|
+
ORDER BY embedding <=> $1::vector
|
|
486
|
+
LIMIT $2`,
|
|
487
|
+
params
|
|
488
|
+
);
|
|
489
|
+
return result.rows.map((row) => ({
|
|
490
|
+
id: String(row["id"]),
|
|
491
|
+
score: parseFloat(String(row["score"])),
|
|
492
|
+
content: String(row["content"]),
|
|
493
|
+
metadata: row["metadata"]
|
|
494
|
+
}));
|
|
495
|
+
} finally {
|
|
496
|
+
client.release();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async delete(id, namespace) {
|
|
500
|
+
const where = namespace ? "WHERE id = $1 AND namespace = $2" : "WHERE id = $1";
|
|
501
|
+
const params = namespace ? [id, namespace] : [id];
|
|
502
|
+
await this.pool.query(`DELETE FROM ${this.tableName} ${where}`, params);
|
|
503
|
+
}
|
|
504
|
+
async deleteNamespace(namespace) {
|
|
505
|
+
await this.pool.query(`DELETE FROM ${this.tableName} WHERE namespace = $1`, [namespace]);
|
|
506
|
+
}
|
|
507
|
+
async ping() {
|
|
508
|
+
try {
|
|
509
|
+
await this.pool.query("SELECT 1");
|
|
510
|
+
return true;
|
|
511
|
+
} catch (e) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async disconnect() {
|
|
516
|
+
await this.pool.end();
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// src/utils/synonyms.ts
|
|
523
|
+
function resolveMetadataValue(meta, uiKey) {
|
|
524
|
+
var _a;
|
|
525
|
+
const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
|
|
526
|
+
const keys = Object.keys(meta);
|
|
527
|
+
const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
|
|
528
|
+
let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
|
|
529
|
+
if (match !== void 0) return meta[match];
|
|
530
|
+
match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
|
|
531
|
+
if (match !== void 0) return meta[match];
|
|
532
|
+
const isBlacklisted = (kl) => {
|
|
533
|
+
return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
|
|
534
|
+
};
|
|
535
|
+
match = keys.find((k) => {
|
|
536
|
+
const kl = k.toLowerCase();
|
|
537
|
+
if (isBlacklisted(kl)) return false;
|
|
538
|
+
return kl.includes(uiKey.toLowerCase());
|
|
539
|
+
});
|
|
540
|
+
if (match !== void 0) return meta[match];
|
|
541
|
+
match = keys.find((k) => {
|
|
542
|
+
const kl = k.toLowerCase();
|
|
543
|
+
if (isBlacklisted(kl)) return false;
|
|
544
|
+
return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
|
|
545
|
+
});
|
|
546
|
+
return match !== void 0 ? meta[match] : void 0;
|
|
547
|
+
}
|
|
548
|
+
var FIELD_SYNONYMS;
|
|
549
|
+
var init_synonyms = __esm({
|
|
550
|
+
"src/utils/synonyms.ts"() {
|
|
551
|
+
"use strict";
|
|
552
|
+
FIELD_SYNONYMS = {
|
|
553
|
+
name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
|
|
554
|
+
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd", "variant price"],
|
|
555
|
+
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
556
|
+
image: [
|
|
557
|
+
"imageUrl",
|
|
558
|
+
"thumbnail",
|
|
559
|
+
"img",
|
|
560
|
+
"url",
|
|
561
|
+
"photo",
|
|
562
|
+
"picture",
|
|
563
|
+
"media",
|
|
564
|
+
"image_url",
|
|
565
|
+
"main_image",
|
|
566
|
+
"product_image",
|
|
567
|
+
"thumb",
|
|
568
|
+
"image src",
|
|
569
|
+
"variant image"
|
|
570
|
+
],
|
|
571
|
+
stock: [
|
|
572
|
+
"inventory",
|
|
573
|
+
"quantity",
|
|
574
|
+
"count",
|
|
575
|
+
"availability",
|
|
576
|
+
"stock_level",
|
|
577
|
+
"inStock",
|
|
578
|
+
"is_available",
|
|
579
|
+
"in stock",
|
|
580
|
+
"status",
|
|
581
|
+
"variant inventory qty"
|
|
582
|
+
],
|
|
583
|
+
category: [
|
|
584
|
+
"product_category",
|
|
585
|
+
"product category",
|
|
586
|
+
"category_name",
|
|
587
|
+
"category name",
|
|
588
|
+
"department",
|
|
589
|
+
"collection",
|
|
590
|
+
"type"
|
|
591
|
+
],
|
|
592
|
+
description: ["summary", "content", "body", "text", "info", "details", "body (html)", "seo description"],
|
|
593
|
+
link: ["url", "href", "product_url", "page_url", "link"]
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
316
598
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
317
599
|
var MultiTablePostgresProvider_exports = {};
|
|
318
600
|
__export(MultiTablePostgresProvider_exports, {
|
|
319
601
|
MultiTablePostgresProvider: () => MultiTablePostgresProvider
|
|
320
602
|
});
|
|
321
|
-
import { Pool } from "pg";
|
|
603
|
+
import { Pool as Pool2 } from "pg";
|
|
322
604
|
var MultiTablePostgresProvider;
|
|
323
605
|
var init_MultiTablePostgresProvider = __esm({
|
|
324
606
|
"src/providers/vectordb/MultiTablePostgresProvider.ts"() {
|
|
325
607
|
"use strict";
|
|
326
608
|
init_BaseVectorProvider();
|
|
609
|
+
init_synonyms();
|
|
327
610
|
MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
328
611
|
constructor(config) {
|
|
329
612
|
var _a, _b, _c;
|
|
@@ -340,7 +623,7 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
340
623
|
this.uploadTable = opts.uploadTable || "document_chunks";
|
|
341
624
|
}
|
|
342
625
|
async initialize() {
|
|
343
|
-
this.pool = new
|
|
626
|
+
this.pool = new Pool2({ connectionString: this.connectionString });
|
|
344
627
|
const client = await this.pool.connect();
|
|
345
628
|
try {
|
|
346
629
|
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
@@ -404,8 +687,13 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
404
687
|
}
|
|
405
688
|
tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
|
|
406
689
|
const firstMeta = fileDocs[0].metadata || {};
|
|
407
|
-
|
|
408
|
-
|
|
690
|
+
let csvHeaders = [];
|
|
691
|
+
if (Array.isArray(firstMeta.csvHeaders)) {
|
|
692
|
+
csvHeaders = firstMeta.csvHeaders;
|
|
693
|
+
} else {
|
|
694
|
+
const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding", "docId", "docid", "chunkId", "chunkid", "csvHeaders", "csvheaders"];
|
|
695
|
+
csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
|
|
696
|
+
}
|
|
409
697
|
const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
|
|
410
698
|
const createTableSql = `
|
|
411
699
|
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
@@ -484,26 +772,41 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
484
772
|
const allResults = [];
|
|
485
773
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
486
774
|
const queryText = _filter == null ? void 0 : _filter.queryText;
|
|
487
|
-
const entityHints = Array.isArray(_filter == null ? void 0 : _filter.
|
|
488
|
-
(hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
|
|
489
|
-
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
775
|
+
const entityHints = Array.isArray(_filter == null ? void 0 : _filter.keywords) ? _filter.keywords.map((k) => k.trim().toLowerCase()).filter(Boolean) : [];
|
|
490
776
|
console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
|
|
491
777
|
console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
|
|
492
778
|
const getDynamicKeywordQuery = () => {
|
|
493
779
|
if (entityHints.length > 0) {
|
|
494
|
-
return entityHints.map((h) => h.
|
|
780
|
+
return entityHints.map((h) => h.replace(/\s+/g, " & ")).join(" & ");
|
|
495
781
|
}
|
|
496
782
|
if (queryText) {
|
|
497
|
-
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, " & ");
|
|
783
|
+
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, " & ");
|
|
498
784
|
}
|
|
499
785
|
return "";
|
|
500
786
|
};
|
|
501
787
|
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
788
|
+
const tableLimit = Math.max(topK, 50);
|
|
789
|
+
const metadataFilters = _filter == null ? void 0 : _filter.metadata;
|
|
502
790
|
const queryPromises = this.tables.map(async (table) => {
|
|
503
791
|
try {
|
|
504
792
|
let sqlQuery = "";
|
|
505
793
|
let params = [];
|
|
506
|
-
|
|
794
|
+
let whereClause = "";
|
|
795
|
+
const filterParams = [];
|
|
796
|
+
if (metadataFilters && Object.keys(metadataFilters).length > 0) {
|
|
797
|
+
const conditions = Object.entries(metadataFilters).map(([key, val]) => {
|
|
798
|
+
var _a3;
|
|
799
|
+
filterParams.push(String(val));
|
|
800
|
+
const baseOffset = queryText && dynamicKeywordQuery ? 2 : 1;
|
|
801
|
+
const paramIdx = baseOffset + filterParams.length;
|
|
802
|
+
const synonyms = (_a3 = FIELD_SYNONYMS[key]) != null ? _a3 : [];
|
|
803
|
+
const keysToCheck = [key, ...synonyms];
|
|
804
|
+
const coalesceExprs = keysToCheck.map((k) => `LOWER(metadata->>'${k}')`);
|
|
805
|
+
return `COALESCE(${coalesceExprs.join(", ")}) = LOWER($${paramIdx})`;
|
|
806
|
+
});
|
|
807
|
+
whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
808
|
+
}
|
|
809
|
+
if (queryText && dynamicKeywordQuery) {
|
|
507
810
|
const hasEntityHints = entityHints.length > 0;
|
|
508
811
|
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
509
812
|
SELECT COALESCE(MAX(
|
|
@@ -519,19 +822,21 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
519
822
|
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
520
823
|
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
521
824
|
FROM "${table}" t
|
|
825
|
+
${whereClause}
|
|
522
826
|
ORDER BY hybrid_score DESC
|
|
523
|
-
LIMIT
|
|
827
|
+
LIMIT ${tableLimit}
|
|
524
828
|
`;
|
|
525
|
-
params = [vectorLiteral, dynamicKeywordQuery];
|
|
829
|
+
params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
|
|
526
830
|
} else {
|
|
527
831
|
sqlQuery = `
|
|
528
832
|
SELECT *,
|
|
529
|
-
|
|
833
|
+
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
530
834
|
FROM "${table}" t
|
|
835
|
+
${whereClause}
|
|
531
836
|
ORDER BY hybrid_score DESC
|
|
532
|
-
LIMIT
|
|
837
|
+
LIMIT ${tableLimit}
|
|
533
838
|
`;
|
|
534
|
-
params = [vectorLiteral];
|
|
839
|
+
params = [vectorLiteral, ...filterParams];
|
|
535
840
|
}
|
|
536
841
|
const result = await this.pool.query(sqlQuery, params);
|
|
537
842
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -854,13 +1159,25 @@ var init_MilvusProvider = __esm({
|
|
|
854
1159
|
};
|
|
855
1160
|
await this.http.post("/v1/vector/upsert", payload);
|
|
856
1161
|
}
|
|
857
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
858
1162
|
async query(vector, topK, namespace, _filter) {
|
|
1163
|
+
const sanitizedFilter = this.sanitizeFilter(_filter);
|
|
1164
|
+
const filterParts = [];
|
|
1165
|
+
if (namespace) {
|
|
1166
|
+
filterParts.push(`namespace == "${namespace}"`);
|
|
1167
|
+
}
|
|
1168
|
+
for (const [key, value] of Object.entries(sanitizedFilter)) {
|
|
1169
|
+
if (key === "queryText" || key === "keywords") continue;
|
|
1170
|
+
if (typeof value === "string") {
|
|
1171
|
+
filterParts.push(`metadata["${key}"] == "${value.replace(/"/g, '\\"')}"`);
|
|
1172
|
+
} else if (typeof value === "number") {
|
|
1173
|
+
filterParts.push(`metadata["${key}"] == ${value}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
859
1176
|
const payload = {
|
|
860
1177
|
collectionName: this.indexName,
|
|
861
1178
|
vector,
|
|
862
1179
|
limit: topK,
|
|
863
|
-
filter:
|
|
1180
|
+
filter: filterParts.length > 0 ? filterParts.join(" && ") : void 0,
|
|
864
1181
|
outputFields: ["content", "metadata"],
|
|
865
1182
|
searchParams: {
|
|
866
1183
|
nprobe: this.config.options.nprobe || 16,
|
|
@@ -1059,6 +1376,7 @@ var init_QdrantProvider = __esm({
|
|
|
1059
1376
|
await this.http.put(`/collections/${this.indexName}/points`, payload);
|
|
1060
1377
|
}
|
|
1061
1378
|
async query(vector, topK, namespace, _filter) {
|
|
1379
|
+
var _a;
|
|
1062
1380
|
const must = [];
|
|
1063
1381
|
if (namespace) {
|
|
1064
1382
|
must.push({ key: "namespace", match: { value: namespace } });
|
|
@@ -1080,7 +1398,7 @@ var init_QdrantProvider = __esm({
|
|
|
1080
1398
|
limit: topK,
|
|
1081
1399
|
with_payload: true,
|
|
1082
1400
|
params: {
|
|
1083
|
-
hnsw_ef: this.config.options.efSearch || Math.max(topK * 20, 128),
|
|
1401
|
+
hnsw_ef: ((_a = this.config.options) == null ? void 0 : _a.efSearch) || Math.max(topK * 20, 128),
|
|
1084
1402
|
exact: false
|
|
1085
1403
|
},
|
|
1086
1404
|
filter: must.length > 0 ? { must } : void 0
|
|
@@ -1205,12 +1523,20 @@ var init_ChromaDBProvider = __esm({
|
|
|
1205
1523
|
};
|
|
1206
1524
|
await this.http.post(`/api/v1/collections/${this.collectionId}/add`, payload);
|
|
1207
1525
|
}
|
|
1208
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1209
1526
|
async query(vector, topK, namespace, _filter) {
|
|
1527
|
+
const sanitizedFilter = this.sanitizeFilter(_filter);
|
|
1528
|
+
const whereClauses = [];
|
|
1529
|
+
if (namespace) whereClauses.push({ namespace: { $eq: namespace } });
|
|
1530
|
+
Object.entries(sanitizedFilter).forEach(([key, value]) => {
|
|
1531
|
+
if (key === "namespace") return;
|
|
1532
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1533
|
+
whereClauses.push({ [key]: { $eq: value } });
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1210
1536
|
const payload = {
|
|
1211
1537
|
query_embeddings: [vector],
|
|
1212
1538
|
n_results: topK,
|
|
1213
|
-
where:
|
|
1539
|
+
where: whereClauses.length > 1 ? { $and: whereClauses } : whereClauses[0]
|
|
1214
1540
|
};
|
|
1215
1541
|
const { data } = await this.http.post(`/api/v1/collections/${this.collectionId}/query`, payload);
|
|
1216
1542
|
const matches = [];
|
|
@@ -1315,15 +1641,15 @@ var init_RedisProvider = __esm({
|
|
|
1315
1641
|
console.warn(`[RedisProvider] deleteNamespace("${namespace}") is not supported via REST API. Use Redis CLI: SCAN + DEL`);
|
|
1316
1642
|
}
|
|
1317
1643
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
* Returns
|
|
1644
|
+
* Check reachability via a PING command.
|
|
1645
|
+
* Returns false on connection failure so health checks surface real problems.
|
|
1320
1646
|
*/
|
|
1321
1647
|
async ping() {
|
|
1322
1648
|
try {
|
|
1323
1649
|
await this.http.post("/", ["PING"]);
|
|
1324
1650
|
return true;
|
|
1325
1651
|
} catch (e) {
|
|
1326
|
-
return
|
|
1652
|
+
return false;
|
|
1327
1653
|
}
|
|
1328
1654
|
}
|
|
1329
1655
|
async disconnect() {
|
|
@@ -1363,15 +1689,17 @@ var init_WeaviateProvider = __esm({
|
|
|
1363
1689
|
await this.ping();
|
|
1364
1690
|
}
|
|
1365
1691
|
async upsert(doc, namespace) {
|
|
1692
|
+
const primitiveMetadata = this.extractPrimitiveMetadata(doc.metadata);
|
|
1366
1693
|
const payload = {
|
|
1367
1694
|
class: this.indexName,
|
|
1368
1695
|
id: doc.id,
|
|
1369
1696
|
vector: doc.vector,
|
|
1370
|
-
properties: {
|
|
1697
|
+
properties: __spreadProps(__spreadValues({
|
|
1371
1698
|
content: doc.content,
|
|
1372
|
-
metadata: JSON.stringify(doc.metadata || {})
|
|
1699
|
+
metadata: JSON.stringify(doc.metadata || {})
|
|
1700
|
+
}, primitiveMetadata), {
|
|
1373
1701
|
namespace: namespace || ""
|
|
1374
|
-
}
|
|
1702
|
+
})
|
|
1375
1703
|
};
|
|
1376
1704
|
await this.http.post("/v1/objects", payload);
|
|
1377
1705
|
}
|
|
@@ -1381,20 +1709,22 @@ var init_WeaviateProvider = __esm({
|
|
|
1381
1709
|
class: this.indexName,
|
|
1382
1710
|
id: doc.id,
|
|
1383
1711
|
vector: doc.vector,
|
|
1384
|
-
properties: {
|
|
1712
|
+
properties: __spreadProps(__spreadValues({
|
|
1385
1713
|
content: doc.content,
|
|
1386
|
-
metadata: JSON.stringify(doc.metadata || {})
|
|
1714
|
+
metadata: JSON.stringify(doc.metadata || {})
|
|
1715
|
+
}, this.extractPrimitiveMetadata(doc.metadata)), {
|
|
1387
1716
|
namespace: namespace || ""
|
|
1388
|
-
}
|
|
1717
|
+
})
|
|
1389
1718
|
}))
|
|
1390
1719
|
};
|
|
1391
1720
|
await this.http.post("/v1/batch/objects", payload);
|
|
1392
1721
|
}
|
|
1393
1722
|
async query(vector, topK, namespace, _filter) {
|
|
1394
1723
|
var _a, _b;
|
|
1724
|
+
const queryText = _filter == null ? void 0 : _filter.queryText;
|
|
1395
1725
|
const sanitizedFilter = this.sanitizeFilter(_filter);
|
|
1396
|
-
const queryText = sanitizedFilter.queryText;
|
|
1397
1726
|
const searchParams = queryText ? `hybrid: { query: ${JSON.stringify(queryText)}, alpha: 0.5 }` : `nearVector: { vector: ${JSON.stringify(vector)} }`;
|
|
1727
|
+
const where = this.buildWhereFilter(namespace, sanitizedFilter);
|
|
1398
1728
|
const graphqlQuery = {
|
|
1399
1729
|
query: `
|
|
1400
1730
|
{
|
|
@@ -1402,7 +1732,7 @@ var init_WeaviateProvider = __esm({
|
|
|
1402
1732
|
${this.indexName}(
|
|
1403
1733
|
${searchParams}
|
|
1404
1734
|
limit: ${topK}
|
|
1405
|
-
${
|
|
1735
|
+
${where ? `where: ${where}` : ""}
|
|
1406
1736
|
) {
|
|
1407
1737
|
content
|
|
1408
1738
|
metadata
|
|
@@ -1446,6 +1776,34 @@ var init_WeaviateProvider = __esm({
|
|
|
1446
1776
|
}
|
|
1447
1777
|
async disconnect() {
|
|
1448
1778
|
}
|
|
1779
|
+
extractPrimitiveMetadata(metadata) {
|
|
1780
|
+
const result = {};
|
|
1781
|
+
Object.entries(metadata || {}).forEach(([key, value]) => {
|
|
1782
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1783
|
+
result[key] = value;
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
return result;
|
|
1787
|
+
}
|
|
1788
|
+
buildWhereFilter(namespace, filter) {
|
|
1789
|
+
const operands = [];
|
|
1790
|
+
if (namespace) operands.push(this.weaviateOperand("namespace", namespace));
|
|
1791
|
+
Object.entries(filter || {}).forEach(([key, value]) => {
|
|
1792
|
+
if (key === "namespace") return;
|
|
1793
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1794
|
+
operands.push(this.weaviateOperand(key, value));
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
if (operands.length === 0) return void 0;
|
|
1798
|
+
if (operands.length === 1) return operands[0];
|
|
1799
|
+
return `{ operator: And, operands: [${operands.join(", ")}] }`;
|
|
1800
|
+
}
|
|
1801
|
+
weaviateOperand(key, value) {
|
|
1802
|
+
const path = `path: [${JSON.stringify(key)}], operator: Equal`;
|
|
1803
|
+
if (typeof value === "number") return `{ ${path}, valueNumber: ${value} }`;
|
|
1804
|
+
if (typeof value === "boolean") return `{ ${path}, valueBoolean: ${value} }`;
|
|
1805
|
+
return `{ ${path}, valueString: ${JSON.stringify(value)} }`;
|
|
1806
|
+
}
|
|
1449
1807
|
};
|
|
1450
1808
|
}
|
|
1451
1809
|
});
|
|
@@ -1751,7 +2109,7 @@ function readEnum(env, name, fallback, allowed) {
|
|
|
1751
2109
|
throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(", ")}`);
|
|
1752
2110
|
}
|
|
1753
2111
|
function getEnvConfig(env = process.env, base) {
|
|
1754
|
-
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;
|
|
2112
|
+
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;
|
|
1755
2113
|
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__";
|
|
1756
2114
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
1757
2115
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -1805,11 +2163,22 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1805
2163
|
};
|
|
1806
2164
|
const embeddingApiKeyByProvider = {
|
|
1807
2165
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
2166
|
+
anthropic: readString(env, "ANTHROPIC_API_KEY"),
|
|
2167
|
+
// Anthropic needs a separate embedding provider; key kept for completeness
|
|
1808
2168
|
gemini: readString(env, "GEMINI_API_KEY"),
|
|
1809
2169
|
ollama: void 0,
|
|
1810
2170
|
universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
|
|
1811
2171
|
custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
|
|
1812
2172
|
};
|
|
2173
|
+
const DEFAULT_MODEL_BY_PROVIDER = {
|
|
2174
|
+
openai: "gpt-4o",
|
|
2175
|
+
anthropic: "claude-3-5-sonnet-20241022",
|
|
2176
|
+
gemini: "gemini-2.0-flash",
|
|
2177
|
+
ollama: "llama3",
|
|
2178
|
+
universal_rest: "default",
|
|
2179
|
+
rest: "default",
|
|
2180
|
+
custom: "default"
|
|
2181
|
+
};
|
|
1813
2182
|
return {
|
|
1814
2183
|
projectId,
|
|
1815
2184
|
vectorDb: {
|
|
@@ -1819,8 +2188,8 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1819
2188
|
},
|
|
1820
2189
|
llm: {
|
|
1821
2190
|
provider: llmProvider,
|
|
1822
|
-
model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
|
|
1823
|
-
apiKey: (
|
|
2191
|
+
model: (_ja = (_ia = readString(env, "LLM_MODEL")) != null ? _ia : DEFAULT_MODEL_BY_PROVIDER[llmProvider]) != null ? _ja : "gpt-4o",
|
|
2192
|
+
apiKey: (_ka = llmApiKeyByProvider[llmProvider]) != null ? _ka : "",
|
|
1824
2193
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
1825
2194
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
1826
2195
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
|
|
@@ -1831,7 +2200,7 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1831
2200
|
},
|
|
1832
2201
|
embedding: {
|
|
1833
2202
|
provider: embeddingProvider,
|
|
1834
|
-
model: (
|
|
2203
|
+
model: (_la = readString(env, "EMBEDDING_MODEL")) != null ? _la : "text-embedding-3-small",
|
|
1835
2204
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
1836
2205
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
1837
2206
|
dimensions: embeddingDimensions,
|
|
@@ -1842,17 +2211,17 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1842
2211
|
}
|
|
1843
2212
|
},
|
|
1844
2213
|
ui: {
|
|
1845
|
-
title: (
|
|
1846
|
-
subtitle: (
|
|
1847
|
-
primaryColor: (
|
|
1848
|
-
accentColor: (
|
|
1849
|
-
logoUrl: (
|
|
1850
|
-
placeholder: (
|
|
1851
|
-
showSources: ((
|
|
1852
|
-
welcomeMessage: (
|
|
1853
|
-
visualStyle: (
|
|
1854
|
-
borderRadius: (
|
|
1855
|
-
allowUpload: ((
|
|
2214
|
+
title: (_na = (_ma = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _ma : readString(env, "UI_TITLE")) != null ? _na : "AI Assistant",
|
|
2215
|
+
subtitle: (_pa = (_oa = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _oa : readString(env, "UI_SUBTITLE")) != null ? _pa : "Powered by RAG",
|
|
2216
|
+
primaryColor: (_ra = (_qa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _qa : readString(env, "UI_PRIMARY_COLOR")) != null ? _ra : "#10b981",
|
|
2217
|
+
accentColor: (_ta = (_sa = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _sa : readString(env, "UI_ACCENT_COLOR")) != null ? _ta : "#3b82f6",
|
|
2218
|
+
logoUrl: (_ua = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ua : readString(env, "UI_LOGO_URL"),
|
|
2219
|
+
placeholder: (_wa = (_va = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _va : readString(env, "UI_PLACEHOLDER")) != null ? _wa : "Ask me anything\u2026",
|
|
2220
|
+
showSources: ((_ya = (_xa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _xa : readString(env, "UI_SHOW_SOURCES")) != null ? _ya : "true") !== "false",
|
|
2221
|
+
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.",
|
|
2222
|
+
visualStyle: (_Ca = (_Ba = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Ba : readString(env, "UI_VISUAL_STYLE")) != null ? _Ca : "glass",
|
|
2223
|
+
borderRadius: (_Ea = (_Da = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Da : readString(env, "UI_BORDER_RADIUS")) != null ? _Ea : "xl",
|
|
2224
|
+
allowUpload: ((_Ga = (_Fa = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Fa : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Ga : "false") === "true"
|
|
1856
2225
|
},
|
|
1857
2226
|
rag: {
|
|
1858
2227
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
@@ -2714,7 +3083,7 @@ var LLM_PROFILES = {
|
|
|
2714
3083
|
// src/llm/providers/UniversalLLMAdapter.ts
|
|
2715
3084
|
var UniversalLLMAdapter = class {
|
|
2716
3085
|
constructor(config) {
|
|
2717
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
3086
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2718
3087
|
this.model = config.model;
|
|
2719
3088
|
const llmConfig = config;
|
|
2720
3089
|
const options = (_a = llmConfig.options) != null ? _a : {};
|
|
@@ -2728,16 +3097,18 @@ var UniversalLLMAdapter = class {
|
|
|
2728
3097
|
this.systemPrompt = (_c = llmConfig.systemPrompt) != null ? _c : "You are a helpful AI assistant. Use the provided context to answer the user.";
|
|
2729
3098
|
this.maxTokens = (_d = llmConfig.maxTokens) != null ? _d : 1024;
|
|
2730
3099
|
this.temperature = (_e = llmConfig.temperature) != null ? _e : 0;
|
|
2731
|
-
|
|
2732
|
-
|
|
3100
|
+
this.apiKey = config.apiKey;
|
|
3101
|
+
this.baseUrl = (_g = (_f = llmConfig.baseUrl) != null ? _f : this.opts.baseUrl) != null ? _g : "";
|
|
3102
|
+
if (!this.baseUrl) {
|
|
2733
3103
|
throw new Error("[UniversalLLMAdapter] baseUrl is required in config or config.options");
|
|
2734
3104
|
}
|
|
3105
|
+
this.resolvedHeaders = __spreadValues(__spreadValues({
|
|
3106
|
+
"Content-Type": "application/json"
|
|
3107
|
+
}, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {});
|
|
2735
3108
|
this.http = axios2.create({
|
|
2736
|
-
baseURL: baseUrl,
|
|
2737
|
-
headers:
|
|
2738
|
-
|
|
2739
|
-
}, config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), this.opts.headers || {}),
|
|
2740
|
-
timeout: (_g = this.opts.timeout) != null ? _g : 6e4
|
|
3109
|
+
baseURL: this.baseUrl,
|
|
3110
|
+
headers: this.resolvedHeaders,
|
|
3111
|
+
timeout: (_h = this.opts.timeout) != null ? _h : 6e4
|
|
2741
3112
|
});
|
|
2742
3113
|
}
|
|
2743
3114
|
async chat(messages, context) {
|
|
@@ -2774,6 +3145,92 @@ ${context != null ? context : "None"}` },
|
|
|
2774
3145
|
}
|
|
2775
3146
|
return String(result);
|
|
2776
3147
|
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Streaming chat using native fetch + ReadableStream.
|
|
3150
|
+
* Parses OpenAI-compatible SSE frames: `data: {...}\n\n`
|
|
3151
|
+
* Works with vLLM, LMStudio, Together AI, Fireworks, and any OpenAI-compatible API.
|
|
3152
|
+
*/
|
|
3153
|
+
chatStream(messages, context) {
|
|
3154
|
+
return __asyncGenerator(this, null, function* () {
|
|
3155
|
+
var _a, _b, _c;
|
|
3156
|
+
const path = (_a = this.opts.chatPath) != null ? _a : "/chat/completions";
|
|
3157
|
+
const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
|
|
3158
|
+
const extractPath = ((_b = this.opts.responseExtractPath) != null ? _b : "choices[0].message.content").replace("message.content", "delta.content");
|
|
3159
|
+
const formattedMessages = [
|
|
3160
|
+
{ role: "system", content: `${this.systemPrompt}
|
|
3161
|
+
|
|
3162
|
+
Context:
|
|
3163
|
+
${context != null ? context : "None"}` },
|
|
3164
|
+
...messages
|
|
3165
|
+
];
|
|
3166
|
+
let payload;
|
|
3167
|
+
if (this.opts.chatPayloadTemplate) {
|
|
3168
|
+
payload = buildPayload(this.opts.chatPayloadTemplate, {
|
|
3169
|
+
model: this.model,
|
|
3170
|
+
messages: formattedMessages,
|
|
3171
|
+
maxTokens: this.maxTokens,
|
|
3172
|
+
temperature: this.temperature
|
|
3173
|
+
});
|
|
3174
|
+
if (typeof payload === "object" && payload !== null) {
|
|
3175
|
+
payload.stream = true;
|
|
3176
|
+
}
|
|
3177
|
+
} else {
|
|
3178
|
+
payload = {
|
|
3179
|
+
model: this.model,
|
|
3180
|
+
messages: formattedMessages,
|
|
3181
|
+
max_tokens: this.maxTokens,
|
|
3182
|
+
temperature: this.temperature,
|
|
3183
|
+
stream: true
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
const response = yield new __await(fetch(url, {
|
|
3187
|
+
method: "POST",
|
|
3188
|
+
headers: this.resolvedHeaders,
|
|
3189
|
+
body: JSON.stringify(payload)
|
|
3190
|
+
}));
|
|
3191
|
+
if (!response.ok) {
|
|
3192
|
+
const errorText = yield new __await(response.text().catch(() => response.statusText));
|
|
3193
|
+
throw new Error(`[UniversalLLMAdapter] Streaming request failed (${response.status}): ${errorText}`);
|
|
3194
|
+
}
|
|
3195
|
+
if (!response.body) {
|
|
3196
|
+
throw new Error("[UniversalLLMAdapter] Response body is null \u2014 server did not send a streaming response.");
|
|
3197
|
+
}
|
|
3198
|
+
const reader = response.body.getReader();
|
|
3199
|
+
const decoder = new TextDecoder("utf-8");
|
|
3200
|
+
let buffer = "";
|
|
3201
|
+
try {
|
|
3202
|
+
while (true) {
|
|
3203
|
+
const { done, value } = yield new __await(reader.read());
|
|
3204
|
+
if (done) break;
|
|
3205
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3206
|
+
const lines = buffer.split("\n");
|
|
3207
|
+
buffer = (_c = lines.pop()) != null ? _c : "";
|
|
3208
|
+
for (const line of lines) {
|
|
3209
|
+
const trimmed = line.trim();
|
|
3210
|
+
if (!trimmed || trimmed === "data: [DONE]") continue;
|
|
3211
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
3212
|
+
try {
|
|
3213
|
+
const json = JSON.parse(trimmed.slice(5).trim());
|
|
3214
|
+
const text = resolvePath(json, extractPath);
|
|
3215
|
+
if (text && typeof text === "string") yield text;
|
|
3216
|
+
} catch (e) {
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
if (buffer.trim() && buffer.trim() !== "data: [DONE]") {
|
|
3221
|
+
const jsonStr = buffer.replace(/^data:\s*/, "").trim();
|
|
3222
|
+
try {
|
|
3223
|
+
const json = JSON.parse(jsonStr);
|
|
3224
|
+
const text = resolvePath(json, extractPath);
|
|
3225
|
+
if (text && typeof text === "string") yield text;
|
|
3226
|
+
} catch (e) {
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
} finally {
|
|
3230
|
+
reader.releaseLock();
|
|
3231
|
+
}
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
2777
3234
|
async embed(text) {
|
|
2778
3235
|
var _a, _b;
|
|
2779
3236
|
const path = (_a = this.opts.embedPath) != null ? _a : "/embeddings";
|
|
@@ -2970,6 +3427,7 @@ var ProviderRegistry = class {
|
|
|
2970
3427
|
return null;
|
|
2971
3428
|
}
|
|
2972
3429
|
static async loadVectorProviderClass(provider) {
|
|
3430
|
+
var _a;
|
|
2973
3431
|
if (this.vectorProviders[provider]) return this.vectorProviders[provider];
|
|
2974
3432
|
switch (provider) {
|
|
2975
3433
|
case "pinecone": {
|
|
@@ -2978,6 +3436,11 @@ var ProviderRegistry = class {
|
|
|
2978
3436
|
}
|
|
2979
3437
|
case "pgvector":
|
|
2980
3438
|
case "postgresql": {
|
|
3439
|
+
const postgresMode = ((_a = process.env.POSTGRES_MODE) != null ? _a : "multi").toLowerCase();
|
|
3440
|
+
if (postgresMode === "single") {
|
|
3441
|
+
const { PostgreSQLProvider: PostgreSQLProvider2 } = await Promise.resolve().then(() => (init_PostgreSQLProvider(), PostgreSQLProvider_exports));
|
|
3442
|
+
return PostgreSQLProvider2;
|
|
3443
|
+
}
|
|
2981
3444
|
const { MultiTablePostgresProvider: MultiTablePostgresProvider2 } = await Promise.resolve().then(() => (init_MultiTablePostgresProvider(), MultiTablePostgresProvider_exports));
|
|
2982
3445
|
return MultiTablePostgresProvider2;
|
|
2983
3446
|
}
|
|
@@ -3953,7 +4416,8 @@ var QueryProcessor = class {
|
|
|
3953
4416
|
const fieldValuePatterns = [
|
|
3954
4417
|
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
3955
4418
|
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
3956
|
-
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
4419
|
+
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
4420
|
+
new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
3957
4421
|
];
|
|
3958
4422
|
for (const pattern of fieldValuePatterns) {
|
|
3959
4423
|
for (const match of question.matchAll(pattern)) {
|
|
@@ -3971,6 +4435,76 @@ var QueryProcessor = class {
|
|
|
3971
4435
|
}
|
|
3972
4436
|
return [...hints.values()];
|
|
3973
4437
|
}
|
|
4438
|
+
static extractNumericPredicates(question, validFields = []) {
|
|
4439
|
+
const predicates = [];
|
|
4440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4441
|
+
const comparatorPattern = [
|
|
4442
|
+
"greater than or equal to",
|
|
4443
|
+
"more than or equal to",
|
|
4444
|
+
"less than or equal to",
|
|
4445
|
+
"greater than",
|
|
4446
|
+
"more than",
|
|
4447
|
+
"less than",
|
|
4448
|
+
"equal to",
|
|
4449
|
+
"at least",
|
|
4450
|
+
"at most",
|
|
4451
|
+
"above",
|
|
4452
|
+
"over",
|
|
4453
|
+
"below",
|
|
4454
|
+
"under",
|
|
4455
|
+
"equals?",
|
|
4456
|
+
">=",
|
|
4457
|
+
"<=",
|
|
4458
|
+
">",
|
|
4459
|
+
"<",
|
|
4460
|
+
"="
|
|
4461
|
+
].join("|");
|
|
4462
|
+
const addPredicate = (rawField, rawOperator, rawValue) => {
|
|
4463
|
+
const value = Number(rawValue.replace(/,/g, ""));
|
|
4464
|
+
if (!Number.isFinite(value)) return;
|
|
4465
|
+
const operator = this.normalizeNumericOperator(rawOperator);
|
|
4466
|
+
const field = rawField ? this.normalizePredicateField(rawField, validFields) : void 0;
|
|
4467
|
+
const key = `${field != null ? field : "*"}::${operator}::${value}`;
|
|
4468
|
+
if (seen.has(key)) return;
|
|
4469
|
+
seen.add(key);
|
|
4470
|
+
predicates.push(__spreadProps(__spreadValues({}, field ? { field } : {}), { operator, value }));
|
|
4471
|
+
};
|
|
4472
|
+
const scopedPatterns = [
|
|
4473
|
+
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"),
|
|
4474
|
+
new RegExp(`\\b([a-zA-Z][a-zA-Z0-9_\\s\\-/]{1,80}?)\\s+(?:is|are|was|were)?\\s*(?:${comparatorPattern})\\s+([\\d,]+(?:\\.\\d+)?)`, "gi")
|
|
4475
|
+
];
|
|
4476
|
+
for (const pattern of scopedPatterns) {
|
|
4477
|
+
for (const match of question.matchAll(pattern)) {
|
|
4478
|
+
const full = match[0];
|
|
4479
|
+
const operatorMatch = full.match(new RegExp(`(${comparatorPattern})`, "i"));
|
|
4480
|
+
if (!operatorMatch) continue;
|
|
4481
|
+
addPredicate(match[1], operatorMatch[1], match[2]);
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
for (const match of question.matchAll(/\b([a-zA-Z][a-zA-Z0-9_\s\-/]{1,80}?)\s*(>=|<=|>|<|=)\s*([\d,]+(?:\.\d+)?)/g)) {
|
|
4485
|
+
addPredicate(match[1], match[2], match[3]);
|
|
4486
|
+
}
|
|
4487
|
+
return predicates;
|
|
4488
|
+
}
|
|
4489
|
+
static normalizeNumericOperator(operator) {
|
|
4490
|
+
const op = operator.toLowerCase().trim();
|
|
4491
|
+
if (op === ">" || /\b(greater than|more than|above|over)\b/.test(op)) return "gt";
|
|
4492
|
+
if (op === ">=" || /\b(greater than or equal to|more than or equal to|at least)\b/.test(op)) return "gte";
|
|
4493
|
+
if (op === "<" || /\b(less than|below|under)\b/.test(op)) return "lt";
|
|
4494
|
+
if (op === "<=" || /\b(less than or equal to|at most)\b/.test(op)) return "lte";
|
|
4495
|
+
return "eq";
|
|
4496
|
+
}
|
|
4497
|
+
static normalizePredicateField(field, validFields) {
|
|
4498
|
+
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();
|
|
4499
|
+
if (validFields.length === 0) return cleaned;
|
|
4500
|
+
const comparable = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4501
|
+
const cleanedComparable = comparable(cleaned);
|
|
4502
|
+
const matchedField = validFields.find((fieldName) => {
|
|
4503
|
+
const candidate = comparable(fieldName);
|
|
4504
|
+
return candidate === cleanedComparable || candidate.includes(cleanedComparable) || cleanedComparable.includes(candidate);
|
|
4505
|
+
});
|
|
4506
|
+
return matchedField != null ? matchedField : cleaned;
|
|
4507
|
+
}
|
|
3974
4508
|
/**
|
|
3975
4509
|
* Constructs a QueryFilter object from extracted hints.
|
|
3976
4510
|
*
|
|
@@ -3991,6 +4525,10 @@ var QueryProcessor = class {
|
|
|
3991
4525
|
}
|
|
3992
4526
|
if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
|
|
3993
4527
|
if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
|
|
4528
|
+
const numericPredicates = this.extractNumericPredicates(question);
|
|
4529
|
+
if (numericPredicates.length > 0) {
|
|
4530
|
+
filter.__numericPredicates = numericPredicates;
|
|
4531
|
+
}
|
|
3994
4532
|
return filter;
|
|
3995
4533
|
}
|
|
3996
4534
|
/**
|
|
@@ -4058,102 +4596,360 @@ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
|
|
|
4058
4596
|
}
|
|
4059
4597
|
};
|
|
4060
4598
|
|
|
4061
|
-
// src/
|
|
4062
|
-
var
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
"photo",
|
|
4072
|
-
"picture",
|
|
4073
|
-
"media",
|
|
4074
|
-
"image_url",
|
|
4075
|
-
"main_image",
|
|
4076
|
-
"product_image",
|
|
4077
|
-
"thumb"
|
|
4078
|
-
],
|
|
4079
|
-
stock: [
|
|
4080
|
-
"inventory",
|
|
4081
|
-
"quantity",
|
|
4082
|
-
"count",
|
|
4083
|
-
"availability",
|
|
4084
|
-
"stock_level",
|
|
4085
|
-
"inStock",
|
|
4086
|
-
"is_available",
|
|
4087
|
-
"in stock",
|
|
4088
|
-
"status"
|
|
4089
|
-
],
|
|
4090
|
-
description: ["summary", "content", "body", "text", "info", "details"],
|
|
4091
|
-
link: ["url", "href", "product_url", "page_url", "link"]
|
|
4599
|
+
// src/core/LLMRouter.ts
|
|
4600
|
+
var FAST_MODEL_DEFAULTS = {
|
|
4601
|
+
openai: "gpt-4o-mini",
|
|
4602
|
+
gemini: "gemini-2.0-flash",
|
|
4603
|
+
anthropic: "claude-3-haiku-20240307",
|
|
4604
|
+
ollama: "",
|
|
4605
|
+
// Ollama has no universal lightweight default — reuse main model
|
|
4606
|
+
rest: "",
|
|
4607
|
+
universal_rest: "",
|
|
4608
|
+
custom: ""
|
|
4092
4609
|
};
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
const
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4610
|
+
var LLMRouter = class {
|
|
4611
|
+
constructor(config) {
|
|
4612
|
+
this.config = config;
|
|
4613
|
+
this.models = /* @__PURE__ */ new Map();
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Initialize all LLM roles.
|
|
4617
|
+
*
|
|
4618
|
+
* @param prebuiltDefault - optional pre-built provider (from EmbeddingStrategyResolver).
|
|
4619
|
+
* When provided it is used directly as the 'default' role without re-constructing.
|
|
4620
|
+
*/
|
|
4621
|
+
async initialize(prebuiltDefault) {
|
|
4622
|
+
var _a;
|
|
4623
|
+
const defaultModel = prebuiltDefault != null ? prebuiltDefault : LLMFactory.create(this.config.llm, this.config.embedding);
|
|
4624
|
+
this.models.set("default", defaultModel);
|
|
4625
|
+
const envFastModel = process.env.FAST_LLM_MODEL;
|
|
4626
|
+
const providerFastDefault = (_a = FAST_MODEL_DEFAULTS[this.config.llm.provider]) != null ? _a : "";
|
|
4627
|
+
const fastModelName = envFastModel || providerFastDefault;
|
|
4628
|
+
if (fastModelName && fastModelName !== this.config.llm.model) {
|
|
4629
|
+
console.log(`[LLMRouter] Fast role \u2192 provider="${this.config.llm.provider}" model="${fastModelName}"`);
|
|
4630
|
+
const fastConfig = __spreadProps(__spreadValues({}, this.config.llm), {
|
|
4631
|
+
model: fastModelName
|
|
4632
|
+
});
|
|
4633
|
+
this.models.set("fast", LLMFactory.create(fastConfig, this.config.embedding));
|
|
4634
|
+
} else {
|
|
4635
|
+
console.log(`[LLMRouter] Fast role \u2192 reusing default model (no lightweight alternative configured).`);
|
|
4636
|
+
this.models.set("fast", defaultModel);
|
|
4637
|
+
}
|
|
4638
|
+
this.models.set("powerful", defaultModel);
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Retrieve a model provider by its task role.
|
|
4642
|
+
* Falls back to 'default' if the requested role is not registered.
|
|
4643
|
+
*/
|
|
4644
|
+
get(role) {
|
|
4645
|
+
var _a;
|
|
4646
|
+
const provider = (_a = this.models.get(role)) != null ? _a : this.models.get("default");
|
|
4647
|
+
if (!provider) {
|
|
4648
|
+
throw new Error(`[LLMRouter] No provider registered for role: "${role}". Did you call initialize()?`);
|
|
4649
|
+
}
|
|
4650
|
+
return provider;
|
|
4651
|
+
}
|
|
4652
|
+
};
|
|
4653
|
+
|
|
4654
|
+
// src/utils/UITransformer.ts
|
|
4655
|
+
init_synonyms();
|
|
4656
|
+
var UITransformer = class {
|
|
4657
|
+
// ─── Public Entry Points ─────────────────────────────────────────────────
|
|
4658
|
+
/**
|
|
4659
|
+
* Heuristic-only transform (no LLM required).
|
|
4660
|
+
* Uses the lightweight heuristic intent detector as a fallback.
|
|
4661
|
+
* Prefer `analyzeAndDecide()` in production.
|
|
4124
4662
|
*/
|
|
4125
|
-
static transform(userQuery, retrievedData, config, trainedSchema) {
|
|
4663
|
+
static transform(userQuery, retrievedData, config, trainedSchema, intent) {
|
|
4664
|
+
var _a, _b, _c;
|
|
4126
4665
|
if (!retrievedData || retrievedData.length === 0) {
|
|
4127
4666
|
return this.createTextResponse("No data available", "No relevant data found for your query.");
|
|
4128
4667
|
}
|
|
4129
|
-
const
|
|
4130
|
-
const filteredData =
|
|
4131
|
-
const
|
|
4668
|
+
const resolvedIntent = intent != null ? intent : this.detectIntentHeuristic(userQuery);
|
|
4669
|
+
const filteredData = resolvedIntent.filterInStockOnly ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
|
|
4670
|
+
const profile = this.profileData(filteredData);
|
|
4132
4671
|
const hasProducts = filteredData.some((item) => this.isProductData(item));
|
|
4133
|
-
const
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
return this.transformToLineChart(filteredData);
|
|
4672
|
+
const wantsPieLikeChart = ["pie_chart", "donut_chart"].includes(resolvedIntent.recommendedChart);
|
|
4673
|
+
if (resolvedIntent.visualizationHint === "trend" && profile.dateFields.length > 0 && profile.numericFields.length > 0) {
|
|
4674
|
+
return this.transformToLineChart(profile);
|
|
4137
4675
|
}
|
|
4138
|
-
if (
|
|
4139
|
-
|
|
4676
|
+
if (wantsPieLikeChart || ["composition", "category_breakdown"].includes(resolvedIntent.visualizationHint)) {
|
|
4677
|
+
const pieChart = this.transformToPieChart(filteredData, profile, userQuery);
|
|
4678
|
+
if (pieChart) return pieChart;
|
|
4140
4679
|
}
|
|
4141
|
-
if (
|
|
4142
|
-
return this.
|
|
4680
|
+
if (["comparison", "ranking"].includes(resolvedIntent.visualizationHint)) {
|
|
4681
|
+
return this.transformToBarChart(filteredData, profile, userQuery, resolvedIntent.visualizationHint === "ranking");
|
|
4143
4682
|
}
|
|
4144
|
-
if (
|
|
4145
|
-
return this.
|
|
4683
|
+
if (resolvedIntent.visualizationHint === "distribution") {
|
|
4684
|
+
return (_a = this.transformToHistogram(profile, userQuery)) != null ? _a : this.transformToBarChart(filteredData, profile, userQuery);
|
|
4685
|
+
}
|
|
4686
|
+
if (resolvedIntent.visualizationHint === "correlation") {
|
|
4687
|
+
return (_b = this.transformToScatterPlot(profile, userQuery)) != null ? _b : this.transformToBarChart(filteredData, profile, userQuery);
|
|
4688
|
+
}
|
|
4689
|
+
if (resolvedIntent.visualizationHint === "geographic") {
|
|
4690
|
+
return this.transformToBarChart(filteredData, profile, userQuery);
|
|
4691
|
+
}
|
|
4692
|
+
if (this.isStructuredListQuery(userQuery)) {
|
|
4693
|
+
return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
|
|
4694
|
+
}
|
|
4695
|
+
if (resolvedIntent.visualizationHint === "kpi") {
|
|
4696
|
+
return (_c = this.transformToMetricCard(profile, userQuery)) != null ? _c : this.transformToText(filteredData);
|
|
4146
4697
|
}
|
|
4147
|
-
if (
|
|
4148
|
-
return this.transformToTable(filteredData);
|
|
4698
|
+
if (["tabular", "table"].includes(resolvedIntent.visualizationHint) || resolvedIntent.wantsExplicitTable) {
|
|
4699
|
+
return this.hasMultipleFields(filteredData) ? this.transformToTable(filteredData, userQuery) : this.transformToText(filteredData);
|
|
4149
4700
|
}
|
|
4701
|
+
if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === "product_browse") {
|
|
4702
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
4703
|
+
}
|
|
4704
|
+
const automatic = this.chooseAutomaticVisualization(filteredData, profile, userQuery);
|
|
4705
|
+
if (automatic) return automatic;
|
|
4150
4706
|
return this.transformToText(filteredData);
|
|
4151
4707
|
}
|
|
4152
4708
|
/**
|
|
4153
|
-
*
|
|
4709
|
+
* LLM-driven entry point (recommended for production).
|
|
4710
|
+
*
|
|
4711
|
+
* Step 1 — Detect intent via a dedicated, lightweight LLM call.
|
|
4712
|
+
* Step 2 — Pass the intent + data to the visualization-selection prompt.
|
|
4713
|
+
* Step 3 — Fall back to the heuristic `transform()` if either LLM call fails.
|
|
4714
|
+
*/
|
|
4715
|
+
static async analyzeAndDecide(query, sources, llm) {
|
|
4716
|
+
let intent;
|
|
4717
|
+
try {
|
|
4718
|
+
intent = await this.detectIntent(query, llm);
|
|
4719
|
+
console.debug("[UITransformer] Detected intent:", intent);
|
|
4720
|
+
} catch (err) {
|
|
4721
|
+
console.warn("[UITransformer] Intent detection failed; using heuristic.", err);
|
|
4722
|
+
intent = this.detectIntentHeuristic(query);
|
|
4723
|
+
}
|
|
4724
|
+
if (this.isProductQuery(query) && ["text", "product_browse"].includes(intent.visualizationHint)) {
|
|
4725
|
+
return this.transform(
|
|
4726
|
+
query,
|
|
4727
|
+
sources,
|
|
4728
|
+
void 0,
|
|
4729
|
+
void 0,
|
|
4730
|
+
__spreadProps(__spreadValues({}, intent), { visualizationHint: "product_browse", recommendedChart: "text" })
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
try {
|
|
4734
|
+
const context = this.buildContextSummary(sources);
|
|
4735
|
+
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
4736
|
+
const userPrompt = [
|
|
4737
|
+
`USER QUESTION: ${query}`,
|
|
4738
|
+
"",
|
|
4739
|
+
`DETECTED INTENT (JSON): ${JSON.stringify(intent)}`,
|
|
4740
|
+
"",
|
|
4741
|
+
"RETRIEVED DATA (JSON):",
|
|
4742
|
+
context
|
|
4743
|
+
].join("\n");
|
|
4744
|
+
const rawResponse = await llm.chat(
|
|
4745
|
+
[{ role: "user", content: userPrompt }],
|
|
4746
|
+
"",
|
|
4747
|
+
{ systemPrompt, temperature: 0 }
|
|
4748
|
+
);
|
|
4749
|
+
const parsed = this.parseTransformationResponse(rawResponse);
|
|
4750
|
+
if (parsed) {
|
|
4751
|
+
const intentAllowsTable = intent.wantsExplicitTable || ["tabular", "table", "geographic"].includes(intent.visualizationHint);
|
|
4752
|
+
const intentWantsPieLikeChart = ["pie_chart", "donut_chart"].includes(intent.recommendedChart) || ["composition", "category_breakdown"].includes(intent.visualizationHint);
|
|
4753
|
+
if (parsed.type === "table" && !intentAllowsTable) {
|
|
4754
|
+
console.debug("[UITransformer] LLM chose table but intent says no. Falling back to text.");
|
|
4755
|
+
return this.transform(query, sources, void 0, void 0, intent);
|
|
4756
|
+
}
|
|
4757
|
+
if (intentWantsPieLikeChart && parsed.type !== "pie_chart" && this.detectCategories(sources).length > 1) {
|
|
4758
|
+
console.debug("[UITransformer] LLM ignored pie/composition intent. Using deterministic pie chart.");
|
|
4759
|
+
return this.transform(query, sources, void 0, void 0, intent);
|
|
4760
|
+
}
|
|
4761
|
+
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
4762
|
+
return parsed;
|
|
4763
|
+
}
|
|
4764
|
+
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
4765
|
+
} catch (err) {
|
|
4766
|
+
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
4767
|
+
}
|
|
4768
|
+
return this.transform(query, sources, void 0, void 0, intent);
|
|
4769
|
+
}
|
|
4770
|
+
// ─── Dynamic Intent Detection ─────────────────────────────────────────────
|
|
4771
|
+
/**
|
|
4772
|
+
* Calls the LLM with a compact, focused prompt to extract a structured
|
|
4773
|
+
* `QueryIntent` from the user's query.
|
|
4774
|
+
*
|
|
4775
|
+
* Keeping this as a *separate* call from visualization selection means:
|
|
4776
|
+
* - The prompt is shorter and more reliable.
|
|
4777
|
+
* - The intent object can be reused across both the heuristic and LLM paths.
|
|
4778
|
+
* - It is easy to unit-test intent detection in isolation.
|
|
4779
|
+
*/
|
|
4780
|
+
static async detectIntent(query, llm) {
|
|
4781
|
+
const systemPrompt = `You are an intent classifier for a product-search RAG system.
|
|
4782
|
+
Given a user query, return ONLY a valid JSON object with this exact shape \u2014 no prose, no markdown:
|
|
4783
|
+
|
|
4784
|
+
{
|
|
4785
|
+
"visualizationHint": "trend" | "comparison" | "distribution" | "composition" | "correlation" | "ranking" | "kpi" | "tabular" | "geographic" | "product_browse" | "table" | "text",
|
|
4786
|
+
"recommendedChart": "line_chart" | "bar_chart" | "histogram" | "pie_chart" | "donut_chart" | "scatter_plot" | "horizontal_bar" | "metric_card" | "table" | "geo_map" | "text",
|
|
4787
|
+
"filterInStockOnly": boolean,
|
|
4788
|
+
"wantsExplicitTable": boolean,
|
|
4789
|
+
"isTemporal": boolean,
|
|
4790
|
+
"isComparison": boolean,
|
|
4791
|
+
"language": "<BCP-47 language tag, e.g. en, de, hi, ja>",
|
|
4792
|
+
"reasoning": "<one short sentence \u2014 why you chose these values>"
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
RULES:
|
|
4796
|
+
- visualizationHint and recommendedChart
|
|
4797
|
+
"trend" \u2192 keywords: trend, growth, over time; recommendedChart "line_chart"
|
|
4798
|
+
"comparison" \u2192 keywords: compare, versus, top; recommendedChart "bar_chart"
|
|
4799
|
+
"distribution" \u2192 keywords: distribution, spread; recommendedChart "histogram"
|
|
4800
|
+
"composition" \u2192 keywords: share, percentage, breakup, breakdown; recommendedChart "pie_chart" or "donut_chart"
|
|
4801
|
+
"correlation" \u2192 keywords: relation, correlation; recommendedChart "scatter_plot"
|
|
4802
|
+
"ranking" \u2192 keywords: highest, lowest, top 10, ranking; recommendedChart "horizontal_bar"
|
|
4803
|
+
"kpi" \u2192 keywords: total, average, count; recommendedChart "metric_card"
|
|
4804
|
+
"tabular" \u2192 keywords: detailed records, table, grid, spreadsheet; recommendedChart "table"
|
|
4805
|
+
"geographic" \u2192 keywords: region, country, map; recommendedChart "geo_map"
|
|
4806
|
+
"product_browse" \u2192 user is browsing, searching, describing, viewing details for, or asking about one or more products without analytical visualization intent
|
|
4807
|
+
"table" \u2192 legacy alias for "tabular"; use only if the query literally says table
|
|
4808
|
+
"text" \u2192 conversational, factual, or other intent
|
|
4809
|
+
- If the user explicitly asks for a chart type, honor that chart type in recommendedChart.
|
|
4810
|
+
- If a query says "distribution ... in a pie chart", use visualizationHint "composition" and recommendedChart "pie_chart".
|
|
4811
|
+
- filterInStockOnly: true only if user mentions stock, availability, in stock, etc.
|
|
4812
|
+
- wantsExplicitTable: true if the user asks for a table, list, grid, spreadsheet, or detailed records.
|
|
4813
|
+
- isTemporal: true if time words appear (trend, historical, over time, last year, monthly, etc.)
|
|
4814
|
+
- isComparison: true if user compares, ranks, or contrasts entities or categories.
|
|
4815
|
+
- language: detect from the query text itself; default "en" if uncertain.`;
|
|
4816
|
+
const rawResponse = await llm.chat(
|
|
4817
|
+
[{ role: "user", content: `QUERY: ${query}` }],
|
|
4818
|
+
"",
|
|
4819
|
+
{ systemPrompt, temperature: 0 }
|
|
4820
|
+
);
|
|
4821
|
+
const parsed = this.parseIntentResponse(rawResponse);
|
|
4822
|
+
if (!parsed) {
|
|
4823
|
+
throw new Error(`Could not parse intent JSON from LLM response: ${rawResponse}`);
|
|
4824
|
+
}
|
|
4825
|
+
return parsed;
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* Parse and validate the raw LLM response into a `QueryIntent`.
|
|
4154
4829
|
*/
|
|
4830
|
+
static parseIntentResponse(raw) {
|
|
4831
|
+
const jsonStr = this.extractJsonCandidate(raw);
|
|
4832
|
+
if (!jsonStr) return null;
|
|
4833
|
+
try {
|
|
4834
|
+
const obj = JSON.parse(jsonStr);
|
|
4835
|
+
const validHints = [
|
|
4836
|
+
"trend",
|
|
4837
|
+
"comparison",
|
|
4838
|
+
"distribution",
|
|
4839
|
+
"composition",
|
|
4840
|
+
"correlation",
|
|
4841
|
+
"ranking",
|
|
4842
|
+
"kpi",
|
|
4843
|
+
"tabular",
|
|
4844
|
+
"geographic",
|
|
4845
|
+
"category_breakdown",
|
|
4846
|
+
"product_browse",
|
|
4847
|
+
"table",
|
|
4848
|
+
"text"
|
|
4849
|
+
];
|
|
4850
|
+
const validCharts = [
|
|
4851
|
+
"line_chart",
|
|
4852
|
+
"bar_chart",
|
|
4853
|
+
"histogram",
|
|
4854
|
+
"pie_chart",
|
|
4855
|
+
"donut_chart",
|
|
4856
|
+
"scatter_plot",
|
|
4857
|
+
"horizontal_bar",
|
|
4858
|
+
"metric_card",
|
|
4859
|
+
"table",
|
|
4860
|
+
"geo_map",
|
|
4861
|
+
"text"
|
|
4862
|
+
];
|
|
4863
|
+
const hint = obj.visualizationHint;
|
|
4864
|
+
if (!hint || !validHints.includes(hint)) return null;
|
|
4865
|
+
const normalizedHint = hint === "category_breakdown" ? "composition" : hint;
|
|
4866
|
+
const recommendedChart = typeof obj.recommendedChart === "string" && validCharts.includes(obj.recommendedChart) ? obj.recommendedChart : this.getRecommendedChartForIntent(normalizedHint);
|
|
4867
|
+
return {
|
|
4868
|
+
visualizationHint: normalizedHint,
|
|
4869
|
+
recommendedChart,
|
|
4870
|
+
filterInStockOnly: Boolean(obj.filterInStockOnly),
|
|
4871
|
+
wantsExplicitTable: Boolean(obj.wantsExplicitTable),
|
|
4872
|
+
isTemporal: Boolean(obj.isTemporal),
|
|
4873
|
+
isComparison: Boolean(obj.isComparison),
|
|
4874
|
+
language: typeof obj.language === "string" && obj.language ? obj.language : "en",
|
|
4875
|
+
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : void 0
|
|
4876
|
+
};
|
|
4877
|
+
} catch (e) {
|
|
4878
|
+
return null;
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Heuristic intent detector — used when the LLM is unavailable.
|
|
4883
|
+
* Intentionally minimal: it should only catch the most obvious signals.
|
|
4884
|
+
* The LLM path handles everything subtle.
|
|
4885
|
+
*/
|
|
4886
|
+
static detectIntentHeuristic(query) {
|
|
4887
|
+
const q = query.toLowerCase();
|
|
4888
|
+
const isTemporal = /\b(trend|trends|over time|historical|history|growth|decline|monthly|yearly|weekly|daily|last (year|month|week)|timeline|forecast)\b/.test(q);
|
|
4889
|
+
const isRanking = /\b(highest|lowest|top\s*\d+|bottom\s*\d+|rank(?:ing)?|best|worst|leading)\b/.test(q);
|
|
4890
|
+
const isComparison = /\b(compare|comparison|vs\.?|versus|against|differ|difference|contrast|top)\b/.test(q) || isRanking;
|
|
4891
|
+
const isDistribution = /\b(distribution|spread|histogram|frequency|variance|range)\b/.test(q);
|
|
4892
|
+
const wantsPieLikeChart = /\b(pie|donut|doughnut)(?:\s+chart)?\b/.test(q);
|
|
4893
|
+
const isComposition = wantsPieLikeChart || /\b(share|percentage|percent|breakup|breakdown|composition|split|segmentation|by category|by type|proportion)\b/.test(q);
|
|
4894
|
+
const isCorrelation = /\b(relation|relationship|correlation|correlate|scatter|association|impact of|depend(?:s|ence)? on)\b/.test(q);
|
|
4895
|
+
const isKpi = /\b(total|average|avg|count|sum|median|minimum|maximum|metric|kpi|how many|number of)\b/.test(q);
|
|
4896
|
+
const isGeographic = /\b(region|country|countries|state|city|location|map|geo|geographic|territory)\b/.test(q);
|
|
4897
|
+
const filterInStockOnly = /\b(in[- ]?stock|available|availability|inventory|stock status)\b/.test(q);
|
|
4898
|
+
const wantsExplicitTable = /\b(table|spreadsheet|grid|detailed records|record details|list all|compare all)\b/.test(q);
|
|
4899
|
+
let visualizationHint = "text";
|
|
4900
|
+
if (wantsExplicitTable) visualizationHint = "tabular";
|
|
4901
|
+
else if (isTemporal) visualizationHint = "trend";
|
|
4902
|
+
else if (wantsPieLikeChart) visualizationHint = "composition";
|
|
4903
|
+
else if (isRanking) visualizationHint = "ranking";
|
|
4904
|
+
else if (isComparison) visualizationHint = "comparison";
|
|
4905
|
+
else if (isDistribution) visualizationHint = "distribution";
|
|
4906
|
+
else if (isComposition) visualizationHint = "composition";
|
|
4907
|
+
else if (isCorrelation) visualizationHint = "correlation";
|
|
4908
|
+
else if (isGeographic) visualizationHint = "geographic";
|
|
4909
|
+
else if (isKpi) visualizationHint = "kpi";
|
|
4910
|
+
else if (this.isProductQuery(query)) visualizationHint = "product_browse";
|
|
4911
|
+
return {
|
|
4912
|
+
visualizationHint,
|
|
4913
|
+
recommendedChart: this.getRecommendedChartForIntent(visualizationHint),
|
|
4914
|
+
filterInStockOnly,
|
|
4915
|
+
wantsExplicitTable,
|
|
4916
|
+
isTemporal,
|
|
4917
|
+
isComparison,
|
|
4918
|
+
language: "en"
|
|
4919
|
+
// heuristic cannot reliably detect language
|
|
4920
|
+
};
|
|
4921
|
+
}
|
|
4922
|
+
static getRecommendedChartForIntent(intent) {
|
|
4923
|
+
switch (intent) {
|
|
4924
|
+
case "trend":
|
|
4925
|
+
return "line_chart";
|
|
4926
|
+
case "comparison":
|
|
4927
|
+
return "bar_chart";
|
|
4928
|
+
case "distribution":
|
|
4929
|
+
return "histogram";
|
|
4930
|
+
case "composition":
|
|
4931
|
+
case "category_breakdown":
|
|
4932
|
+
return "pie_chart";
|
|
4933
|
+
case "correlation":
|
|
4934
|
+
return "scatter_plot";
|
|
4935
|
+
case "ranking":
|
|
4936
|
+
return "horizontal_bar";
|
|
4937
|
+
case "kpi":
|
|
4938
|
+
return "metric_card";
|
|
4939
|
+
case "tabular":
|
|
4940
|
+
case "table":
|
|
4941
|
+
return "table";
|
|
4942
|
+
case "geographic":
|
|
4943
|
+
return "geo_map";
|
|
4944
|
+
case "product_browse":
|
|
4945
|
+
case "text":
|
|
4946
|
+
default:
|
|
4947
|
+
return "text";
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
// ─── Transform Helpers ────────────────────────────────────────────────────
|
|
4155
4951
|
static transformToProductCarousel(data, config, trainedSchema) {
|
|
4156
|
-
const products = data.
|
|
4952
|
+
const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
|
|
4157
4953
|
return {
|
|
4158
4954
|
type: "product_carousel",
|
|
4159
4955
|
title: "Recommended Products",
|
|
@@ -4161,55 +4957,154 @@ var UITransformer = class {
|
|
|
4161
4957
|
data: products
|
|
4162
4958
|
};
|
|
4163
4959
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4960
|
+
static transformToPieChart(data, profile, query = "") {
|
|
4961
|
+
var _a;
|
|
4962
|
+
const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
|
|
4963
|
+
const categories = dimension ? Array.from(new Set(profile.records.map((record) => {
|
|
4964
|
+
var _a2;
|
|
4965
|
+
return String((_a2 = record.fields[dimension.key]) != null ? _a2 : "");
|
|
4966
|
+
}).filter(Boolean))) : this.detectCategories(data);
|
|
4967
|
+
if (categories.length === 0) return null;
|
|
4968
|
+
const categoryData = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key) : this.aggregateByCategory(data, categories);
|
|
4170
4969
|
const pieData = Object.entries(categoryData).map(([label, count]) => {
|
|
4171
4970
|
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
|
|
4172
|
-
return {
|
|
4173
|
-
label,
|
|
4174
|
-
value: count,
|
|
4175
|
-
inStockCount,
|
|
4176
|
-
outOfStockCount
|
|
4177
|
-
};
|
|
4971
|
+
return { label, value: count, inStockCount, outOfStockCount };
|
|
4178
4972
|
});
|
|
4179
4973
|
return {
|
|
4180
4974
|
type: "pie_chart",
|
|
4181
|
-
title:
|
|
4182
|
-
description: `Showing breakdown across ${
|
|
4975
|
+
title: `Distribution by ${(_a = dimension == null ? void 0 : dimension.label) != null ? _a : "Category"}`,
|
|
4976
|
+
description: `Showing breakdown across ${pieData.length} categories`,
|
|
4183
4977
|
data: pieData
|
|
4184
4978
|
};
|
|
4185
4979
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
timestamp:
|
|
4193
|
-
value:
|
|
4194
|
-
|
|
4195
|
-
})
|
|
4980
|
+
static transformToLineChart(profile) {
|
|
4981
|
+
const dateField = profile.dateFields[0];
|
|
4982
|
+
const valueField = profile.numericFields[0];
|
|
4983
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
4984
|
+
profile.records.forEach((record) => {
|
|
4985
|
+
var _a, _b, _c;
|
|
4986
|
+
const timestamp = String((_a = record.fields[dateField.key]) != null ? _a : "");
|
|
4987
|
+
const value = (_b = this.toFiniteNumber(record.fields[valueField.key])) != null ? _b : 0;
|
|
4988
|
+
buckets.set(timestamp, ((_c = buckets.get(timestamp)) != null ? _c : 0) + value);
|
|
4989
|
+
});
|
|
4990
|
+
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 }));
|
|
4196
4991
|
return {
|
|
4197
4992
|
type: "line_chart",
|
|
4198
|
-
title:
|
|
4993
|
+
title: `${valueField.label} Over Time`,
|
|
4199
4994
|
description: `Showing ${lineData.length} data points`,
|
|
4200
4995
|
data: lineData
|
|
4201
4996
|
};
|
|
4202
4997
|
}
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
const
|
|
4208
|
-
const
|
|
4209
|
-
const
|
|
4210
|
-
|
|
4211
|
-
|
|
4998
|
+
static transformToBarChart(data, profile, query = "", horizontal = false) {
|
|
4999
|
+
var _a;
|
|
5000
|
+
const dimension = profile ? this.selectDimensionField(profile, query) : void 0;
|
|
5001
|
+
const measure = profile ? this.selectNumericField(profile, query) : void 0;
|
|
5002
|
+
const aggregate = dimension && profile ? this.aggregateProfileByDimension(profile, dimension.key, measure == null ? void 0 : measure.key) : this.aggregateByCategory(data, this.detectCategories(data));
|
|
5003
|
+
const barData = Object.entries(aggregate).map(([category, value]) => ({ category, value: Number(value) })).sort((a, b) => horizontal ? b.value - a.value : 0).slice(0, 12);
|
|
5004
|
+
const fallbackData = barData.length > 0 ? barData : data.slice(0, 12).map((item, index) => {
|
|
5005
|
+
var _a2, _b, _c, _d, _e;
|
|
5006
|
+
const meta = item.metadata || {};
|
|
5007
|
+
const label = String(
|
|
5008
|
+
(_c = (_b = (_a2 = this.getDynamicVal(meta, "name")) != null ? _a2 : meta.label) != null ? _b : meta.title) != null ? _c : `Result ${index + 1}`
|
|
5009
|
+
);
|
|
5010
|
+
const value = (_e = (_d = this.extractNumericValue(meta)) != null ? _d : item.score) != null ? _e : 0;
|
|
5011
|
+
return { category: label, value: Number(value) };
|
|
5012
|
+
});
|
|
5013
|
+
return {
|
|
5014
|
+
type: horizontal ? "horizontal_bar" : "bar_chart",
|
|
5015
|
+
title: dimension ? `${(_a = measure == null ? void 0 : measure.label) != null ? _a : "Count"} by ${dimension.label}` : "Comparison",
|
|
5016
|
+
description: `Showing ${fallbackData.length} comparable values`,
|
|
5017
|
+
data: fallbackData
|
|
5018
|
+
};
|
|
5019
|
+
}
|
|
5020
|
+
static transformToHistogram(profile, query = "") {
|
|
5021
|
+
const field = this.selectNumericField(profile, query);
|
|
5022
|
+
if (!field) return null;
|
|
5023
|
+
const values = profile.records.map((record) => this.toFiniteNumber(record.fields[field.key])).filter((value) => value !== null).sort((a, b) => a - b);
|
|
5024
|
+
if (values.length === 0) return null;
|
|
5025
|
+
const min = values[0];
|
|
5026
|
+
const max = values[values.length - 1];
|
|
5027
|
+
const bucketCount = Math.min(10, Math.max(3, Math.ceil(Math.sqrt(values.length))));
|
|
5028
|
+
const width = max === min ? 1 : (max - min) / bucketCount;
|
|
5029
|
+
const buckets = Array.from({ length: bucketCount }, (_, index) => {
|
|
5030
|
+
const start = min + index * width;
|
|
5031
|
+
const end = index === bucketCount - 1 ? max : start + width;
|
|
5032
|
+
return { category: `${this.formatNumber(start)}-${this.formatNumber(end)}`, value: 0 };
|
|
5033
|
+
});
|
|
5034
|
+
values.forEach((value) => {
|
|
5035
|
+
const bucketIndex = max === min ? 0 : Math.min(bucketCount - 1, Math.floor((value - min) / width));
|
|
5036
|
+
buckets[bucketIndex].value += 1;
|
|
5037
|
+
});
|
|
5038
|
+
return {
|
|
5039
|
+
type: "histogram",
|
|
5040
|
+
title: `${field.label} Distribution`,
|
|
5041
|
+
description: `Showing ${values.length} values across ${bucketCount} buckets`,
|
|
5042
|
+
data: buckets
|
|
5043
|
+
};
|
|
5044
|
+
}
|
|
5045
|
+
static transformToScatterPlot(profile, query = "") {
|
|
5046
|
+
const fields = this.rankFieldsByQuery(profile.numericFields, query).slice(0, 2);
|
|
5047
|
+
if (fields.length < 2) return null;
|
|
5048
|
+
const [xField, yField] = fields;
|
|
5049
|
+
const points = profile.records.map((record) => {
|
|
5050
|
+
var _a;
|
|
5051
|
+
const x = this.toFiniteNumber(record.fields[xField.key]);
|
|
5052
|
+
const y = this.toFiniteNumber(record.fields[yField.key]);
|
|
5053
|
+
if (x === null || y === null) return null;
|
|
5054
|
+
return { x, y, label: String((_a = this.getRecordLabel(record)) != null ? _a : record.id) };
|
|
5055
|
+
}).filter((point) => point !== null).slice(0, 100);
|
|
5056
|
+
if (points.length === 0) return null;
|
|
5057
|
+
return {
|
|
5058
|
+
type: "scatter_plot",
|
|
5059
|
+
title: `${xField.label} vs ${yField.label}`,
|
|
5060
|
+
description: `Showing ${points.length} paired values`,
|
|
5061
|
+
data: points
|
|
5062
|
+
};
|
|
5063
|
+
}
|
|
5064
|
+
static transformToMetricCard(profile, query = "") {
|
|
5065
|
+
const operation = this.detectAggregationOperation(query);
|
|
5066
|
+
const numericField = this.selectNumericField(profile, query);
|
|
5067
|
+
const values = numericField ? profile.records.map((record) => this.toFiniteNumber(record.fields[numericField.key])).filter((value2) => value2 !== null) : [];
|
|
5068
|
+
const value = operation === "count" || values.length === 0 ? profile.records.length : this.calculateAggregate(values, operation);
|
|
5069
|
+
const metric = {
|
|
5070
|
+
label: numericField ? `${operation} ${numericField.label}` : "Count",
|
|
5071
|
+
value,
|
|
5072
|
+
operation
|
|
5073
|
+
};
|
|
5074
|
+
return {
|
|
5075
|
+
type: "metric_card",
|
|
5076
|
+
title: metric.label,
|
|
5077
|
+
description: `Calculated from ${profile.records.length} result${profile.records.length === 1 ? "" : "s"}`,
|
|
5078
|
+
data: metric
|
|
5079
|
+
};
|
|
5080
|
+
}
|
|
5081
|
+
static transformToRadarChart(data) {
|
|
5082
|
+
const attributeMap = {};
|
|
5083
|
+
data.forEach((item) => {
|
|
5084
|
+
var _a, _b, _c;
|
|
5085
|
+
const meta = item.metadata || {};
|
|
5086
|
+
const seriesName = String((_c = (_b = (_a = meta.name) != null ? _a : meta.product) != null ? _b : item.id) != null ? _c : "Item");
|
|
5087
|
+
Object.entries(meta).forEach(([key, val]) => {
|
|
5088
|
+
if (typeof val === "number" && !["price", "id"].includes(key.toLowerCase())) {
|
|
5089
|
+
if (!attributeMap[key]) attributeMap[key] = {};
|
|
5090
|
+
attributeMap[key][seriesName] = val;
|
|
5091
|
+
}
|
|
5092
|
+
});
|
|
5093
|
+
});
|
|
5094
|
+
const radarData = Object.entries(attributeMap).map(([attribute, series]) => __spreadValues({
|
|
5095
|
+
attribute
|
|
5096
|
+
}, series));
|
|
5097
|
+
return {
|
|
5098
|
+
type: "radar_chart",
|
|
5099
|
+
title: "Product Comparison",
|
|
5100
|
+
description: `Comparing ${data.length} items across ${radarData.length} attributes`,
|
|
5101
|
+
data: radarData.length > 0 ? radarData : data.map((d) => ({ attribute: d.content.substring(0, 40) }))
|
|
4212
5102
|
};
|
|
5103
|
+
}
|
|
5104
|
+
static transformToTable(data, query = "") {
|
|
5105
|
+
const columns = this.extractTableColumns(data, query);
|
|
5106
|
+
const rows = data.map((item) => this.extractTableRow(item, columns));
|
|
5107
|
+
const tableData = { columns, rows };
|
|
4213
5108
|
return {
|
|
4214
5109
|
type: "table",
|
|
4215
5110
|
title: "Detailed Results",
|
|
@@ -4217,28 +5112,17 @@ var UITransformer = class {
|
|
|
4217
5112
|
data: tableData
|
|
4218
5113
|
};
|
|
4219
5114
|
}
|
|
4220
|
-
/**
|
|
4221
|
-
* Transform data to text format (fallback)
|
|
4222
|
-
*/
|
|
4223
5115
|
static transformToText(data) {
|
|
4224
|
-
const textContent = data.map((item) => item.content).join("\n\n");
|
|
4225
5116
|
return this.createTextResponse(
|
|
4226
5117
|
"Search Results",
|
|
4227
|
-
|
|
5118
|
+
data.map((item) => item.content).join("\n\n"),
|
|
4228
5119
|
`Found ${data.length} relevant results`
|
|
4229
5120
|
);
|
|
4230
5121
|
}
|
|
4231
|
-
/**
|
|
4232
|
-
* Helper: Create text response
|
|
4233
|
-
*/
|
|
4234
5122
|
static createTextResponse(title, content, description) {
|
|
4235
|
-
return {
|
|
4236
|
-
type: "text",
|
|
4237
|
-
title,
|
|
4238
|
-
description,
|
|
4239
|
-
data: { content }
|
|
4240
|
-
};
|
|
5123
|
+
return { type: "text", title, description, data: { content } };
|
|
4241
5124
|
}
|
|
5125
|
+
// ─── LLM Response Parsing ─────────────────────────────────────────────────
|
|
4242
5126
|
static parseTransformationResponse(raw) {
|
|
4243
5127
|
const payloadText = this.extractJsonCandidate(raw);
|
|
4244
5128
|
if (!payloadText) return null;
|
|
@@ -4275,9 +5159,7 @@ var UITransformer = class {
|
|
|
4275
5159
|
if (char === "{") depth += 1;
|
|
4276
5160
|
if (char === "}") {
|
|
4277
5161
|
depth -= 1;
|
|
4278
|
-
if (depth === 0)
|
|
4279
|
-
return cleaned.slice(start, i + 1);
|
|
4280
|
-
}
|
|
5162
|
+
if (depth === 0) return cleaned.slice(start, i + 1);
|
|
4281
5163
|
}
|
|
4282
5164
|
}
|
|
4283
5165
|
return null;
|
|
@@ -4285,19 +5167,16 @@ var UITransformer = class {
|
|
|
4285
5167
|
static normalizeTransformation(payload) {
|
|
4286
5168
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
4287
5169
|
if (!payload || typeof payload !== "object") return null;
|
|
4288
|
-
const
|
|
4289
|
-
const type = this.normalizeVisualizationType(
|
|
5170
|
+
const p = payload;
|
|
5171
|
+
const type = this.normalizeVisualizationType(
|
|
5172
|
+
String((_c = (_b = (_a = p.type) != null ? _a : p.view) != null ? _b : p.chartType) != null ? _c : "")
|
|
5173
|
+
);
|
|
4290
5174
|
if (!type) return null;
|
|
4291
|
-
const title = String((_e = (_d =
|
|
4292
|
-
const description =
|
|
4293
|
-
const rawData = (_j = (_i = (_h = (_g = (_f =
|
|
5175
|
+
const title = String((_e = (_d = p.title) != null ? _d : p.heading) != null ? _e : "Visualization");
|
|
5176
|
+
const description = p.description ? String(p.description) : void 0;
|
|
5177
|
+
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;
|
|
4294
5178
|
const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
|
|
4295
|
-
const transformation = {
|
|
4296
|
-
type,
|
|
4297
|
-
title,
|
|
4298
|
-
description,
|
|
4299
|
-
data
|
|
4300
|
-
};
|
|
5179
|
+
const transformation = { type, title, description, data };
|
|
4301
5180
|
return this.validateTransformation(transformation) ? transformation : null;
|
|
4302
5181
|
}
|
|
4303
5182
|
static normalizeVisualizationType(type) {
|
|
@@ -4307,10 +5186,23 @@ var UITransformer = class {
|
|
|
4307
5186
|
pie_chart: "pie_chart",
|
|
4308
5187
|
bar: "bar_chart",
|
|
4309
5188
|
bar_chart: "bar_chart",
|
|
5189
|
+
histogram: "histogram",
|
|
5190
|
+
horizontal_bar: "horizontal_bar",
|
|
5191
|
+
horizontalbar: "horizontal_bar",
|
|
4310
5192
|
line: "line_chart",
|
|
4311
5193
|
line_chart: "line_chart",
|
|
5194
|
+
scatter: "scatter_plot",
|
|
5195
|
+
scatter_plot: "scatter_plot",
|
|
5196
|
+
scatterplot: "scatter_plot",
|
|
4312
5197
|
radar: "radar_chart",
|
|
4313
5198
|
radar_chart: "radar_chart",
|
|
5199
|
+
metric: "metric_card",
|
|
5200
|
+
metric_card: "metric_card",
|
|
5201
|
+
card: "metric_card",
|
|
5202
|
+
kpi: "metric_card",
|
|
5203
|
+
geo: "geo_map",
|
|
5204
|
+
geo_map: "geo_map",
|
|
5205
|
+
map: "geo_map",
|
|
4314
5206
|
table: "table",
|
|
4315
5207
|
text: "text",
|
|
4316
5208
|
product_carousel: "product_carousel",
|
|
@@ -4318,21 +5210,31 @@ var UITransformer = class {
|
|
|
4318
5210
|
};
|
|
4319
5211
|
return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
|
|
4320
5212
|
}
|
|
4321
|
-
static validateTransformation(
|
|
4322
|
-
const { type, data } =
|
|
5213
|
+
static validateTransformation(t) {
|
|
5214
|
+
const { type, data } = t;
|
|
4323
5215
|
switch (type) {
|
|
4324
5216
|
case "pie_chart":
|
|
4325
5217
|
case "bar_chart":
|
|
5218
|
+
case "histogram":
|
|
5219
|
+
case "horizontal_bar":
|
|
5220
|
+
return Array.isArray(data) && data.every(
|
|
5221
|
+
(i) => i !== null && typeof i === "object" && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
|
|
5222
|
+
);
|
|
5223
|
+
case "scatter_plot":
|
|
4326
5224
|
return Array.isArray(data) && data.every(
|
|
4327
|
-
(
|
|
5225
|
+
(i) => i !== null && typeof i === "object" && (typeof i.x === "number" || !Number.isNaN(Number(i.x))) && (typeof i.y === "number" || !Number.isNaN(Number(i.y)))
|
|
4328
5226
|
);
|
|
4329
5227
|
case "line_chart":
|
|
4330
5228
|
return Array.isArray(data) && data.every(
|
|
4331
|
-
(
|
|
5229
|
+
(i) => i !== null && typeof i === "object" && "timestamp" in i && (typeof i.value === "number" || !Number.isNaN(Number(i.value)))
|
|
4332
5230
|
);
|
|
5231
|
+
case "metric_card":
|
|
5232
|
+
return typeof data === "object" && data !== null && (typeof data.value === "number" || !Number.isNaN(Number(data.value)));
|
|
5233
|
+
case "geo_map":
|
|
5234
|
+
return Array.isArray(data) || typeof data === "object" && data !== null;
|
|
4333
5235
|
case "radar_chart":
|
|
4334
5236
|
return Array.isArray(data) && data.every(
|
|
4335
|
-
(
|
|
5237
|
+
(i) => i !== null && typeof i === "object" && "attribute" in i
|
|
4336
5238
|
);
|
|
4337
5239
|
case "table":
|
|
4338
5240
|
return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
|
|
@@ -4341,15 +5243,27 @@ var UITransformer = class {
|
|
|
4341
5243
|
case "product_carousel":
|
|
4342
5244
|
case "carousel":
|
|
4343
5245
|
return Array.isArray(data) && data.every(
|
|
4344
|
-
(
|
|
5246
|
+
(i) => i !== null && typeof i === "object" && ("id" in i || "name" in i)
|
|
4345
5247
|
);
|
|
4346
5248
|
default:
|
|
4347
5249
|
return false;
|
|
4348
5250
|
}
|
|
4349
5251
|
}
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
5252
|
+
// ─── Data Inspection Helpers ──────────────────────────────────────────────
|
|
5253
|
+
static isStructuredListQuery(query) {
|
|
5254
|
+
const q = query.toLowerCase();
|
|
5255
|
+
const asksForRecords = /\b(list|provide|show|give|get|find|which|whose|records?|details?)\b/.test(q);
|
|
5256
|
+
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);
|
|
5257
|
+
const hasEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
|
|
5258
|
+
return asksForRecords && (hasNumericPredicate || hasEntityTerms);
|
|
5259
|
+
}
|
|
5260
|
+
static isProductQuery(query) {
|
|
5261
|
+
const q = query.toLowerCase();
|
|
5262
|
+
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);
|
|
5263
|
+
const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list)\b/.test(q);
|
|
5264
|
+
const nonProductEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
|
|
5265
|
+
return productTerms && productAction && !nonProductEntityTerms;
|
|
5266
|
+
}
|
|
4353
5267
|
static isProductData(item) {
|
|
4354
5268
|
const content = (item.content || "").toLowerCase();
|
|
4355
5269
|
const productKeywords = [
|
|
@@ -4375,360 +5289,582 @@ var UITransformer = class {
|
|
|
4375
5289
|
"fragrance"
|
|
4376
5290
|
];
|
|
4377
5291
|
const hasKeywords = productKeywords.some((kw) => content.includes(kw));
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
5292
|
+
const metadata = item.metadata || {};
|
|
5293
|
+
const hasMetadataKey = Object.keys(metadata).some((k) => {
|
|
5294
|
+
const val = metadata[k];
|
|
5295
|
+
return ["price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase()) && val !== null && val !== void 0 && val !== "";
|
|
5296
|
+
});
|
|
5297
|
+
const hasPricePattern = /\$\s*\d+\.\d{2}/.test(content);
|
|
4382
5298
|
return hasKeywords || hasMetadataKey || hasPricePattern;
|
|
4383
5299
|
}
|
|
4384
|
-
/**
|
|
4385
|
-
* Helper: Check if data contains time series
|
|
4386
|
-
*/
|
|
4387
5300
|
static isTimeSeriesData(item) {
|
|
4388
|
-
const content = (item.content || "").toLowerCase();
|
|
4389
|
-
const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
|
|
4390
|
-
const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
|
|
4391
5301
|
const metadata = item.metadata || {};
|
|
4392
5302
|
const maybeDateKeys = Object.keys(metadata).filter(
|
|
4393
5303
|
(k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
|
|
4394
5304
|
);
|
|
4395
|
-
|
|
5305
|
+
return maybeDateKeys.some((key) => {
|
|
4396
5306
|
const value = metadata[key];
|
|
4397
|
-
if (typeof value === "string")
|
|
4398
|
-
return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
4399
|
-
}
|
|
5307
|
+
if (typeof value === "string") return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
4400
5308
|
return typeof value === "number";
|
|
4401
5309
|
});
|
|
4402
|
-
return hasTimeKeyword || hasValidDateValue;
|
|
4403
5310
|
}
|
|
4404
|
-
static
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
const normalized = query.toLowerCase();
|
|
4409
|
-
const chartKeywords = [
|
|
4410
|
-
"distribution",
|
|
4411
|
-
"breakdown",
|
|
4412
|
-
"by category",
|
|
4413
|
-
"by type",
|
|
4414
|
-
"compare",
|
|
4415
|
-
"share",
|
|
4416
|
-
"percentage",
|
|
4417
|
-
"segmentation",
|
|
4418
|
-
"split",
|
|
4419
|
-
"category breakdown",
|
|
4420
|
-
"category distribution"
|
|
4421
|
-
];
|
|
4422
|
-
return chartKeywords.some((keyword) => normalized.includes(keyword));
|
|
5311
|
+
static hasMultipleFields(data) {
|
|
5312
|
+
const fieldCount = /* @__PURE__ */ new Set();
|
|
5313
|
+
data.forEach((item) => Object.keys(item.metadata || {}).forEach((k) => fieldCount.add(k)));
|
|
5314
|
+
return fieldCount.size > 2;
|
|
4423
5315
|
}
|
|
4424
|
-
static
|
|
4425
|
-
const
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
5316
|
+
static profileData(data) {
|
|
5317
|
+
const records = data.map((item) => {
|
|
5318
|
+
const fields2 = {};
|
|
5319
|
+
Object.entries(item.metadata || {}).forEach(([key, value]) => {
|
|
5320
|
+
const primitive = this.toPrimitive(value);
|
|
5321
|
+
if (primitive !== null) fields2[key] = primitive;
|
|
5322
|
+
});
|
|
5323
|
+
if (!fields2.content && item.content) fields2.content = item.content.substring(0, 500);
|
|
5324
|
+
return {
|
|
5325
|
+
id: item.id,
|
|
5326
|
+
content: item.content,
|
|
5327
|
+
score: item.score,
|
|
5328
|
+
fields: fields2,
|
|
5329
|
+
source: item
|
|
5330
|
+
};
|
|
5331
|
+
});
|
|
5332
|
+
const keys = Array.from(new Set(records.flatMap((record) => Object.keys(record.fields))));
|
|
5333
|
+
const fields = keys.map((key) => {
|
|
5334
|
+
const values = records.map((record) => record.fields[key]).filter((value) => value !== void 0 && value !== null && String(value).trim() !== "");
|
|
5335
|
+
const uniqueCount = new Set(values.map((value) => String(value).toLowerCase())).size;
|
|
5336
|
+
return {
|
|
5337
|
+
key,
|
|
5338
|
+
label: this.humanizeFieldName(key),
|
|
5339
|
+
kind: this.inferFieldKind(key, values, records.length, uniqueCount),
|
|
5340
|
+
values,
|
|
5341
|
+
uniqueCount
|
|
5342
|
+
};
|
|
5343
|
+
});
|
|
5344
|
+
return {
|
|
5345
|
+
records,
|
|
5346
|
+
fields,
|
|
5347
|
+
numericFields: fields.filter((field) => field.kind === "number"),
|
|
5348
|
+
dateFields: fields.filter((field) => field.kind === "date"),
|
|
5349
|
+
categoricalFields: fields.filter((field) => field.kind === "category"),
|
|
5350
|
+
booleanFields: fields.filter((field) => field.kind === "boolean")
|
|
5351
|
+
};
|
|
4444
5352
|
}
|
|
4445
|
-
static
|
|
4446
|
-
|
|
4447
|
-
|
|
5353
|
+
static toPrimitive(value) {
|
|
5354
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
5355
|
+
if (value === null || value === void 0) return null;
|
|
5356
|
+
if (value instanceof Date) return value.toISOString();
|
|
5357
|
+
return null;
|
|
4448
5358
|
}
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
if (!meta) return void 0;
|
|
4455
|
-
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
4456
|
-
if (mapping && mapping[uiKey]) {
|
|
4457
|
-
const mappedKey = mapping[uiKey];
|
|
4458
|
-
if (meta[mappedKey] !== void 0) return meta[mappedKey];
|
|
5359
|
+
static inferFieldKind(key, values, rowCount, uniqueCount) {
|
|
5360
|
+
const lowerKey = key.toLowerCase();
|
|
5361
|
+
if (values.length === 0) return "text";
|
|
5362
|
+
if (values.every((value) => typeof value === "boolean" || /^(true|false|yes|no|in_stock|out_of_stock|available|unavailable)$/i.test(String(value)))) {
|
|
5363
|
+
return "boolean";
|
|
4459
5364
|
}
|
|
4460
|
-
if (
|
|
4461
|
-
|
|
4462
|
-
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
5365
|
+
if (/(date|time|timestamp|created|updated|month|year|period)/i.test(lowerKey) && values.some((value) => this.isDateLike(value))) {
|
|
5366
|
+
return "date";
|
|
4463
5367
|
}
|
|
4464
|
-
|
|
5368
|
+
const numericCount = values.filter((value) => this.toFiniteNumber(value) !== null).length;
|
|
5369
|
+
if (numericCount / values.length >= 0.85 && !/(id|sku|ean|upc|zip|postal|phone|code)/i.test(lowerKey)) {
|
|
5370
|
+
return "number";
|
|
5371
|
+
}
|
|
5372
|
+
if (uniqueCount <= Math.max(20, Math.ceil(rowCount * 0.7)) && !/(id|sku|ean|upc|description|content|url|image|thumbnail)/i.test(lowerKey)) {
|
|
5373
|
+
return "category";
|
|
5374
|
+
}
|
|
5375
|
+
return "text";
|
|
4465
5376
|
}
|
|
4466
|
-
static
|
|
4467
|
-
|
|
4468
|
-
const
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
return
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
4488
|
-
brand: brand ? String(brand) : void 0,
|
|
4489
|
-
description: item.content,
|
|
4490
|
-
inStock: this.determineStockStatus(item)
|
|
4491
|
-
};
|
|
5377
|
+
static isDateLike(value) {
|
|
5378
|
+
if (typeof value === "number") return value > 1900 && value < 3e3;
|
|
5379
|
+
const text = String(value).trim();
|
|
5380
|
+
return text.length > 3 && !Number.isNaN(Date.parse(text));
|
|
5381
|
+
}
|
|
5382
|
+
static chooseAutomaticVisualization(data, profile, query) {
|
|
5383
|
+
if (profile.records.length === 0) return null;
|
|
5384
|
+
if (profile.dateFields.length > 0 && profile.numericFields.length > 0) {
|
|
5385
|
+
return this.transformToLineChart(profile);
|
|
5386
|
+
}
|
|
5387
|
+
if (profile.categoricalFields.length > 0 && profile.numericFields.length > 0) {
|
|
5388
|
+
return this.transformToBarChart(data, profile, query);
|
|
5389
|
+
}
|
|
5390
|
+
if (profile.categoricalFields.length > 0) {
|
|
5391
|
+
return this.transformToPieChart(data, profile, query);
|
|
5392
|
+
}
|
|
5393
|
+
if (profile.numericFields.length >= 2) {
|
|
5394
|
+
return this.transformToScatterPlot(profile, query);
|
|
5395
|
+
}
|
|
5396
|
+
if (profile.numericFields.length === 1) {
|
|
5397
|
+
return this.transformToMetricCard(profile, query);
|
|
4492
5398
|
}
|
|
4493
5399
|
return null;
|
|
4494
5400
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
5401
|
+
static selectDimensionField(profile, query) {
|
|
5402
|
+
var _a, _b;
|
|
5403
|
+
const productCategory = profile.categoricalFields.find(
|
|
5404
|
+
(field) => /category|department|collection|type|group|segment|region|country|state|city/i.test(field.key)
|
|
5405
|
+
);
|
|
5406
|
+
const ranked = this.rankFieldsByQuery(profile.categoricalFields, query);
|
|
5407
|
+
return (_b = (_a = ranked[0]) != null ? _a : productCategory) != null ? _b : profile.categoricalFields[0];
|
|
5408
|
+
}
|
|
5409
|
+
static selectNumericField(profile, query) {
|
|
5410
|
+
var _a;
|
|
5411
|
+
return (_a = this.rankFieldsByQuery(profile.numericFields, query)[0]) != null ? _a : profile.numericFields[0];
|
|
5412
|
+
}
|
|
5413
|
+
static rankFieldsByQuery(fields, query) {
|
|
5414
|
+
const q = query.toLowerCase();
|
|
5415
|
+
return [...fields].sort((a, b) => this.fieldScore(b, q) - this.fieldScore(a, q));
|
|
5416
|
+
}
|
|
5417
|
+
static fieldScore(field, query) {
|
|
5418
|
+
const key = field.key.toLowerCase();
|
|
5419
|
+
const label = field.label.toLowerCase();
|
|
5420
|
+
let score = 0;
|
|
5421
|
+
if (query.includes(key)) score += 4;
|
|
5422
|
+
if (query.includes(label)) score += 4;
|
|
5423
|
+
key.split(/[_\s-]+/).forEach((part) => {
|
|
5424
|
+
if (part && query.includes(part)) score += 1;
|
|
5425
|
+
});
|
|
5426
|
+
if (/category|department|collection|type|group|segment/.test(key)) score += 2;
|
|
5427
|
+
if (/count|total|quantity|stock|inventory|sales|revenue|amount|price|value|score/.test(key)) score += 2;
|
|
5428
|
+
if (/id|sku|ean|upc|code/.test(key)) score -= 5;
|
|
5429
|
+
return score;
|
|
5430
|
+
}
|
|
5431
|
+
static normalizeComparableField(field) {
|
|
5432
|
+
return field.toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "").trim();
|
|
5433
|
+
}
|
|
5434
|
+
static fieldTokens(field) {
|
|
5435
|
+
return field.toLowerCase().replace(/[_-]+/g, " ").split(/\s+/).map((token) => token.replace(/[^a-z0-9]/g, "")).filter(Boolean);
|
|
5436
|
+
}
|
|
5437
|
+
static aggregateProfileByDimension(profile, dimensionKey, measureKey) {
|
|
5438
|
+
const result = {};
|
|
5439
|
+
profile.records.forEach((record) => {
|
|
5440
|
+
var _a, _b, _c;
|
|
5441
|
+
const category = String((_a = record.fields[dimensionKey]) != null ? _a : "Other").trim() || "Other";
|
|
5442
|
+
const value = measureKey ? (_b = this.toFiniteNumber(record.fields[measureKey])) != null ? _b : 0 : 1;
|
|
5443
|
+
result[category] = ((_c = result[category]) != null ? _c : 0) + value;
|
|
5444
|
+
});
|
|
5445
|
+
return result;
|
|
5446
|
+
}
|
|
5447
|
+
static getRecordLabel(record) {
|
|
5448
|
+
const labelKeys = ["name", "title", "label", "product", "item", "brand"];
|
|
5449
|
+
const key = Object.keys(record.fields).find(
|
|
5450
|
+
(fieldKey) => labelKeys.some((labelKey) => fieldKey.toLowerCase().includes(labelKey))
|
|
5451
|
+
);
|
|
5452
|
+
return key ? record.fields[key] : void 0;
|
|
5453
|
+
}
|
|
5454
|
+
static detectAggregationOperation(query) {
|
|
5455
|
+
const q = query.toLowerCase();
|
|
5456
|
+
if (/\b(avg|average|mean)\b/.test(q)) return "average";
|
|
5457
|
+
if (/\b(count|how many|number of)\b/.test(q)) return "count";
|
|
5458
|
+
if (/\b(min|minimum|lowest)\b/.test(q)) return "min";
|
|
5459
|
+
if (/\b(max|maximum|highest)\b/.test(q)) return "max";
|
|
5460
|
+
if (/\bmedian\b/.test(q)) return "median";
|
|
5461
|
+
return "sum";
|
|
5462
|
+
}
|
|
5463
|
+
static calculateAggregate(values, operation) {
|
|
5464
|
+
if (values.length === 0) return 0;
|
|
5465
|
+
switch (operation) {
|
|
5466
|
+
case "average":
|
|
5467
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
5468
|
+
case "count":
|
|
5469
|
+
return values.length;
|
|
5470
|
+
case "min":
|
|
5471
|
+
return Math.min(...values);
|
|
5472
|
+
case "max":
|
|
5473
|
+
return Math.max(...values);
|
|
5474
|
+
case "median": {
|
|
5475
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5476
|
+
const middle = Math.floor(sorted.length / 2);
|
|
5477
|
+
return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
5478
|
+
}
|
|
5479
|
+
case "sum":
|
|
5480
|
+
default:
|
|
5481
|
+
return values.reduce((sum, value) => sum + value, 0);
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
static humanizeFieldName(key) {
|
|
5485
|
+
return key.replace(/[_-]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\s+/g, " ").trim().replace(/\b\w/g, (char) => char.toUpperCase());
|
|
5486
|
+
}
|
|
5487
|
+
static formatNumber(value) {
|
|
5488
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
5489
|
+
}
|
|
4498
5490
|
static detectCategories(data) {
|
|
4499
5491
|
const categories = /* @__PURE__ */ new Set();
|
|
4500
5492
|
data.forEach((item) => {
|
|
4501
|
-
const
|
|
4502
|
-
if (
|
|
4503
|
-
categories.add(String(meta.category));
|
|
4504
|
-
}
|
|
4505
|
-
if (meta.type) {
|
|
4506
|
-
categories.add(String(meta.type));
|
|
4507
|
-
}
|
|
4508
|
-
if (meta.tag) {
|
|
4509
|
-
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
4510
|
-
tags.forEach((t) => categories.add(String(t)));
|
|
4511
|
-
}
|
|
4512
|
-
const contentCategories = Array.from(new Set(
|
|
4513
|
-
Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
|
|
4514
|
-
));
|
|
4515
|
-
contentCategories.forEach((category) => categories.add(category));
|
|
4516
|
-
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
4517
|
-
if (categoryMatch) {
|
|
4518
|
-
categories.add(categoryMatch[1].trim());
|
|
4519
|
-
}
|
|
5493
|
+
const category = this.getProductCategory(item);
|
|
5494
|
+
if (category) categories.add(category);
|
|
4520
5495
|
});
|
|
4521
5496
|
return Array.from(categories);
|
|
4522
5497
|
}
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
5498
|
+
static getProductCategory(item) {
|
|
5499
|
+
const meta = item.metadata || {};
|
|
5500
|
+
const metadataCategory = resolveMetadataValue(meta, "category");
|
|
5501
|
+
if (this.isUsableCategory(metadataCategory)) return String(metadataCategory).trim();
|
|
5502
|
+
const content = item.content || "";
|
|
5503
|
+
const explicitCategoryMatch = content.match(
|
|
5504
|
+
/(?:^|\n)\s*(?:product\s*)?(?:category|department|collection)\s*[:\-–]\s*([^\n]+)/i
|
|
5505
|
+
);
|
|
5506
|
+
if (explicitCategoryMatch && this.isUsableCategory(explicitCategoryMatch[1])) {
|
|
5507
|
+
return explicitCategoryMatch[1].trim();
|
|
5508
|
+
}
|
|
5509
|
+
return null;
|
|
5510
|
+
}
|
|
5511
|
+
static isUsableCategory(value) {
|
|
5512
|
+
if (value === null || value === void 0) return false;
|
|
5513
|
+
const category = String(value).trim();
|
|
5514
|
+
if (!category) return false;
|
|
5515
|
+
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);
|
|
5516
|
+
}
|
|
4526
5517
|
static aggregateByCategory(data, categories) {
|
|
4527
|
-
const result =
|
|
4528
|
-
categories.forEach((cat) => {
|
|
4529
|
-
result[cat] = 0;
|
|
4530
|
-
});
|
|
5518
|
+
const result = Object.fromEntries(categories.map((c) => [c, 0]));
|
|
4531
5519
|
data.forEach((item) => {
|
|
4532
|
-
|
|
4533
|
-
const
|
|
4534
|
-
if (Object.prototype.hasOwnProperty.call(result,
|
|
4535
|
-
|
|
4536
|
-
} else {
|
|
4537
|
-
result["Other"] = (result["Other"] || 0) + 1;
|
|
4538
|
-
}
|
|
5520
|
+
var _a;
|
|
5521
|
+
const cat = (_a = this.getProductCategory(item)) != null ? _a : "Other";
|
|
5522
|
+
if (Object.prototype.hasOwnProperty.call(result, cat)) result[cat]++;
|
|
5523
|
+
else result["Other"] = (result["Other"] || 0) + 1;
|
|
4539
5524
|
});
|
|
4540
5525
|
return result;
|
|
4541
5526
|
}
|
|
4542
|
-
/**
|
|
4543
|
-
* Helper: Extract time series data
|
|
4544
|
-
*/
|
|
4545
5527
|
static extractTimeSeriesData(data) {
|
|
4546
5528
|
return data.map((item) => {
|
|
5529
|
+
var _a, _b, _c, _d, _e;
|
|
4547
5530
|
const meta = item.metadata || {};
|
|
4548
5531
|
return {
|
|
4549
|
-
timestamp: meta.timestamp
|
|
4550
|
-
value: meta.value
|
|
4551
|
-
label: meta.label
|
|
5532
|
+
timestamp: (_b = (_a = meta.timestamp) != null ? _a : meta.date) != null ? _b : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5533
|
+
value: (_d = (_c = meta.value) != null ? _c : item.score) != null ? _d : 0,
|
|
5534
|
+
label: (_e = meta.label) != null ? _e : item.content.substring(0, 50)
|
|
4552
5535
|
};
|
|
4553
5536
|
});
|
|
4554
5537
|
}
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
5538
|
+
static extractNumericValue(meta) {
|
|
5539
|
+
var _a;
|
|
5540
|
+
const preferredKeys = [
|
|
5541
|
+
"value",
|
|
5542
|
+
"count",
|
|
5543
|
+
"total",
|
|
5544
|
+
"average",
|
|
5545
|
+
"avg",
|
|
5546
|
+
"amount",
|
|
5547
|
+
"sales",
|
|
5548
|
+
"revenue",
|
|
5549
|
+
"score",
|
|
5550
|
+
"quantity",
|
|
5551
|
+
"price"
|
|
5552
|
+
];
|
|
5553
|
+
for (const key of preferredKeys) {
|
|
5554
|
+
const raw = (_a = resolveMetadataValue(meta, key)) != null ? _a : meta[key];
|
|
5555
|
+
const value = typeof raw === "number" ? raw : Number(String(raw != null ? raw : "").replace(/[$,% ,]/g, ""));
|
|
5556
|
+
if (Number.isFinite(value)) return value;
|
|
5557
|
+
}
|
|
5558
|
+
for (const value of Object.values(meta)) {
|
|
5559
|
+
const numeric = typeof value === "number" ? value : Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
|
|
5560
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
5561
|
+
}
|
|
5562
|
+
return null;
|
|
5563
|
+
}
|
|
5564
|
+
static extractTableColumns(data, query = "") {
|
|
5565
|
+
const q = query.toLowerCase();
|
|
5566
|
+
const availableFields = this.extractAvailableTableFields(data);
|
|
5567
|
+
if (/\b(organization|organisations?|organizations?|company|companies)\b/.test(q) && /\b(employee|employees|staff|headcount|workforce)\b/.test(q)) {
|
|
5568
|
+
const nameField = this.pickTableField(availableFields, [
|
|
5569
|
+
"company name",
|
|
5570
|
+
"organization name",
|
|
5571
|
+
"organisation name",
|
|
5572
|
+
"name",
|
|
5573
|
+
"company",
|
|
5574
|
+
"organization",
|
|
5575
|
+
"organisation"
|
|
5576
|
+
]);
|
|
5577
|
+
const employeeField = this.pickTableField(availableFields, [
|
|
5578
|
+
"number of employees",
|
|
5579
|
+
"employee count",
|
|
5580
|
+
"employees",
|
|
5581
|
+
"employee_count",
|
|
5582
|
+
"number_employees",
|
|
5583
|
+
"num employees",
|
|
5584
|
+
"staff count",
|
|
5585
|
+
"headcount",
|
|
5586
|
+
"workforce"
|
|
5587
|
+
]);
|
|
5588
|
+
const columns = [nameField, employeeField].filter((field) => Boolean(field));
|
|
5589
|
+
if (columns.length > 0) return columns;
|
|
5590
|
+
}
|
|
5591
|
+
const requestedFields = availableFields.map((field) => ({
|
|
5592
|
+
field,
|
|
5593
|
+
score: this.tableFieldQueryScore(field, q)
|
|
5594
|
+
})).filter((item) => item.score > 0).sort((a, b) => b.score - a.score).map((item) => item.field);
|
|
5595
|
+
if (requestedFields.length > 0) {
|
|
5596
|
+
return requestedFields.slice(0, Math.min(6, requestedFields.length));
|
|
5597
|
+
}
|
|
5598
|
+
const metadataFields = Array.from(new Set(
|
|
5599
|
+
data.flatMap((item) => Object.keys(item.metadata || {}).map((k) => this.humanizeFieldName(k)))
|
|
5600
|
+
));
|
|
5601
|
+
return metadataFields.length > 0 ? metadataFields : ["Content"];
|
|
4567
5602
|
}
|
|
4568
|
-
/**
|
|
4569
|
-
* Helper: Extract table row
|
|
4570
|
-
*/
|
|
4571
5603
|
static extractTableRow(item, columns) {
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
5604
|
+
return columns.map((col) => this.resolveTableCellValue(item, col));
|
|
5605
|
+
}
|
|
5606
|
+
static extractAvailableTableFields(data) {
|
|
5607
|
+
const fields = /* @__PURE__ */ new Map();
|
|
5608
|
+
const addField = (field) => {
|
|
5609
|
+
const clean = this.humanizeFieldName(field);
|
|
5610
|
+
if (!clean || /^(content|\[?type\]?)$/i.test(clean)) return;
|
|
5611
|
+
const normalized = this.normalizeComparableField(clean);
|
|
5612
|
+
if (!fields.has(normalized)) fields.set(normalized, clean);
|
|
5613
|
+
};
|
|
5614
|
+
data.forEach((item) => {
|
|
5615
|
+
Object.keys(item.metadata || {}).forEach(addField);
|
|
5616
|
+
for (const match of (item.content || "").matchAll(/(?:^|\n)\s*([A-Za-z][A-Za-z0-9_ /-]{1,50})\s*[:\-–]\s*([^\n]+)/g)) {
|
|
5617
|
+
addField(match[1]);
|
|
4576
5618
|
}
|
|
4577
|
-
const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
|
|
4578
|
-
const value = meta[metaKey];
|
|
4579
|
-
return value !== void 0 ? String(value) : "";
|
|
4580
5619
|
});
|
|
5620
|
+
return Array.from(fields.values());
|
|
5621
|
+
}
|
|
5622
|
+
static pickTableField(fields, aliases) {
|
|
5623
|
+
const normalizedAliases = aliases.map((alias) => this.normalizeComparableField(alias));
|
|
5624
|
+
for (const alias of normalizedAliases) {
|
|
5625
|
+
const exact = fields.find((field) => this.normalizeComparableField(field) === alias);
|
|
5626
|
+
if (exact) return exact;
|
|
5627
|
+
}
|
|
5628
|
+
for (const alias of normalizedAliases) {
|
|
5629
|
+
const fuzzy = fields.find((field) => {
|
|
5630
|
+
const normalizedField = this.normalizeComparableField(field);
|
|
5631
|
+
if (/(id|code|uuid)$/.test(normalizedField) && !/(id|code|uuid)$/.test(alias)) return false;
|
|
5632
|
+
return normalizedField.includes(alias) || alias.includes(normalizedField);
|
|
5633
|
+
});
|
|
5634
|
+
if (fuzzy) return fuzzy;
|
|
5635
|
+
}
|
|
5636
|
+
return void 0;
|
|
5637
|
+
}
|
|
5638
|
+
static tableFieldQueryScore(field, query) {
|
|
5639
|
+
const normalizedField = this.normalizeComparableField(field);
|
|
5640
|
+
if (!normalizedField) return 0;
|
|
5641
|
+
const fieldTokens = this.fieldTokens(field);
|
|
5642
|
+
return fieldTokens.reduce((score, token) => {
|
|
5643
|
+
if (token.length < 3) return score;
|
|
5644
|
+
return query.includes(token) ? score + 1 : score;
|
|
5645
|
+
}, query.includes(normalizedField) ? 3 : 0);
|
|
5646
|
+
}
|
|
5647
|
+
static resolveTableCellValue(item, column) {
|
|
5648
|
+
if (column === "Content") return item.content.substring(0, 100);
|
|
5649
|
+
const meta = item.metadata || {};
|
|
5650
|
+
const normalizedColumn = this.normalizeComparableField(column);
|
|
5651
|
+
const exactMetadata = Object.entries(meta).find(
|
|
5652
|
+
([key]) => this.normalizeComparableField(key) === normalizedColumn || this.normalizeComparableField(this.humanizeFieldName(key)) === normalizedColumn
|
|
5653
|
+
);
|
|
5654
|
+
if (exactMetadata && exactMetadata[1] !== void 0 && exactMetadata[1] !== null) {
|
|
5655
|
+
return this.toDisplayValue(exactMetadata[1]);
|
|
5656
|
+
}
|
|
5657
|
+
const aliasValue = this.resolveAliasedTableCell(meta, column);
|
|
5658
|
+
if (aliasValue !== void 0) return this.toDisplayValue(aliasValue);
|
|
5659
|
+
const contentValue = this.extractContentFieldValue(item.content, column);
|
|
5660
|
+
if (contentValue !== null) return contentValue;
|
|
5661
|
+
const aliasContentValue = this.extractAliasedContentFieldValue(item.content, column);
|
|
5662
|
+
return aliasContentValue != null ? aliasContentValue : "";
|
|
5663
|
+
}
|
|
5664
|
+
static resolveAliasedTableCell(meta, column) {
|
|
5665
|
+
const normalizedColumn = this.normalizeComparableField(column);
|
|
5666
|
+
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"] : [];
|
|
5667
|
+
for (const alias of aliases) {
|
|
5668
|
+
const normalizedAlias = this.normalizeComparableField(alias);
|
|
5669
|
+
const match = Object.entries(meta).find(([key]) => {
|
|
5670
|
+
const normalizedKey = this.normalizeComparableField(key);
|
|
5671
|
+
return normalizedKey === normalizedAlias || normalizedKey.includes(normalizedAlias) || normalizedAlias.includes(normalizedKey);
|
|
5672
|
+
});
|
|
5673
|
+
if (match && match[1] !== void 0 && match[1] !== null) return match[1];
|
|
5674
|
+
}
|
|
5675
|
+
return void 0;
|
|
5676
|
+
}
|
|
5677
|
+
static extractContentFieldValue(content, column) {
|
|
5678
|
+
const escaped = column.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "[\\s_ -]+");
|
|
5679
|
+
const pattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\s*[:\\-\u2013]\\s*([^\\n]+)`, "i");
|
|
5680
|
+
const match = content.match(pattern);
|
|
5681
|
+
return match ? match[1].trim() : null;
|
|
5682
|
+
}
|
|
5683
|
+
static extractAliasedContentFieldValue(content, column) {
|
|
5684
|
+
const normalizedColumn = this.normalizeComparableField(column);
|
|
5685
|
+
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"] : [];
|
|
5686
|
+
for (const alias of aliases) {
|
|
5687
|
+
const value = this.extractContentFieldValue(content, alias);
|
|
5688
|
+
if (value !== null) return value;
|
|
5689
|
+
}
|
|
5690
|
+
return null;
|
|
5691
|
+
}
|
|
5692
|
+
static toDisplayValue(value) {
|
|
5693
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
5694
|
+
return String(value != null ? value : "");
|
|
4581
5695
|
}
|
|
4582
|
-
/**
|
|
4583
|
-
* Helper: Calculate stock counts by category
|
|
4584
|
-
*/
|
|
4585
5696
|
static calculateStockCounts(category, data) {
|
|
4586
5697
|
let inStock = 0;
|
|
4587
5698
|
let outOfStock = 0;
|
|
4588
5699
|
data.forEach((d) => {
|
|
4589
|
-
|
|
4590
|
-
const
|
|
4591
|
-
if (
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
outOfStock++;
|
|
4596
|
-
}
|
|
5700
|
+
var _a, _b;
|
|
5701
|
+
const cat = (_a = this.getProductCategory(d)) != null ? _a : "Other";
|
|
5702
|
+
if (cat === category) {
|
|
5703
|
+
const quantity = (_b = this.extractStockQuantity(d)) != null ? _b : 1;
|
|
5704
|
+
if (this.determineStockStatus(d)) inStock += quantity;
|
|
5705
|
+
else outOfStock += quantity;
|
|
4597
5706
|
}
|
|
4598
5707
|
});
|
|
4599
5708
|
return { inStockCount: inStock, outOfStockCount: outOfStock };
|
|
4600
5709
|
}
|
|
4601
|
-
/**
|
|
4602
|
-
* Helper: Determine if item is in stock
|
|
4603
|
-
*/
|
|
4604
5710
|
static determineStockStatus(item) {
|
|
4605
5711
|
const meta = item.metadata || {};
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
5712
|
+
const stockValue = resolveMetadataValue(meta, "stock");
|
|
5713
|
+
if (stockValue !== void 0) {
|
|
5714
|
+
const normalized = String(stockValue).toLowerCase();
|
|
5715
|
+
if (/out[_\s-]?of[_\s-]?stock|unavailable|false|no|sold out|0/.test(normalized)) return false;
|
|
5716
|
+
if (/in[_\s-]?stock|available|true|yes/.test(normalized)) return true;
|
|
5717
|
+
const numeric = this.toFiniteNumber(stockValue);
|
|
5718
|
+
if (numeric !== null) return numeric > 0;
|
|
5719
|
+
}
|
|
5720
|
+
if (meta.inStock !== void 0) return Boolean(meta.inStock);
|
|
5721
|
+
if (meta.stock !== void 0) return Boolean(meta.stock);
|
|
5722
|
+
if (meta.available !== void 0) return Boolean(meta.available);
|
|
4615
5723
|
const content = (item.content || "").toLowerCase();
|
|
4616
|
-
if (
|
|
4617
|
-
|
|
5724
|
+
if (/out[_\s-]?of[_\s-]?stock|unavailable|sold out/.test(content)) return false;
|
|
5725
|
+
if (/in[_\s-]?stock|available/.test(content)) return true;
|
|
5726
|
+
return true;
|
|
5727
|
+
}
|
|
5728
|
+
static extractStockQuantity(item) {
|
|
5729
|
+
const meta = item.metadata || {};
|
|
5730
|
+
const stockValue = resolveMetadataValue(meta, "stock");
|
|
5731
|
+
const numericStock = this.toFiniteNumber(stockValue);
|
|
5732
|
+
if (numericStock !== null) return numericStock;
|
|
5733
|
+
const content = item.content || "";
|
|
5734
|
+
const stockMatch = content.match(/\b(?:stock|inventory|quantity|count)\s*[:\-–]?\s*([\d,]+)/i);
|
|
5735
|
+
if (!stockMatch) return null;
|
|
5736
|
+
return this.toFiniteNumber(stockMatch[1]);
|
|
5737
|
+
}
|
|
5738
|
+
static toFiniteNumber(value) {
|
|
5739
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
5740
|
+
const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
|
|
5741
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
5742
|
+
}
|
|
5743
|
+
// ─── Product Extraction ───────────────────────────────────────────────────
|
|
5744
|
+
static getDynamicVal(meta, uiKey, config, trainedSchema) {
|
|
5745
|
+
var _a;
|
|
5746
|
+
if (!meta) return void 0;
|
|
5747
|
+
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
5748
|
+
if ((mapping == null ? void 0 : mapping[uiKey]) && meta[mapping[uiKey]] !== void 0) return meta[mapping[uiKey]];
|
|
5749
|
+
if (trainedSchema && typeof trainedSchema === "object") {
|
|
5750
|
+
const trainedKey = trainedSchema[uiKey];
|
|
5751
|
+
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
4618
5752
|
}
|
|
4619
|
-
|
|
4620
|
-
|
|
5753
|
+
return resolveMetadataValue(meta, uiKey);
|
|
5754
|
+
}
|
|
5755
|
+
static extractProductInfo(item, config, trainedSchema) {
|
|
5756
|
+
var _a;
|
|
5757
|
+
const meta = item.metadata || {};
|
|
5758
|
+
const name = this.getDynamicVal(meta, "name", config, trainedSchema);
|
|
5759
|
+
const price = this.getDynamicVal(meta, "price", config, trainedSchema);
|
|
5760
|
+
const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
|
|
5761
|
+
const description = this.cleanProductDescription(
|
|
5762
|
+
(_a = this.extractProductDescriptionFromContent(item.content)) != null ? _a : this.getProductDescriptionValue(meta, config, trainedSchema)
|
|
5763
|
+
);
|
|
5764
|
+
if (name || this.isProductData(item)) {
|
|
5765
|
+
let finalName = name ? String(name) : void 0;
|
|
5766
|
+
if (!finalName) {
|
|
5767
|
+
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
5768
|
+
finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
|
|
5769
|
+
}
|
|
5770
|
+
let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
|
|
5771
|
+
if (!finalPrice) {
|
|
5772
|
+
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
|
|
5773
|
+
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
|
|
5774
|
+
}
|
|
5775
|
+
const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
|
|
5776
|
+
return {
|
|
5777
|
+
id: item.id,
|
|
5778
|
+
name: finalName,
|
|
5779
|
+
price: finalPrice,
|
|
5780
|
+
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
5781
|
+
brand: brand ? String(brand) : void 0,
|
|
5782
|
+
description,
|
|
5783
|
+
inStock: this.determineStockStatus(item)
|
|
5784
|
+
};
|
|
4621
5785
|
}
|
|
4622
|
-
return
|
|
5786
|
+
return null;
|
|
4623
5787
|
}
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
return fieldCount.size > 2;
|
|
5788
|
+
static extractProductDescriptionFromContent(content) {
|
|
5789
|
+
const bodyMatch = content.match(
|
|
5790
|
+
/\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
|
|
5791
|
+
);
|
|
5792
|
+
if (bodyMatch == null ? void 0 : bodyMatch[1]) return bodyMatch[1];
|
|
5793
|
+
const descriptionMatch = content.match(
|
|
5794
|
+
/\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
|
|
5795
|
+
);
|
|
5796
|
+
if (descriptionMatch == null ? void 0 : descriptionMatch[1]) return descriptionMatch[1];
|
|
5797
|
+
return null;
|
|
4635
5798
|
}
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
* @param sources - vector DB matches returned by RAG retrieval
|
|
4652
|
-
* @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
|
|
4653
|
-
* @returns - a validated UITransformationResponse (type + title + description + data)
|
|
4654
|
-
*/
|
|
4655
|
-
static async analyzeAndDecide(query, sources, llm) {
|
|
4656
|
-
try {
|
|
4657
|
-
const context = this.buildContextSummary(sources);
|
|
4658
|
-
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
4659
|
-
const userPrompt = [
|
|
4660
|
-
`USER QUESTION: ${query}`,
|
|
4661
|
-
"",
|
|
4662
|
-
"RETRIEVED DATA (JSON):",
|
|
4663
|
-
context
|
|
4664
|
-
].join("\n");
|
|
4665
|
-
const rawResponse = await llm.chat(
|
|
4666
|
-
[{ role: "user", content: userPrompt }],
|
|
4667
|
-
"",
|
|
4668
|
-
{ systemPrompt, temperature: 0 }
|
|
5799
|
+
static getProductDescriptionValue(meta, config, trainedSchema) {
|
|
5800
|
+
const mapped = this.getDynamicVal(meta, "description", config, trainedSchema);
|
|
5801
|
+
if (mapped !== void 0 && mapped !== meta.content && mapped !== meta.text) return mapped;
|
|
5802
|
+
const preferredKeys = [
|
|
5803
|
+
"body_html",
|
|
5804
|
+
"body html",
|
|
5805
|
+
"bodyHtml",
|
|
5806
|
+
"description",
|
|
5807
|
+
"summary",
|
|
5808
|
+
"details",
|
|
5809
|
+
"body"
|
|
5810
|
+
];
|
|
5811
|
+
for (const key of preferredKeys) {
|
|
5812
|
+
const match = Object.keys(meta).find(
|
|
5813
|
+
(candidate) => this.normalizeComparableField(candidate) === this.normalizeComparableField(key)
|
|
4669
5814
|
);
|
|
4670
|
-
|
|
4671
|
-
if (parsed) {
|
|
4672
|
-
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
4673
|
-
return parsed;
|
|
4674
|
-
}
|
|
4675
|
-
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
4676
|
-
} catch (err) {
|
|
4677
|
-
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
5815
|
+
if (match && meta[match] !== void 0 && meta[match] !== null) return meta[match];
|
|
4678
5816
|
}
|
|
4679
|
-
return
|
|
5817
|
+
return void 0;
|
|
4680
5818
|
}
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
5819
|
+
static cleanProductDescription(raw) {
|
|
5820
|
+
if (raw === null || raw === void 0) return void 0;
|
|
5821
|
+
const extracted = this.extractProductDescriptionFromContent(String(raw));
|
|
5822
|
+
if (extracted && extracted !== String(raw)) return this.cleanProductDescription(extracted);
|
|
5823
|
+
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();
|
|
5824
|
+
return text || void 0;
|
|
5825
|
+
}
|
|
5826
|
+
// ─── Visualization Prompt ─────────────────────────────────────────────────
|
|
4684
5827
|
static buildVisualizationSystemPrompt() {
|
|
4685
5828
|
return `You are a data visualization expert embedded in a RAG chat system.
|
|
4686
|
-
You will receive a user question and structured data
|
|
4687
|
-
Your ONLY job is to
|
|
4688
|
-
|
|
5829
|
+
You will receive a user question, a pre-computed INTENT object, and structured data from a vector database.
|
|
5830
|
+
Your ONLY job is to return a single JSON object describing how to visualize the data.
|
|
5831
|
+
The INTENT object is authoritative \u2014 honor it. For example:
|
|
5832
|
+
- If intent.wantsExplicitTable is false, never return type "table".
|
|
5833
|
+
- If intent.isTemporal is true, prefer line_chart.
|
|
5834
|
+
- If intent.visualizationHint is "comparison" or "ranking", prefer bar_chart.
|
|
5835
|
+
- If intent.visualizationHint is "composition", prefer pie_chart.
|
|
5836
|
+
- If intent.recommendedChart is unsupported by the response schema, use the nearest supported type.
|
|
5837
|
+
- Always respond in the language specified by intent.language.
|
|
4689
5838
|
|
|
4690
|
-
Return ONLY a valid JSON object \u2014 no markdown
|
|
5839
|
+
Return ONLY a valid JSON object \u2014 no markdown fences, no prose.
|
|
4691
5840
|
|
|
4692
|
-
The JSON must have this exact shape:
|
|
4693
5841
|
{
|
|
4694
|
-
"type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
|
|
4695
|
-
"title": "
|
|
4696
|
-
"description": "
|
|
4697
|
-
"data": <structured data
|
|
5842
|
+
"type": "bar_chart" | "horizontal_bar" | "line_chart" | "pie_chart" | "histogram" | "scatter_plot" | "radar_chart" | "metric_card" | "geo_map" | "table" | "text",
|
|
5843
|
+
"title": "concise descriptive title",
|
|
5844
|
+
"description": "one sentence describing the visualization",
|
|
5845
|
+
"data": <structured data per type below>
|
|
4698
5846
|
}
|
|
4699
5847
|
|
|
4700
|
-
DATA
|
|
4701
|
-
- bar_chart:
|
|
4702
|
-
-
|
|
4703
|
-
-
|
|
4704
|
-
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
-
|
|
4712
|
-
- text: { "content": "<prose answer>" }
|
|
5848
|
+
DATA SHAPES:
|
|
5849
|
+
- bar_chart: [{ "category": string, "value": number }]
|
|
5850
|
+
- horizontal_bar: [{ "category": string, "value": number }]
|
|
5851
|
+
- histogram: [{ "category": string, "value": number }]
|
|
5852
|
+
- line_chart: [{ "timestamp": string, "value": number, "label": string }]
|
|
5853
|
+
- pie_chart: [{ "label": string, "value": number }]
|
|
5854
|
+
- scatter_plot:[{ "x": number, "y": number, "label": string }]
|
|
5855
|
+
- radar_chart: [{ "attribute": string, "<series1>": number, "<series2>": number, ... }]
|
|
5856
|
+
- metric_card: { "label": string, "value": number, "operation": "sum"|"average"|"count"|"min"|"max"|"median" }
|
|
5857
|
+
- geo_map: [{ "category": string, "value": number }]
|
|
5858
|
+
- table: { "columns": string[], "rows": (string|number)[][] }
|
|
5859
|
+
- text: { "content": "A concise plain-language answer." }
|
|
4713
5860
|
|
|
4714
|
-
|
|
4715
|
-
1.
|
|
4716
|
-
2.
|
|
4717
|
-
3.
|
|
4718
|
-
4.
|
|
4719
|
-
5.
|
|
4720
|
-
6. text \u2192 conversational or free-form answers where no chart adds value
|
|
4721
|
-
|
|
4722
|
-
IMPORTANT:
|
|
4723
|
-
- Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
|
|
4724
|
-
- Ensure all "value" fields are numbers, not strings.
|
|
4725
|
-
- For bar/line/pie, keep at most 12 data points for readability.
|
|
4726
|
-
- Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
|
|
5861
|
+
RULES:
|
|
5862
|
+
1. Aggregate values \u2014 never pass raw nested objects in chart arrays.
|
|
5863
|
+
2. All "value" fields must be numbers.
|
|
5864
|
+
3. Cap bar/line/pie at 12 data points.
|
|
5865
|
+
4. Use dollar signs only for monetary prices, not for stock quantities or years.
|
|
5866
|
+
5. If no relevant data is found, return text: "I cannot answer this question based on the available information."`;
|
|
4727
5867
|
}
|
|
4728
|
-
/**
|
|
4729
|
-
* Serialize retrieved vector matches into a compact JSON context string.
|
|
4730
|
-
* Limits the total character count to avoid exceeding LLM context windows.
|
|
4731
|
-
*/
|
|
4732
5868
|
static buildContextSummary(sources, maxChars = 6e3) {
|
|
4733
5869
|
const items = sources.map((s, i) => {
|
|
4734
5870
|
var _a, _b, _c, _d;
|
|
@@ -4772,14 +5908,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
|
|
|
4772
5908
|
Identify which keys best correspond to these standard UI properties:
|
|
4773
5909
|
${propertyList}
|
|
4774
5910
|
|
|
4775
|
-
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
5911
|
+
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.
|
|
4776
5912
|
If no good match is found for a property, omit it.
|
|
4777
5913
|
|
|
4778
5914
|
Example:
|
|
4779
5915
|
{
|
|
4780
|
-
"name": "
|
|
4781
|
-
"price": "
|
|
4782
|
-
"brand": "
|
|
5916
|
+
"name": "Title",
|
|
5917
|
+
"price": "Variant Price",
|
|
5918
|
+
"brand": "Vendor",
|
|
5919
|
+
"image": "Image Src",
|
|
5920
|
+
"stock": "Variant Inventory Qty"
|
|
4783
5921
|
}
|
|
4784
5922
|
`;
|
|
4785
5923
|
try {
|
|
@@ -4949,27 +6087,38 @@ var Pipeline = class {
|
|
|
4949
6087
|
async initialize() {
|
|
4950
6088
|
var _a;
|
|
4951
6089
|
if (this.initialised) return;
|
|
4952
|
-
const chartInstruction = `You are a helpful
|
|
6090
|
+
const chartInstruction = `You are a helpful assistant. Use the provided context to answer questions accurately.
|
|
4953
6091
|
|
|
4954
|
-
###
|
|
6092
|
+
### CRITICAL RULES:
|
|
6093
|
+
- ONLY answer the user's specific question. Do NOT suggest or recommend unrelated products unless specifically asked.
|
|
4955
6094
|
- NEVER generate markdown tables. If you do, the UI will break.
|
|
4956
6095
|
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
4957
6096
|
- NEVER generate text-based charts or graphs.
|
|
6097
|
+
- NEVER say you cannot display, render, draw, or create a chart when the user asks for a visualization. The UI handles visual rendering separately.
|
|
4958
6098
|
- ONLY use plain text and bullet points.
|
|
6099
|
+
- NEVER use the plus sign (+) as a separator between names, categories, or products. Use commas or bullet points.
|
|
6100
|
+
- ONLY answer the question if the sources contain relevant information to answer it, else say that you cannot answer the question.
|
|
6101
|
+
- If answer cannot be found in the sources, say that you cannot answer the question and do not hallucinate.
|
|
6102
|
+
- Do not use information from previous turns to answer the question.
|
|
6103
|
+
- 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.
|
|
4959
6104
|
|
|
4960
6105
|
### PRODUCT DISPLAY:
|
|
4961
|
-
-
|
|
6106
|
+
- Recommended Products should only be displayed when the user explicitly asks for products.
|
|
6107
|
+
- When recommending products (ONLY when asked), simply list their names, prices, and features in a friendly, conversational manner.
|
|
4962
6108
|
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
6109
|
+
- 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.
|
|
4963
6110
|
- Do NOT try to format product lists as tables.
|
|
4964
6111
|
`;
|
|
4965
6112
|
this.config.llm.systemPrompt = chartInstruction;
|
|
4966
6113
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
4967
|
-
|
|
6114
|
+
this.llmRouter = new LLMRouter(this.config);
|
|
6115
|
+
const { llmProvider: resolvedLLM, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
|
|
4968
6116
|
this.config.llm,
|
|
4969
6117
|
this.config.embedding
|
|
4970
6118
|
);
|
|
4971
|
-
this.llmProvider = llmProvider;
|
|
4972
6119
|
this.embeddingProvider = embeddingProvider;
|
|
6120
|
+
await this.llmRouter.initialize(resolvedLLM);
|
|
6121
|
+
this.llmProvider = this.llmRouter.get("default");
|
|
4973
6122
|
if (this.config.graphDb) {
|
|
4974
6123
|
this.graphDB = await ProviderRegistry.createGraphProvider(this.config.graphDb);
|
|
4975
6124
|
await this.graphDB.initialize();
|
|
@@ -5059,7 +6208,10 @@ var Pipeline = class {
|
|
|
5059
6208
|
upsertBatchOptions
|
|
5060
6209
|
);
|
|
5061
6210
|
if (upsertResult.errors.length > 0) {
|
|
5062
|
-
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed
|
|
6211
|
+
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
|
|
6212
|
+
upsertResult.errors.forEach((err, idx) => {
|
|
6213
|
+
console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
|
|
6214
|
+
});
|
|
5063
6215
|
}
|
|
5064
6216
|
return upsertResult.totalProcessed;
|
|
5065
6217
|
}
|
|
@@ -5126,10 +6278,16 @@ var Pipeline = class {
|
|
|
5126
6278
|
/**
|
|
5127
6279
|
* High-performance streaming RAG flow.
|
|
5128
6280
|
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
6281
|
+
*
|
|
6282
|
+
* Latency optimizations:
|
|
6283
|
+
* - Strategy classification runs in parallel with query embedding (saves ~400ms)
|
|
6284
|
+
* - Hallucination scoring is fire-and-forget (doesn't block metadata yield)
|
|
6285
|
+
* - UITransformation is computed after text streaming and emitted with metadata
|
|
6286
|
+
* - SchemaMapper.train runs while answer generation streams
|
|
5129
6287
|
*/
|
|
5130
6288
|
askStream(_0) {
|
|
5131
6289
|
return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
|
|
5132
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
6290
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
5133
6291
|
yield new __await(this.initialize());
|
|
5134
6292
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
5135
6293
|
const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
|
|
@@ -5145,27 +6303,61 @@ var Pipeline = class {
|
|
|
5145
6303
|
}
|
|
5146
6304
|
const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
|
|
5147
6305
|
const filter = QueryProcessor.buildQueryFilter(question, hints);
|
|
6306
|
+
const numericPredicates = QueryProcessor.extractNumericPredicates(question, (_g = this.config.rag) == null ? void 0 : _g.filterableFields);
|
|
6307
|
+
if (numericPredicates.length > 0) {
|
|
6308
|
+
filter.__numericPredicates = numericPredicates;
|
|
6309
|
+
}
|
|
5148
6310
|
const embedStart = performance.now();
|
|
5149
|
-
const
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
6311
|
+
const cacheKey = `${ns}::${searchQuery}`;
|
|
6312
|
+
const cachedVector = this.embeddingCache.get(cacheKey);
|
|
6313
|
+
const [strategyResult, embeddedVector] = yield new __await(Promise.all([
|
|
6314
|
+
QueryProcessor.determineRetrievalStrategy(
|
|
6315
|
+
searchQuery,
|
|
6316
|
+
this.llmRouter.get("fast"),
|
|
6317
|
+
(_h = this.config.rag) == null ? void 0 : _h.graphKeywords,
|
|
6318
|
+
(_i = this.config.rag) == null ? void 0 : _i.vectorKeywords
|
|
6319
|
+
),
|
|
6320
|
+
// Embed immediately regardless of strategy — costs nothing if cached.
|
|
6321
|
+
// If strategy turns out to be 'graph'-only we just won't use the vector.
|
|
6322
|
+
cachedVector ? Promise.resolve(cachedVector) : this.embeddingProvider.embed(searchQuery, { taskType: "query" })
|
|
6323
|
+
]));
|
|
6324
|
+
const queryVector = embeddedVector;
|
|
6325
|
+
if (!cachedVector && queryVector.length > 0) {
|
|
6326
|
+
this.embeddingCache.set(cacheKey, queryVector);
|
|
6327
|
+
}
|
|
6328
|
+
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;
|
|
6329
|
+
const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
|
|
6330
|
+
const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
|
|
6331
|
+
const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
|
|
6332
|
+
const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
|
|
6333
|
+
const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
|
|
5154
6334
|
const retrieveEnd = performance.now();
|
|
5155
6335
|
const embedMs = retrieveEnd - embedStart;
|
|
5156
6336
|
const retrieveMs = retrieveEnd - embedStart;
|
|
5157
6337
|
const rerankStart = performance.now();
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
6338
|
+
const structuredSources = this.applyStructuredFilters(rawSources, filter);
|
|
6339
|
+
let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
|
|
6340
|
+
const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
|
|
6341
|
+
if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
|
|
6342
|
+
fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
|
|
6343
|
+
} else if (!wantsExhaustiveList) {
|
|
6344
|
+
fullSources = fullSources.slice(0, topK);
|
|
5163
6345
|
}
|
|
5164
6346
|
const rerankMs = performance.now() - rerankStart;
|
|
5165
|
-
let context =
|
|
6347
|
+
let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
|
|
5166
6348
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
6349
|
+
let displayCount = 15;
|
|
6350
|
+
if (hasMetadataFilter) {
|
|
6351
|
+
displayCount = fullSources.length;
|
|
6352
|
+
} else {
|
|
6353
|
+
const highlyRelevant = fullSources.filter((m) => m.score >= 0.4);
|
|
6354
|
+
displayCount = Math.max(highlyRelevant.length, topK);
|
|
6355
|
+
if (displayCount > 15) {
|
|
6356
|
+
displayCount = 15;
|
|
6357
|
+
}
|
|
6358
|
+
}
|
|
6359
|
+
const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
|
|
5167
6360
|
if (graphData && graphData.nodes.length > 0) {
|
|
5168
|
-
console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
|
|
5169
6361
|
const graphContext = graphData.nodes.map(
|
|
5170
6362
|
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
|
5171
6363
|
).join("\n");
|
|
@@ -5176,20 +6368,29 @@ VECTOR CONTEXT:
|
|
|
5176
6368
|
${context}`;
|
|
5177
6369
|
}
|
|
5178
6370
|
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
5179
|
-
const
|
|
5180
|
-
const
|
|
6371
|
+
const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
|
|
6372
|
+
const uiTransformationPromise = trainedSchemaPromise.then(
|
|
6373
|
+
(trainedSchema) => this.generateUiTransformation(
|
|
6374
|
+
question,
|
|
6375
|
+
sources,
|
|
6376
|
+
trainedSchema,
|
|
6377
|
+
hasNumericPredicates
|
|
6378
|
+
)
|
|
6379
|
+
).catch((uiError) => {
|
|
6380
|
+
console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
|
|
6381
|
+
return UITransformer.transform(question, sources, this.config);
|
|
6382
|
+
});
|
|
6383
|
+
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.)";
|
|
5181
6384
|
const hardenedHistory = [...history];
|
|
5182
6385
|
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
5183
6386
|
const messages = [...hardenedHistory, userQuestion];
|
|
5184
|
-
const systemPrompt = (
|
|
6387
|
+
const systemPrompt = (_l = this.config.llm.systemPrompt) != null ? _l : "";
|
|
5185
6388
|
const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
5186
6389
|
let fullReply = "";
|
|
5187
6390
|
const generateStart = performance.now();
|
|
5188
6391
|
if (this.llmProvider.chatStream) {
|
|
5189
6392
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
5190
|
-
if (!stream) {
|
|
5191
|
-
throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
5192
|
-
}
|
|
6393
|
+
if (!stream) throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
5193
6394
|
try {
|
|
5194
6395
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
5195
6396
|
const chunk = temp.value;
|
|
@@ -5216,7 +6417,7 @@ ${context}`;
|
|
|
5216
6417
|
const latency = {
|
|
5217
6418
|
embedMs: Math.round(embedMs),
|
|
5218
6419
|
retrieveMs: Math.round(retrieveMs),
|
|
5219
|
-
rerankMs: ((
|
|
6420
|
+
rerankMs: ((_m = this.config.rag) == null ? void 0 : _m.useReranking) ? Math.round(rerankMs) : void 0,
|
|
5220
6421
|
generateMs: Math.round(generateMs),
|
|
5221
6422
|
totalMs: Math.round(totalMs)
|
|
5222
6423
|
};
|
|
@@ -5229,9 +6430,6 @@ ${context}`;
|
|
|
5229
6430
|
totalTokens: promptTokens + completionTokens,
|
|
5230
6431
|
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
|
|
5231
6432
|
};
|
|
5232
|
-
const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
|
|
5233
|
-
const trainedSchema = yield new __await(trainingPromise);
|
|
5234
|
-
const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, trainedSchema));
|
|
5235
6433
|
const trace = {
|
|
5236
6434
|
requestId,
|
|
5237
6435
|
query: question,
|
|
@@ -5250,10 +6448,9 @@ ${context}`;
|
|
|
5250
6448
|
}),
|
|
5251
6449
|
latency,
|
|
5252
6450
|
tokens,
|
|
5253
|
-
hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
|
|
5254
|
-
hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
|
|
5255
6451
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5256
6452
|
};
|
|
6453
|
+
const uiTransformation = yield new __await(uiTransformationPromise);
|
|
5257
6454
|
yield {
|
|
5258
6455
|
reply: "",
|
|
5259
6456
|
sources,
|
|
@@ -5261,6 +6458,13 @@ ${context}`;
|
|
|
5261
6458
|
ui_transformation: uiTransformation,
|
|
5262
6459
|
trace
|
|
5263
6460
|
};
|
|
6461
|
+
scoreHallucination(this.llmProvider, fullReply, context).then((hallucinationResult) => {
|
|
6462
|
+
if (hallucinationResult) {
|
|
6463
|
+
trace.hallucinationScore = hallucinationResult.score;
|
|
6464
|
+
trace.hallucinationReason = hallucinationResult.reason;
|
|
6465
|
+
}
|
|
6466
|
+
}).catch(() => {
|
|
6467
|
+
});
|
|
5264
6468
|
} catch (error2) {
|
|
5265
6469
|
throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
5266
6470
|
}
|
|
@@ -5270,24 +6474,107 @@ ${context}`;
|
|
|
5270
6474
|
* Universal retrieval method combining all enabled providers.
|
|
5271
6475
|
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
5272
6476
|
*/
|
|
5273
|
-
async generateUiTransformation(question, sources, trainedSchema) {
|
|
6477
|
+
async generateUiTransformation(question, sources, trainedSchema, forceDeterministic = false) {
|
|
5274
6478
|
if (!sources || sources.length === 0) {
|
|
5275
6479
|
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5276
6480
|
}
|
|
6481
|
+
if (forceDeterministic) {
|
|
6482
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
6483
|
+
}
|
|
5277
6484
|
try {
|
|
5278
|
-
return await UITransformer.analyzeAndDecide(question, sources, this.
|
|
6485
|
+
return await UITransformer.analyzeAndDecide(question, sources, this.llmRouter.get("fast"));
|
|
5279
6486
|
} catch (err) {
|
|
5280
6487
|
console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
|
|
5281
6488
|
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5282
6489
|
}
|
|
5283
6490
|
}
|
|
6491
|
+
applyStructuredFilters(sources, filter) {
|
|
6492
|
+
const predicates = Array.isArray(filter.__numericPredicates) ? filter.__numericPredicates : [];
|
|
6493
|
+
if (predicates.length === 0) return sources;
|
|
6494
|
+
return sources.filter((source) => predicates.every((predicate) => {
|
|
6495
|
+
const value = this.resolveNumericPredicateValue(source, predicate);
|
|
6496
|
+
return value !== null && this.matchesNumericPredicate(value, predicate);
|
|
6497
|
+
})).sort((a, b) => {
|
|
6498
|
+
var _a, _b;
|
|
6499
|
+
const primary = predicates[0];
|
|
6500
|
+
const aValue = (_a = this.resolveNumericPredicateValue(a, primary)) != null ? _a : 0;
|
|
6501
|
+
const bValue = (_b = this.resolveNumericPredicateValue(b, primary)) != null ? _b : 0;
|
|
6502
|
+
return primary.operator === "lt" || primary.operator === "lte" ? aValue - bValue : bValue - aValue;
|
|
6503
|
+
});
|
|
6504
|
+
}
|
|
6505
|
+
resolveNumericPredicateValue(source, predicate) {
|
|
6506
|
+
const meta = source.metadata || {};
|
|
6507
|
+
const field = predicate.field;
|
|
6508
|
+
const entries = Object.entries(meta).filter(([, value]) => value !== null && value !== void 0 && typeof value !== "object");
|
|
6509
|
+
if (field) {
|
|
6510
|
+
const normalizedField = this.normalizeComparableField(field);
|
|
6511
|
+
const exact = entries.find(([key]) => this.normalizeComparableField(key) === normalizedField);
|
|
6512
|
+
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];
|
|
6513
|
+
if (fuzzy) {
|
|
6514
|
+
const value = this.toFiniteNumber(Array.isArray(fuzzy) ? fuzzy[1] : fuzzy.value);
|
|
6515
|
+
if (value !== null) return value;
|
|
6516
|
+
}
|
|
6517
|
+
const contentValue = this.extractNumericValueFromContent(source.content, field);
|
|
6518
|
+
if (contentValue !== null) return contentValue;
|
|
6519
|
+
}
|
|
6520
|
+
for (const [key, value] of entries) {
|
|
6521
|
+
if (/(id|sku|ean|upc|phone|zip|postal|code)/i.test(key)) continue;
|
|
6522
|
+
const numeric = this.toFiniteNumber(value);
|
|
6523
|
+
if (numeric !== null) return numeric;
|
|
6524
|
+
}
|
|
6525
|
+
return null;
|
|
6526
|
+
}
|
|
6527
|
+
extractNumericValueFromContent(content, field) {
|
|
6528
|
+
const escapedWords = field.split(/\s+|_+|-+/).filter(Boolean).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
6529
|
+
if (escapedWords.length === 0) return null;
|
|
6530
|
+
const pattern = new RegExp(`(?:${escapedWords.join("[\\\\s_:-]*")})\\s*[:\\-\u2013]?\\s*([\\d,]+(?:\\.\\d+)?)`, "i");
|
|
6531
|
+
const match = content.match(pattern);
|
|
6532
|
+
return match ? this.toFiniteNumber(match[1]) : null;
|
|
6533
|
+
}
|
|
6534
|
+
matchesNumericPredicate(value, predicate) {
|
|
6535
|
+
switch (predicate.operator) {
|
|
6536
|
+
case "gt":
|
|
6537
|
+
return value > predicate.value;
|
|
6538
|
+
case "gte":
|
|
6539
|
+
return value >= predicate.value;
|
|
6540
|
+
case "lt":
|
|
6541
|
+
return value < predicate.value;
|
|
6542
|
+
case "lte":
|
|
6543
|
+
return value <= predicate.value;
|
|
6544
|
+
case "eq":
|
|
6545
|
+
default:
|
|
6546
|
+
return value === predicate.value;
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
normalizeComparableField(value) {
|
|
6550
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6551
|
+
}
|
|
6552
|
+
fieldSimilarityScore(candidate, requested) {
|
|
6553
|
+
const normalizedCandidate = this.normalizeComparableField(candidate);
|
|
6554
|
+
const normalizedRequested = this.normalizeComparableField(requested);
|
|
6555
|
+
if (normalizedCandidate === normalizedRequested) return 10;
|
|
6556
|
+
if (normalizedCandidate.includes(normalizedRequested) || normalizedRequested.includes(normalizedCandidate)) return 8;
|
|
6557
|
+
const candidateTokens = this.fieldTokens(candidate);
|
|
6558
|
+
const requestedTokens = this.fieldTokens(requested);
|
|
6559
|
+
const overlap = requestedTokens.filter((token) => candidateTokens.includes(token)).length;
|
|
6560
|
+
if (overlap === 0) return 0;
|
|
6561
|
+
return overlap / Math.max(requestedTokens.length, candidateTokens.length);
|
|
6562
|
+
}
|
|
6563
|
+
fieldTokens(value) {
|
|
6564
|
+
return value.toLowerCase().split(/[^a-z0-9]+/).map((token) => token.replace(/ies$/, "y").replace(/s$/, "")).filter((token) => token.length > 1);
|
|
6565
|
+
}
|
|
6566
|
+
toFiniteNumber(value) {
|
|
6567
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
6568
|
+
const numeric = Number(String(value != null ? value : "").replace(/[$,% ,]/g, ""));
|
|
6569
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
6570
|
+
}
|
|
5284
6571
|
async retrieve(query, options) {
|
|
5285
|
-
var _a, _b, _c;
|
|
6572
|
+
var _a, _b, _c, _d, _e, _f;
|
|
5286
6573
|
const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
|
|
5287
6574
|
const topK = (_b = options.topK) != null ? _b : 5;
|
|
5288
6575
|
const cacheKey = `${ns}::${query}`;
|
|
5289
6576
|
let queryVector = this.embeddingCache.get(cacheKey);
|
|
5290
|
-
const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.
|
|
6577
|
+
const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmRouter.get("fast"));
|
|
5291
6578
|
console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
|
|
5292
6579
|
const [retrievedVector, graphData] = await Promise.all([
|
|
5293
6580
|
// Only embed if we need vector search (strategy is 'vector' or 'both')
|
|
@@ -5299,8 +6586,27 @@ ${context}`;
|
|
|
5299
6586
|
this.embeddingCache.set(cacheKey, retrievedVector);
|
|
5300
6587
|
queryVector = retrievedVector;
|
|
5301
6588
|
}
|
|
5302
|
-
const
|
|
5303
|
-
|
|
6589
|
+
const baseFilter = __spreadProps(__spreadValues({}, (_d = options.filter) != null ? _d : {}), { queryText: query });
|
|
6590
|
+
const numericPredicates = QueryProcessor.extractNumericPredicates(query, (_e = this.config.rag) == null ? void 0 : _e.filterableFields);
|
|
6591
|
+
if (numericPredicates.length > 0) {
|
|
6592
|
+
baseFilter.__numericPredicates = numericPredicates;
|
|
6593
|
+
}
|
|
6594
|
+
const retrievalLimit = numericPredicates.length > 0 ? Math.max(topK * 20, 100) : topK;
|
|
6595
|
+
const sources = (strategy === "vector" || strategy === "both") && queryVector && queryVector.length > 0 ? this.applyStructuredFilters(
|
|
6596
|
+
await this.vectorDB.query(queryVector, retrievalLimit, ns, baseFilter),
|
|
6597
|
+
baseFilter
|
|
6598
|
+
) : [];
|
|
6599
|
+
const resolvedSources = [];
|
|
6600
|
+
for (const source of sources) {
|
|
6601
|
+
const parentId = (_f = source.metadata) == null ? void 0 : _f.parent_id;
|
|
6602
|
+
if (parentId) {
|
|
6603
|
+
console.log(`[Pipeline] Multi-Vector: Found child chunk. Parent ID: ${parentId}`);
|
|
6604
|
+
resolvedSources.push(source);
|
|
6605
|
+
} else {
|
|
6606
|
+
resolvedSources.push(source);
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
return { sources: resolvedSources, graphData };
|
|
5304
6610
|
}
|
|
5305
6611
|
/** Rewrite the user query for better retrieval performance. */
|
|
5306
6612
|
async rewriteQuery(question, history) {
|
|
@@ -5664,15 +6970,24 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5664
6970
|
});
|
|
5665
6971
|
}
|
|
5666
6972
|
const encoder = new TextEncoder();
|
|
6973
|
+
let isActive = true;
|
|
5667
6974
|
const stream = new ReadableStream({
|
|
5668
6975
|
async start(controller) {
|
|
5669
|
-
var _a
|
|
5670
|
-
const enqueue = (text) =>
|
|
6976
|
+
var _a;
|
|
6977
|
+
const enqueue = (text) => {
|
|
6978
|
+
if (!isActive) return;
|
|
6979
|
+
try {
|
|
6980
|
+
controller.enqueue(encoder.encode(text));
|
|
6981
|
+
} catch (err) {
|
|
6982
|
+
console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
|
|
6983
|
+
}
|
|
6984
|
+
};
|
|
5671
6985
|
try {
|
|
5672
6986
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
5673
6987
|
try {
|
|
5674
6988
|
for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
5675
6989
|
const chunk = temp.value;
|
|
6990
|
+
if (!isActive) break;
|
|
5676
6991
|
if (typeof chunk === "string") {
|
|
5677
6992
|
enqueue(sseTextFrame(chunk));
|
|
5678
6993
|
} else {
|
|
@@ -5684,8 +6999,7 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5684
6999
|
}
|
|
5685
7000
|
if (sources.length > 0) {
|
|
5686
7001
|
try {
|
|
5687
|
-
const
|
|
5688
|
-
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());
|
|
7002
|
+
const uiTransformation = (_a = responseChunk == null ? void 0 : responseChunk.ui_transformation) != null ? _a : UITransformer.transform(message, sources, plugin.getConfig());
|
|
5689
7003
|
if (uiTransformation) {
|
|
5690
7004
|
enqueue(sseUIFrame(uiTransformation));
|
|
5691
7005
|
}
|
|
@@ -5711,12 +7025,27 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5711
7025
|
}
|
|
5712
7026
|
}
|
|
5713
7027
|
} catch (streamError) {
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
7028
|
+
if (isActive) {
|
|
7029
|
+
const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
|
|
7030
|
+
console.error("[createStreamHandler] Stream error:", streamError);
|
|
7031
|
+
try {
|
|
7032
|
+
enqueue(sseErrorFrame(errorMessage));
|
|
7033
|
+
} catch (e) {
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
5717
7036
|
} finally {
|
|
5718
|
-
|
|
7037
|
+
if (isActive) {
|
|
7038
|
+
isActive = false;
|
|
7039
|
+
try {
|
|
7040
|
+
controller.close();
|
|
7041
|
+
} catch (e) {
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
5719
7044
|
}
|
|
7045
|
+
},
|
|
7046
|
+
cancel(reason) {
|
|
7047
|
+
isActive = false;
|
|
7048
|
+
console.log("[createStreamHandler] Stream connection closed by client:", reason);
|
|
5720
7049
|
}
|
|
5721
7050
|
});
|
|
5722
7051
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
@@ -5777,6 +7106,7 @@ function createUploadHandler(configOrPlugin) {
|
|
|
5777
7106
|
if (parsed.data && parsed.data.length > 0) {
|
|
5778
7107
|
let i = 0;
|
|
5779
7108
|
let lastRowData = null;
|
|
7109
|
+
const csvCols = Object.keys(parsed.data[0]);
|
|
5780
7110
|
for (const row of parsed.data) {
|
|
5781
7111
|
i++;
|
|
5782
7112
|
const rowData = row;
|
|
@@ -5798,12 +7128,14 @@ function createUploadHandler(configOrPlugin) {
|
|
|
5798
7128
|
documents.push({
|
|
5799
7129
|
docId: `${file.name}-row-${i}`,
|
|
5800
7130
|
content: contentParts.join(", "),
|
|
5801
|
-
metadata: __spreadValues(__spreadValues({
|
|
7131
|
+
metadata: __spreadValues(__spreadProps(__spreadValues({
|
|
5802
7132
|
fileName: file.name,
|
|
5803
7133
|
fileSize: file.size,
|
|
5804
7134
|
fileType: file.type,
|
|
5805
7135
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5806
|
-
}, dimension ? { dimension } : {}),
|
|
7136
|
+
}, dimension ? { dimension } : {}), {
|
|
7137
|
+
csvHeaders: csvCols
|
|
7138
|
+
}), rowData)
|
|
5807
7139
|
});
|
|
5808
7140
|
}
|
|
5809
7141
|
}
|