@retrivora-ai/rag-engine 1.8.0 → 1.8.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/DocumentChunker-Dh9TvmGG.d.mts +45 -0
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
- package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
- package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
- package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
- package/dist/chunk-ICKRMZQK.mjs +76 -0
- package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
- package/dist/chunk-OZFBG4BA.mjs +291 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1269 -656
- package/dist/handlers/index.mjs +4 -1
- package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
- package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
- package/dist/index.d.mts +24 -4
- package/dist/index.d.ts +24 -4
- package/dist/index.js +1354 -826
- package/dist/index.mjs +1284 -795
- package/dist/server.d.mts +47 -27
- package/dist/server.d.ts +47 -27
- package/dist/server.js +1417 -829
- package/dist/server.mjs +164 -176
- package/package.json +6 -2
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/page.tsx +12 -322
- package/src/components/AmbientBackground.tsx +29 -0
- package/src/components/ArchitectureCard.tsx +17 -0
- package/src/components/ArchitectureCardsSection.tsx +15 -0
- package/src/components/ChatWindow.tsx +32 -0
- package/src/components/CodeViewer.tsx +51 -0
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocViewer.tsx +37 -0
- package/src/components/DocumentUpload.tsx +44 -1
- package/src/components/Documentation.tsx +58 -0
- package/src/components/DynamicChart.tsx +27 -2
- package/src/components/Hero.tsx +59 -0
- package/src/components/HourglassLoader.tsx +87 -0
- package/src/components/Lifecycle.tsx +37 -0
- package/src/components/MarkdownComponents.tsx +140 -0
- package/src/components/MessageBubble.tsx +124 -904
- package/src/components/Navbar.tsx +55 -0
- package/src/components/ObservabilityPanel.tsx +374 -0
- package/src/components/ProductCard.tsx +5 -3
- package/src/components/UIDispatcher.tsx +344 -0
- package/src/components/VisualizationRenderer.tsx +372 -250
- package/src/config/RagConfig.ts +5 -0
- package/src/config/serverConfig.ts +3 -1
- package/src/core/Pipeline.ts +240 -271
- package/src/core/ProviderRegistry.ts +2 -2
- package/src/core/VectorPlugin.ts +9 -0
- package/src/handlers/index.ts +91 -15
- package/src/hooks/useRagChat.ts +21 -11
- package/src/index.ts +9 -1
- package/src/llm/LLMFactory.ts +54 -2
- package/src/llm/providers/AnthropicProvider.ts +12 -8
- package/src/llm/providers/GeminiProvider.ts +188 -143
- package/src/llm/providers/OllamaProvider.ts +7 -3
- package/src/llm/providers/OpenAIProvider.ts +12 -8
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
- package/src/types/chat.ts +8 -0
- package/src/types/index.ts +132 -0
- package/src/types/props.ts +9 -1
- package/src/utils/ProductExtractor.ts +347 -0
- package/src/utils/SchemaMapper.ts +129 -0
- package/src/utils/UITransformer.ts +470 -209
- package/src/utils/synonyms.ts +78 -0
- package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
- package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
- package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
- package/dist/chunk-FLOSGE6A.mjs +0 -202
package/dist/handlers/index.js
CHANGED
|
@@ -26,6 +26,18 @@ var __spreadValues = (a, b) => {
|
|
|
26
26
|
return a;
|
|
27
27
|
};
|
|
28
28
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
29
|
+
var __objRest = (source, exclude) => {
|
|
30
|
+
var target = {};
|
|
31
|
+
for (var prop in source)
|
|
32
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
33
|
+
target[prop] = source[prop];
|
|
34
|
+
if (source != null && __getOwnPropSymbols)
|
|
35
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
36
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
37
|
+
target[prop] = source[prop];
|
|
38
|
+
}
|
|
39
|
+
return target;
|
|
40
|
+
};
|
|
29
41
|
var __esm = (fn, res) => function __init() {
|
|
30
42
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
31
43
|
};
|
|
@@ -322,81 +334,31 @@ var init_PineconeProvider = __esm({
|
|
|
322
334
|
}
|
|
323
335
|
});
|
|
324
336
|
|
|
325
|
-
// src/providers/vectordb/
|
|
326
|
-
var
|
|
327
|
-
__export(
|
|
328
|
-
|
|
337
|
+
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
338
|
+
var MultiTablePostgresProvider_exports = {};
|
|
339
|
+
__export(MultiTablePostgresProvider_exports, {
|
|
340
|
+
MultiTablePostgresProvider: () => MultiTablePostgresProvider
|
|
329
341
|
});
|
|
330
|
-
var import_pg,
|
|
331
|
-
var
|
|
332
|
-
"src/providers/vectordb/
|
|
342
|
+
var import_pg, MultiTablePostgresProvider;
|
|
343
|
+
var init_MultiTablePostgresProvider = __esm({
|
|
344
|
+
"src/providers/vectordb/MultiTablePostgresProvider.ts"() {
|
|
333
345
|
"use strict";
|
|
334
346
|
import_pg = require("pg");
|
|
335
347
|
init_BaseVectorProvider();
|
|
336
|
-
|
|
348
|
+
MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
337
349
|
constructor(config) {
|
|
338
|
-
var _a;
|
|
350
|
+
var _a, _b, _c;
|
|
339
351
|
super(config);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
const opts = config.options || {};
|
|
353
|
+
if (!opts.connectionString) {
|
|
354
|
+
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
355
|
+
}
|
|
343
356
|
this.connectionString = opts.connectionString;
|
|
344
|
-
this.dimensions = (_a = opts.dimensions) != null ? _a :
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const errors = [];
|
|
350
|
-
const opts = config.options || {};
|
|
351
|
-
if (!opts.connectionString) {
|
|
352
|
-
errors.push({
|
|
353
|
-
field: "vectorDb.options.connectionString",
|
|
354
|
-
message: "PostgreSQL connection string is required",
|
|
355
|
-
suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
|
|
356
|
-
severity: "error"
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
|
|
360
|
-
errors.push({
|
|
361
|
-
field: "vectorDb.options.tables",
|
|
362
|
-
message: "PostgreSQL tables must be a string or a string array",
|
|
363
|
-
severity: "error"
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
return errors;
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
static getHealthChecker() {
|
|
371
|
-
return {
|
|
372
|
-
async check(config) {
|
|
373
|
-
const opts = config.options || {};
|
|
374
|
-
const timestamp = Date.now();
|
|
375
|
-
try {
|
|
376
|
-
const { Client } = await import("pg");
|
|
377
|
-
const client = new Client({ connectionString: opts.connectionString });
|
|
378
|
-
await client.connect();
|
|
379
|
-
const result = await client.query(`
|
|
380
|
-
SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
|
|
381
|
-
`);
|
|
382
|
-
const hasVector = result.rows[0].exists;
|
|
383
|
-
await client.end();
|
|
384
|
-
return {
|
|
385
|
-
healthy: true,
|
|
386
|
-
provider: "postgresql",
|
|
387
|
-
capabilities: { pgvectorInstalled: hasVector },
|
|
388
|
-
timestamp
|
|
389
|
-
};
|
|
390
|
-
} catch (error) {
|
|
391
|
-
return {
|
|
392
|
-
healthy: false,
|
|
393
|
-
provider: "postgresql",
|
|
394
|
-
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
395
|
-
timestamp
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
};
|
|
357
|
+
this.dimensions = (_a = opts.dimensions) != null ? _a : 768;
|
|
358
|
+
this.tables = [];
|
|
359
|
+
const rawSearchFields = (_c = (_b = opts.searchFields) != null ? _b : process.env.VECTOR_DB_SEARCH_FIELDS) != null ? _c : ["name", "product_name", "productname", "title"];
|
|
360
|
+
this.searchFields = typeof rawSearchFields === "string" ? rawSearchFields.split(",").map((f) => f.trim()).filter(Boolean) : rawSearchFields;
|
|
361
|
+
this.uploadTable = opts.uploadTable || "document_chunks";
|
|
400
362
|
}
|
|
401
363
|
async initialize() {
|
|
402
364
|
this.pool = new import_pg.Pool({ connectionString: this.connectionString });
|
|
@@ -404,7 +366,7 @@ var init_PostgreSQLProvider = __esm({
|
|
|
404
366
|
try {
|
|
405
367
|
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
406
368
|
await client.query(`
|
|
407
|
-
CREATE TABLE IF NOT EXISTS ${this.
|
|
369
|
+
CREATE TABLE IF NOT EXISTS ${this.uploadTable} (
|
|
408
370
|
id TEXT PRIMARY KEY,
|
|
409
371
|
namespace TEXT NOT NULL DEFAULT '',
|
|
410
372
|
content TEXT NOT NULL,
|
|
@@ -413,53 +375,115 @@ var init_PostgreSQLProvider = __esm({
|
|
|
413
375
|
)
|
|
414
376
|
`);
|
|
415
377
|
await client.query(`
|
|
416
|
-
CREATE INDEX IF NOT EXISTS ${this.
|
|
417
|
-
ON ${this.
|
|
378
|
+
CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
|
|
379
|
+
ON ${this.uploadTable}
|
|
418
380
|
USING hnsw (embedding vector_cosine_ops)
|
|
419
381
|
`);
|
|
382
|
+
const res = await client.query(`
|
|
383
|
+
SELECT table_name
|
|
384
|
+
FROM information_schema.columns
|
|
385
|
+
WHERE column_name = 'embedding'
|
|
386
|
+
AND table_schema = 'public'
|
|
387
|
+
`);
|
|
388
|
+
this.tables = res.rows.map((r) => r.table_name);
|
|
389
|
+
if (this.tables.length === 0) {
|
|
390
|
+
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
391
|
+
} else {
|
|
392
|
+
console.log(
|
|
393
|
+
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
420
396
|
} finally {
|
|
421
397
|
client.release();
|
|
422
398
|
}
|
|
423
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Upsert a document by dynamically provisioning a table and columns.
|
|
402
|
+
*/
|
|
424
403
|
async upsert(doc, namespace = "") {
|
|
425
|
-
|
|
426
|
-
const vectorLiteral = `[${doc.vector.join(",")}]`;
|
|
427
|
-
await this.pool.query(
|
|
428
|
-
`INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
429
|
-
VALUES ($1, $2, $3, $4, $5::vector)
|
|
430
|
-
ON CONFLICT (id) DO UPDATE
|
|
431
|
-
SET namespace = EXCLUDED.namespace,
|
|
432
|
-
content = EXCLUDED.content,
|
|
433
|
-
metadata = EXCLUDED.metadata,
|
|
434
|
-
embedding = EXCLUDED.embedding`,
|
|
435
|
-
[doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
|
|
436
|
-
);
|
|
404
|
+
await this.batchUpsert([doc], namespace);
|
|
437
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Batch upsert documents by dynamically provisioning tables and columns based on CSV headers.
|
|
408
|
+
*/
|
|
438
409
|
async batchUpsert(docs, namespace = "") {
|
|
410
|
+
var _a;
|
|
439
411
|
if (docs.length === 0) return;
|
|
412
|
+
const docsByFile = {};
|
|
413
|
+
for (const doc of docs) {
|
|
414
|
+
const fileName = ((_a = doc.metadata) == null ? void 0 : _a.fileName) || this.uploadTable;
|
|
415
|
+
if (!docsByFile[fileName]) docsByFile[fileName] = [];
|
|
416
|
+
docsByFile[fileName].push(doc);
|
|
417
|
+
}
|
|
440
418
|
const client = await this.pool.connect();
|
|
441
419
|
try {
|
|
442
420
|
await client.query("BEGIN");
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}).join("
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
421
|
+
for (const [fileName, fileDocs] of Object.entries(docsByFile)) {
|
|
422
|
+
let tableName = fileName;
|
|
423
|
+
if (tableName.toLowerCase().endsWith(".csv")) {
|
|
424
|
+
tableName = tableName.slice(0, -4);
|
|
425
|
+
}
|
|
426
|
+
tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
|
|
427
|
+
const firstMeta = fileDocs[0].metadata || {};
|
|
428
|
+
const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding"];
|
|
429
|
+
const csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
|
|
430
|
+
const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
|
|
431
|
+
const createTableSql = `
|
|
432
|
+
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
433
|
+
id TEXT PRIMARY KEY,
|
|
434
|
+
${columnDefs ? columnDefs + "," : ""}
|
|
435
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
436
|
+
content TEXT NOT NULL,
|
|
437
|
+
metadata JSONB,
|
|
438
|
+
embedding VECTOR(${this.dimensions})
|
|
439
|
+
)
|
|
461
440
|
`;
|
|
462
|
-
await client.query(
|
|
441
|
+
await client.query(createTableSql);
|
|
442
|
+
await client.query(`
|
|
443
|
+
CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
|
|
444
|
+
ON "${tableName}"
|
|
445
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
446
|
+
`);
|
|
447
|
+
if (!this.tables.includes(tableName)) {
|
|
448
|
+
this.tables.push(tableName);
|
|
449
|
+
}
|
|
450
|
+
const BATCH_SIZE = 50;
|
|
451
|
+
for (let i = 0; i < fileDocs.length; i += BATCH_SIZE) {
|
|
452
|
+
const batch = fileDocs.slice(i, i + BATCH_SIZE);
|
|
453
|
+
const values = [];
|
|
454
|
+
const headerColumns = csvHeaders.map((h) => `"${h}"`).join(", ");
|
|
455
|
+
const allColumns = `id, ${headerColumns ? headerColumns + ", " : ""}namespace, content, metadata, embedding`;
|
|
456
|
+
const valuePlaceholders = batch.map((doc, idx) => {
|
|
457
|
+
var _a2, _b;
|
|
458
|
+
const offset = idx * (5 + csvHeaders.length);
|
|
459
|
+
values.push(doc.id);
|
|
460
|
+
for (const h of csvHeaders) {
|
|
461
|
+
values.push(String(((_a2 = doc.metadata) == null ? void 0 : _a2[h]) || ""));
|
|
462
|
+
}
|
|
463
|
+
values.push(namespace, doc.content, JSON.stringify((_b = doc.metadata) != null ? _b : {}), `[${doc.vector.join(",")}]`);
|
|
464
|
+
const docPlaceholders = [];
|
|
465
|
+
for (let j = 1; j <= 5 + csvHeaders.length; j++) {
|
|
466
|
+
if (j === 5 + csvHeaders.length) {
|
|
467
|
+
docPlaceholders.push(`$${offset + j}::vector`);
|
|
468
|
+
} else {
|
|
469
|
+
docPlaceholders.push(`$${offset + j}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return `(${docPlaceholders.join(", ")})`;
|
|
473
|
+
}).join(", ");
|
|
474
|
+
const conflictUpdates = csvHeaders.map((h) => `"${h}" = EXCLUDED."${h}"`).join(",\n ");
|
|
475
|
+
const query = `
|
|
476
|
+
INSERT INTO "${tableName}" (${allColumns})
|
|
477
|
+
VALUES ${valuePlaceholders}
|
|
478
|
+
ON CONFLICT (id) DO UPDATE
|
|
479
|
+
SET ${conflictUpdates ? conflictUpdates + "," : ""}
|
|
480
|
+
namespace = EXCLUDED.namespace,
|
|
481
|
+
content = EXCLUDED.content,
|
|
482
|
+
metadata = EXCLUDED.metadata,
|
|
483
|
+
embedding = EXCLUDED.embedding
|
|
484
|
+
`;
|
|
485
|
+
await client.query(query, values);
|
|
486
|
+
}
|
|
463
487
|
}
|
|
464
488
|
await client.query("COMMIT");
|
|
465
489
|
} catch (error) {
|
|
@@ -469,49 +493,119 @@ var init_PostgreSQLProvider = __esm({
|
|
|
469
493
|
client.release();
|
|
470
494
|
}
|
|
471
495
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
|
|
480
|
-
const paramIdx = params.length + 1;
|
|
481
|
-
params.push(JSON.stringify(val));
|
|
482
|
-
return `metadata->>'${key}' = $${paramIdx}`;
|
|
483
|
-
}).join(" AND ");
|
|
484
|
-
whereClause = whereClause ? `${whereClause} AND ${filterConditions}` : `WHERE ${filterConditions}`;
|
|
496
|
+
/**
|
|
497
|
+
* Query all configured tables and merge results, sorted by cosine similarity score.
|
|
498
|
+
*/
|
|
499
|
+
async query(vector, topK, _namespace, _filter) {
|
|
500
|
+
var _a;
|
|
501
|
+
if (!this.pool) {
|
|
502
|
+
throw new Error("[MultiTablePostgresProvider] Provider not initialized. Call initialize() first.");
|
|
485
503
|
}
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
504
|
+
const vectorLiteral = `[${vector.join(",")}]`;
|
|
505
|
+
const allResults = [];
|
|
506
|
+
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
507
|
+
const queryText = _filter == null ? void 0 : _filter.queryText;
|
|
508
|
+
const entityHints = Array.isArray(_filter == null ? void 0 : _filter.__entityHints) ? _filter.__entityHints.filter(
|
|
509
|
+
(hint) => typeof hint === "object" && hint !== null && typeof hint.value === "string"
|
|
510
|
+
).map((hint) => hint.value.trim().toLowerCase()).filter(Boolean) : [];
|
|
511
|
+
console.log(`[MultiTablePostgresProvider] queryText: "${queryText}"`);
|
|
512
|
+
console.log(`[MultiTablePostgresProvider] entityHints: [${entityHints.join(", ")}]`);
|
|
513
|
+
const getDynamicKeywordQuery = () => {
|
|
514
|
+
if (entityHints.length > 0) {
|
|
515
|
+
return entityHints.map((h) => h.includes(" ") ? `"${h}"` : h).join(" & ");
|
|
516
|
+
}
|
|
517
|
+
if (queryText) {
|
|
518
|
+
return queryText.toLowerCase().replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|of|category|find|list|browse|what|are|display|all|under|for|in|with|about)\b/g, "").trim().replace(/\s+/g, " & ");
|
|
519
|
+
}
|
|
520
|
+
return "";
|
|
521
|
+
};
|
|
522
|
+
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
523
|
+
const queryPromises = this.tables.map(async (table) => {
|
|
524
|
+
try {
|
|
525
|
+
let sqlQuery = "";
|
|
526
|
+
let params = [];
|
|
527
|
+
if (queryText) {
|
|
528
|
+
const hasEntityHints = entityHints.length > 0;
|
|
529
|
+
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
530
|
+
SELECT COALESCE(MAX(
|
|
531
|
+
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
|
|
532
|
+
THEN 1.0 ELSE 0.0 END
|
|
533
|
+
), 0)
|
|
534
|
+
FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
|
|
535
|
+
WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
|
|
536
|
+
) * 3.0` : "";
|
|
537
|
+
sqlQuery = `
|
|
538
|
+
SELECT *,
|
|
539
|
+
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
540
|
+
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
541
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
542
|
+
FROM "${table}" t
|
|
543
|
+
ORDER BY hybrid_score DESC
|
|
544
|
+
LIMIT 50
|
|
545
|
+
`;
|
|
546
|
+
params = [vectorLiteral, dynamicKeywordQuery];
|
|
547
|
+
} else {
|
|
548
|
+
sqlQuery = `
|
|
549
|
+
SELECT *,
|
|
550
|
+
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
551
|
+
FROM "${table}" t
|
|
552
|
+
ORDER BY hybrid_score DESC
|
|
553
|
+
LIMIT 50
|
|
554
|
+
`;
|
|
555
|
+
params = [vectorLiteral];
|
|
556
|
+
}
|
|
557
|
+
const result = await this.pool.query(sqlQuery, params);
|
|
558
|
+
if (result.rowCount && result.rowCount > 0) {
|
|
559
|
+
console.log(` \u2705 Table "${table}": Found ${result.rowCount} potential matches. Top score: ${result.rows[0].hybrid_score.toFixed(4)}`);
|
|
560
|
+
} else {
|
|
561
|
+
console.log(` \u26AA Table "${table}": No relevant matches found.`);
|
|
562
|
+
}
|
|
563
|
+
const tableResults = [];
|
|
564
|
+
for (const row of result.rows) {
|
|
565
|
+
const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
|
|
566
|
+
delete rest.embedding;
|
|
567
|
+
delete rest.vector_score;
|
|
568
|
+
delete rest.keyword_score;
|
|
569
|
+
delete rest.exact_name_score;
|
|
570
|
+
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
571
|
+
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
572
|
+
tableResults.push({
|
|
573
|
+
id: `${table}-${id}`,
|
|
574
|
+
score: parseFloat(String(hybrid_score)),
|
|
575
|
+
content,
|
|
576
|
+
metadata: __spreadProps(__spreadValues({}, rest), { source_table: table })
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return tableResults;
|
|
580
|
+
} catch (err) {
|
|
581
|
+
console.error(
|
|
582
|
+
`[MultiTablePostgresProvider] Error querying table "${table}":`,
|
|
583
|
+
err.message
|
|
584
|
+
);
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
const resultsArray = await Promise.all(queryPromises);
|
|
589
|
+
for (const tableResults of resultsArray) {
|
|
590
|
+
allResults.push(...tableResults);
|
|
506
591
|
}
|
|
592
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
593
|
+
if (allResults.length === 0) return [];
|
|
594
|
+
const bestMatchTable = (_a = allResults[0].metadata) == null ? void 0 : _a.source_table;
|
|
595
|
+
const finalSorted = allResults.filter((res) => {
|
|
596
|
+
var _a2;
|
|
597
|
+
return ((_a2 = res.metadata) == null ? void 0 : _a2.source_table) === bestMatchTable;
|
|
598
|
+
});
|
|
599
|
+
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
600
|
+
console.log(`[MultiTablePostgresProvider] Final top match from "${bestMatchTable}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
601
|
+
console.log(`[MultiTablePostgresProvider] Restricting recommendations strictly to table: "${bestMatchTable}"`);
|
|
602
|
+
return finalSorted.slice(0, Math.max(topK, 15));
|
|
507
603
|
}
|
|
508
|
-
async delete(
|
|
509
|
-
|
|
510
|
-
const params = namespace ? [id, namespace] : [id];
|
|
511
|
-
await this.pool.query(`DELETE FROM ${this.tableName} ${where}`, params);
|
|
604
|
+
async delete(_id, _namespace) {
|
|
605
|
+
console.warn("[MultiTablePostgresProvider] delete() is a no-op for multi-table mode.");
|
|
512
606
|
}
|
|
513
|
-
async deleteNamespace(
|
|
514
|
-
|
|
607
|
+
async deleteNamespace(_namespace) {
|
|
608
|
+
console.warn("[MultiTablePostgresProvider] deleteNamespace() is a no-op for multi-table mode.");
|
|
515
609
|
}
|
|
516
610
|
async ping() {
|
|
517
611
|
try {
|
|
@@ -522,7 +616,9 @@ var init_PostgreSQLProvider = __esm({
|
|
|
522
616
|
}
|
|
523
617
|
}
|
|
524
618
|
async disconnect() {
|
|
525
|
-
|
|
619
|
+
if (this.pool) {
|
|
620
|
+
await this.pool.end();
|
|
621
|
+
}
|
|
526
622
|
}
|
|
527
623
|
};
|
|
528
624
|
}
|
|
@@ -1617,6 +1713,7 @@ __export(handlers_exports, {
|
|
|
1617
1713
|
sseErrorFrame: () => sseErrorFrame,
|
|
1618
1714
|
sseFrame: () => sseFrame,
|
|
1619
1715
|
sseMetaFrame: () => sseMetaFrame,
|
|
1716
|
+
sseObservabilityFrame: () => sseObservabilityFrame,
|
|
1620
1717
|
sseTextFrame: () => sseTextFrame,
|
|
1621
1718
|
sseUIFrame: () => sseUIFrame
|
|
1622
1719
|
});
|
|
@@ -1691,7 +1788,7 @@ function readEnum(env, name, fallback, allowed) {
|
|
|
1691
1788
|
throw new Error(`[getRagConfig] ${name} must be one of: ${allowed.join(", ")}`);
|
|
1692
1789
|
}
|
|
1693
1790
|
function getEnvConfig(env = process.env, base) {
|
|
1694
|
-
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;
|
|
1791
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa;
|
|
1695
1792
|
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__";
|
|
1696
1793
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
1697
1794
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -1702,33 +1799,35 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1702
1799
|
vectorDbOptions.apiKey = (_g = (_f = readString(env, "PINECONE_API_KEY")) != null ? _f : (_e = (_d = base == null ? void 0 : base.vectorDb) == null ? void 0 : _d.options) == null ? void 0 : _e.apiKey) != null ? _g : "";
|
|
1703
1800
|
vectorDbOptions.indexName = (_l = (_i = readString(env, "PINECONE_INDEX")) != null ? _i : (_h = base == null ? void 0 : base.vectorDb) == null ? void 0 : _h.indexName) != null ? _l : (_k = (_j = base == null ? void 0 : base.vectorDb) == null ? void 0 : _j.options) == null ? void 0 : _k.indexName;
|
|
1704
1801
|
} else if (vectorProvider === "pgvector" || vectorProvider === "postgresql") {
|
|
1705
|
-
vectorDbOptions.connectionString = (_p = (
|
|
1802
|
+
vectorDbOptions.connectionString = (_q = (_p = (_m = readString(env, "PGVECTOR_CONNECTION_STRING")) != null ? _m : readString(env, "POSTGRES_URL")) != null ? _p : (_o = (_n = base == null ? void 0 : base.vectorDb) == null ? void 0 : _n.options) == null ? void 0 : _o.connectionString) != null ? _q : "";
|
|
1803
|
+
vectorDbOptions.tables = (_v = (_s = (_r = readString(env, "VECTOR_DB_TABLES")) != null ? _r : readString(env, "POSTGRES_TABLES")) == null ? void 0 : _s.split(",").map((t) => t.trim())) != null ? _v : (_u = (_t = base == null ? void 0 : base.vectorDb) == null ? void 0 : _t.options) == null ? void 0 : _u.tables;
|
|
1804
|
+
vectorDbOptions.searchFields = (_z = (_w = readString(env, "POSTGRES_SEARCH_FIELDS")) == null ? void 0 : _w.split(",").map((f) => f.trim())) != null ? _z : (_y = (_x = base == null ? void 0 : base.vectorDb) == null ? void 0 : _x.options) == null ? void 0 : _y.searchFields;
|
|
1706
1805
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1707
1806
|
} else if (vectorProvider === "mongodb") {
|
|
1708
|
-
vectorDbOptions.uri = (
|
|
1709
|
-
vectorDbOptions.database = (
|
|
1710
|
-
vectorDbOptions.collection = (
|
|
1711
|
-
vectorDbOptions.indexName = (
|
|
1807
|
+
vectorDbOptions.uri = (_D = (_C = readString(env, "MONGODB_URI")) != null ? _C : (_B = (_A = base == null ? void 0 : base.vectorDb) == null ? void 0 : _A.options) == null ? void 0 : _B.uri) != null ? _D : "";
|
|
1808
|
+
vectorDbOptions.database = (_H = (_G = readString(env, "MONGODB_DB")) != null ? _G : (_F = (_E = base == null ? void 0 : base.vectorDb) == null ? void 0 : _E.options) == null ? void 0 : _F.database) != null ? _H : "";
|
|
1809
|
+
vectorDbOptions.collection = (_L = (_K = readString(env, "MONGODB_COLLECTION")) != null ? _K : (_J = (_I = base == null ? void 0 : base.vectorDb) == null ? void 0 : _I.options) == null ? void 0 : _J.collection) != null ? _L : "";
|
|
1810
|
+
vectorDbOptions.indexName = (_R = (_Q = (_N = readString(env, "MONGODB_INDEX_NAME")) != null ? _N : (_M = base == null ? void 0 : base.vectorDb) == null ? void 0 : _M.indexName) != null ? _Q : (_P = (_O = base == null ? void 0 : base.vectorDb) == null ? void 0 : _O.options) == null ? void 0 : _P.indexName) != null ? _R : "vector_index";
|
|
1712
1811
|
} else if (vectorProvider === "qdrant") {
|
|
1713
|
-
vectorDbOptions.baseUrl = (
|
|
1714
|
-
vectorDbOptions.apiKey = (
|
|
1812
|
+
vectorDbOptions.baseUrl = (_V = (_U = readString(env, "QDRANT_URL")) != null ? _U : (_T = (_S = base == null ? void 0 : base.vectorDb) == null ? void 0 : _S.options) == null ? void 0 : _T.baseUrl) != null ? _V : "http://localhost:6333";
|
|
1813
|
+
vectorDbOptions.apiKey = (_Y = readString(env, "QDRANT_API_KEY")) != null ? _Y : (_X = (_W = base == null ? void 0 : base.vectorDb) == null ? void 0 : _W.options) == null ? void 0 : _X.apiKey;
|
|
1715
1814
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1716
1815
|
} else if (vectorProvider === "milvus") {
|
|
1717
|
-
vectorDbOptions.baseUrl = (
|
|
1816
|
+
vectorDbOptions.baseUrl = (_Z = readString(env, "MILVUS_URL")) != null ? _Z : "http://localhost:19530";
|
|
1718
1817
|
vectorDbOptions.apiKey = readString(env, "MILVUS_API_KEY");
|
|
1719
1818
|
} else if (vectorProvider === "chromadb") {
|
|
1720
|
-
vectorDbOptions.baseUrl = (
|
|
1819
|
+
vectorDbOptions.baseUrl = (__ = readString(env, "CHROMADB_URL")) != null ? __ : "http://localhost:8000";
|
|
1721
1820
|
} else if (vectorProvider === "weaviate") {
|
|
1722
|
-
vectorDbOptions.baseUrl = (
|
|
1821
|
+
vectorDbOptions.baseUrl = (_$ = readString(env, "WEAVIATE_URL")) != null ? _$ : "http://localhost:8080";
|
|
1723
1822
|
vectorDbOptions.apiKey = readString(env, "WEAVIATE_API_KEY");
|
|
1724
1823
|
} else if (vectorProvider === "redis") {
|
|
1725
|
-
vectorDbOptions.baseUrl = (
|
|
1824
|
+
vectorDbOptions.baseUrl = (_aa = readString(env, "REDIS_URL")) != null ? _aa : "";
|
|
1726
1825
|
vectorDbOptions.apiKey = readString(env, "REDIS_API_KEY");
|
|
1727
1826
|
} else if (vectorProvider === "rest") {
|
|
1728
|
-
vectorDbOptions.baseUrl = (
|
|
1827
|
+
vectorDbOptions.baseUrl = (_ba = readString(env, "VECTOR_DB_REST_URL")) != null ? _ba : "";
|
|
1729
1828
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { "api-key": readString(env, "VECTOR_DB_REST_API_KEY") } : {};
|
|
1730
1829
|
} else if (vectorProvider === "universal_rest") {
|
|
1731
|
-
vectorDbOptions.baseUrl = (
|
|
1830
|
+
vectorDbOptions.baseUrl = (_da = (_ca = readString(env, "VECTOR_BASE_URL")) != null ? _ca : readString(env, "VECTOR_DB_REST_URL")) != null ? _da : "";
|
|
1732
1831
|
vectorDbOptions.profile = readString(env, "VECTOR_UNIVERSAL_PROFILE");
|
|
1733
1832
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { Authorization: `Bearer ${readString(env, "VECTOR_DB_REST_API_KEY")}` } : {};
|
|
1734
1833
|
}
|
|
@@ -1745,20 +1844,20 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1745
1844
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
1746
1845
|
gemini: readString(env, "GEMINI_API_KEY"),
|
|
1747
1846
|
ollama: void 0,
|
|
1748
|
-
universal_rest: (
|
|
1749
|
-
custom: (
|
|
1847
|
+
universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
|
|
1848
|
+
custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
|
|
1750
1849
|
};
|
|
1751
1850
|
return {
|
|
1752
1851
|
projectId,
|
|
1753
1852
|
vectorDb: {
|
|
1754
1853
|
provider: vectorProvider,
|
|
1755
|
-
indexName: (
|
|
1854
|
+
indexName: (_ha = (_ga = readString(env, "VECTOR_DB_INDEX")) != null ? _ga : vectorDbOptions.indexName) != null ? _ha : "rag-index",
|
|
1756
1855
|
options: vectorDbOptions
|
|
1757
1856
|
},
|
|
1758
1857
|
llm: {
|
|
1759
1858
|
provider: llmProvider,
|
|
1760
|
-
model: (
|
|
1761
|
-
apiKey: (
|
|
1859
|
+
model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
|
|
1860
|
+
apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
|
|
1762
1861
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
1763
1862
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
1764
1863
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
|
|
@@ -1769,7 +1868,7 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1769
1868
|
},
|
|
1770
1869
|
embedding: {
|
|
1771
1870
|
provider: embeddingProvider,
|
|
1772
|
-
model: (
|
|
1871
|
+
model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
|
|
1773
1872
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
1774
1873
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
1775
1874
|
dimensions: embeddingDimensions,
|
|
@@ -1780,17 +1879,17 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1780
1879
|
}
|
|
1781
1880
|
},
|
|
1782
1881
|
ui: {
|
|
1783
|
-
title: (
|
|
1784
|
-
subtitle: (
|
|
1785
|
-
primaryColor: (
|
|
1786
|
-
accentColor: (
|
|
1787
|
-
logoUrl: (
|
|
1788
|
-
placeholder: (
|
|
1789
|
-
showSources: ((
|
|
1790
|
-
welcomeMessage: (
|
|
1791
|
-
visualStyle: (
|
|
1792
|
-
borderRadius: (
|
|
1793
|
-
allowUpload: ((
|
|
1882
|
+
title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
|
|
1883
|
+
subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
|
|
1884
|
+
primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
|
|
1885
|
+
accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
|
|
1886
|
+
logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
|
|
1887
|
+
placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
|
|
1888
|
+
showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
|
|
1889
|
+
welcomeMessage: (_za = (_ya = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _ya : readString(env, "UI_WELCOME_MESSAGE")) != null ? _za : "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
1890
|
+
visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
|
|
1891
|
+
borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
|
|
1892
|
+
allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
|
|
1794
1893
|
},
|
|
1795
1894
|
rag: {
|
|
1796
1895
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
@@ -1907,14 +2006,14 @@ var OpenAIProvider = class {
|
|
|
1907
2006
|
};
|
|
1908
2007
|
}
|
|
1909
2008
|
async chat(messages, context, options) {
|
|
1910
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1911
|
-
const
|
|
2009
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
2010
|
+
const resolvedSystemPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Answer questions based on the provided context.
|
|
1912
2011
|
|
|
1913
2012
|
Context:
|
|
1914
2013
|
${context}`;
|
|
1915
2014
|
const systemMessage = {
|
|
1916
2015
|
role: "system",
|
|
1917
|
-
content:
|
|
2016
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
1918
2017
|
|
|
1919
2018
|
Context:
|
|
1920
2019
|
${context}`
|
|
@@ -1929,22 +2028,22 @@ ${context}`
|
|
|
1929
2028
|
const completion = await this.client.chat.completions.create({
|
|
1930
2029
|
model: this.llmConfig.model,
|
|
1931
2030
|
messages: formattedMessages,
|
|
1932
|
-
max_tokens: (
|
|
1933
|
-
temperature: (
|
|
2031
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2032
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
1934
2033
|
stop: options == null ? void 0 : options.stop
|
|
1935
2034
|
});
|
|
1936
|
-
return (
|
|
2035
|
+
return (_i = (_h = (_g = completion.choices[0]) == null ? void 0 : _g.message) == null ? void 0 : _h.content) != null ? _i : "";
|
|
1937
2036
|
}
|
|
1938
2037
|
chatStream(messages, context, options) {
|
|
1939
2038
|
return __asyncGenerator(this, null, function* () {
|
|
1940
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1941
|
-
const
|
|
2039
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2040
|
+
const resolvedSystemPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Answer questions based on the provided context.
|
|
1942
2041
|
|
|
1943
2042
|
Context:
|
|
1944
2043
|
${context}`;
|
|
1945
2044
|
const systemMessage = {
|
|
1946
2045
|
role: "system",
|
|
1947
|
-
content:
|
|
2046
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
1948
2047
|
|
|
1949
2048
|
Context:
|
|
1950
2049
|
${context}`
|
|
@@ -1959,8 +2058,8 @@ ${context}`
|
|
|
1959
2058
|
const stream = yield new __await(this.client.chat.completions.create({
|
|
1960
2059
|
model: this.llmConfig.model,
|
|
1961
2060
|
messages: formattedMessages,
|
|
1962
|
-
max_tokens: (
|
|
1963
|
-
temperature: (
|
|
2061
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2062
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
1964
2063
|
stop: options == null ? void 0 : options.stop,
|
|
1965
2064
|
stream: true
|
|
1966
2065
|
}));
|
|
@@ -1970,7 +2069,7 @@ ${context}`
|
|
|
1970
2069
|
try {
|
|
1971
2070
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
1972
2071
|
const chunk = temp.value;
|
|
1973
|
-
const content = ((
|
|
2072
|
+
const content = ((_h = (_g = chunk.choices[0]) == null ? void 0 : _g.delta) == null ? void 0 : _h.content) || "";
|
|
1974
2073
|
if (content) yield content;
|
|
1975
2074
|
}
|
|
1976
2075
|
} catch (temp) {
|
|
@@ -2068,12 +2167,12 @@ var AnthropicProvider = class {
|
|
|
2068
2167
|
};
|
|
2069
2168
|
}
|
|
2070
2169
|
async chat(messages, context, options) {
|
|
2071
|
-
var _a, _b, _c;
|
|
2072
|
-
const
|
|
2170
|
+
var _a, _b, _c, _d;
|
|
2171
|
+
const resolvedPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Use the context below to answer the user's question accurately.
|
|
2073
2172
|
|
|
2074
2173
|
Context:
|
|
2075
2174
|
${context}`;
|
|
2076
|
-
const system =
|
|
2175
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
2077
2176
|
|
|
2078
2177
|
Context:
|
|
2079
2178
|
${context}`;
|
|
@@ -2083,7 +2182,7 @@ ${context}`;
|
|
|
2083
2182
|
}));
|
|
2084
2183
|
const response = await this.client.messages.create({
|
|
2085
2184
|
model: this.llmConfig.model,
|
|
2086
|
-
max_tokens: (
|
|
2185
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2087
2186
|
system,
|
|
2088
2187
|
messages: anthropicMessages
|
|
2089
2188
|
});
|
|
@@ -2092,12 +2191,12 @@ ${context}`;
|
|
|
2092
2191
|
}
|
|
2093
2192
|
chatStream(messages, context, options) {
|
|
2094
2193
|
return __asyncGenerator(this, null, function* () {
|
|
2095
|
-
var _a, _b, _c;
|
|
2096
|
-
const
|
|
2194
|
+
var _a, _b, _c, _d;
|
|
2195
|
+
const resolvedPrompt = (_b = (_a = options == null ? void 0 : options.systemPrompt) != null ? _a : this.llmConfig.systemPrompt) != null ? _b : `You are a helpful assistant. Use the context below to answer the user's question accurately.
|
|
2097
2196
|
|
|
2098
2197
|
Context:
|
|
2099
2198
|
${context}`;
|
|
2100
|
-
const system =
|
|
2199
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
2101
2200
|
|
|
2102
2201
|
Context:
|
|
2103
2202
|
${context}`;
|
|
@@ -2107,7 +2206,7 @@ ${context}`;
|
|
|
2107
2206
|
}));
|
|
2108
2207
|
const stream = yield new __await(this.client.messages.create({
|
|
2109
2208
|
model: this.llmConfig.model,
|
|
2110
|
-
max_tokens: (
|
|
2209
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2111
2210
|
system,
|
|
2112
2211
|
messages: anthropicMessages,
|
|
2113
2212
|
stream: true
|
|
@@ -2222,7 +2321,7 @@ var OllamaProvider = class {
|
|
|
2222
2321
|
}
|
|
2223
2322
|
async chat(messages, context, options) {
|
|
2224
2323
|
var _a, _b, _c, _d;
|
|
2225
|
-
const system = this.buildSystemPrompt(context);
|
|
2324
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
2226
2325
|
const { data } = await this.http.post("/api/chat", {
|
|
2227
2326
|
model: this.llmConfig.model,
|
|
2228
2327
|
stream: false,
|
|
@@ -2240,7 +2339,7 @@ var OllamaProvider = class {
|
|
|
2240
2339
|
chatStream(messages, context, options) {
|
|
2241
2340
|
return __asyncGenerator(this, null, function* () {
|
|
2242
2341
|
var _a, _b, _c, _d, _e, _f;
|
|
2243
|
-
const system = this.buildSystemPrompt(context);
|
|
2342
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
2244
2343
|
const response = yield new __await(this.http.post("/api/chat", {
|
|
2245
2344
|
model: this.llmConfig.model,
|
|
2246
2345
|
stream: true,
|
|
@@ -2296,8 +2395,11 @@ var OllamaProvider = class {
|
|
|
2296
2395
|
}
|
|
2297
2396
|
});
|
|
2298
2397
|
}
|
|
2299
|
-
buildSystemPrompt(context) {
|
|
2398
|
+
buildSystemPrompt(context, overridePrompt) {
|
|
2300
2399
|
var _a;
|
|
2400
|
+
if (overridePrompt) {
|
|
2401
|
+
return overridePrompt;
|
|
2402
|
+
}
|
|
2301
2403
|
const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the provided context to answer the user's question.
|
|
2302
2404
|
|
|
2303
2405
|
Context:
|
|
@@ -2349,20 +2451,41 @@ ${context}`;
|
|
|
2349
2451
|
};
|
|
2350
2452
|
|
|
2351
2453
|
// src/llm/providers/GeminiProvider.ts
|
|
2352
|
-
var
|
|
2454
|
+
var import_generative_ai = require("@google/generative-ai");
|
|
2455
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-004";
|
|
2456
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
2457
|
+
var DEFAULT_MAX_TOKENS = 1024;
|
|
2458
|
+
function sanitizeModel(model) {
|
|
2459
|
+
return model ? model.split(":")[0] : model;
|
|
2460
|
+
}
|
|
2461
|
+
function buildClient(apiKey) {
|
|
2462
|
+
return new import_generative_ai.GoogleGenerativeAI(apiKey);
|
|
2463
|
+
}
|
|
2464
|
+
function applyPrefix(text, taskType, queryPrefix, docPrefix) {
|
|
2465
|
+
if (taskType === "query" && queryPrefix && !text.startsWith(queryPrefix)) {
|
|
2466
|
+
return `${queryPrefix}${text}`;
|
|
2467
|
+
}
|
|
2468
|
+
if (taskType === "document" && docPrefix && !text.startsWith(docPrefix)) {
|
|
2469
|
+
return `${docPrefix}${text}`;
|
|
2470
|
+
}
|
|
2471
|
+
return text;
|
|
2472
|
+
}
|
|
2353
2473
|
var GeminiProvider = class {
|
|
2354
2474
|
constructor(llmConfig, embeddingConfig) {
|
|
2355
|
-
if (!llmConfig.apiKey)
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2475
|
+
if (!llmConfig.apiKey) {
|
|
2476
|
+
throw new Error("[GeminiProvider] llmConfig.apiKey is required");
|
|
2477
|
+
}
|
|
2478
|
+
this.llmConfig = __spreadProps(__spreadValues({}, llmConfig), { model: sanitizeModel(llmConfig.model) });
|
|
2479
|
+
this.client = buildClient(this.llmConfig.apiKey);
|
|
2360
2480
|
if (embeddingConfig) {
|
|
2361
2481
|
this.embeddingConfig = __spreadProps(__spreadValues({}, embeddingConfig), {
|
|
2362
|
-
model:
|
|
2482
|
+
model: sanitizeModel(embeddingConfig.model)
|
|
2363
2483
|
});
|
|
2364
2484
|
}
|
|
2365
2485
|
}
|
|
2486
|
+
// -------------------------------------------------------------------------
|
|
2487
|
+
// Static factory helpers
|
|
2488
|
+
// -------------------------------------------------------------------------
|
|
2366
2489
|
static getValidator() {
|
|
2367
2490
|
return {
|
|
2368
2491
|
validate(config) {
|
|
@@ -2376,7 +2499,11 @@ var GeminiProvider = class {
|
|
|
2376
2499
|
});
|
|
2377
2500
|
}
|
|
2378
2501
|
if (!config.model) {
|
|
2379
|
-
errors.push({
|
|
2502
|
+
errors.push({
|
|
2503
|
+
field: "llm.model",
|
|
2504
|
+
message: "Gemini model name is required",
|
|
2505
|
+
severity: "error"
|
|
2506
|
+
});
|
|
2380
2507
|
}
|
|
2381
2508
|
return errors;
|
|
2382
2509
|
}
|
|
@@ -2385,13 +2512,15 @@ var GeminiProvider = class {
|
|
|
2385
2512
|
static getHealthChecker() {
|
|
2386
2513
|
return {
|
|
2387
2514
|
async check(config) {
|
|
2515
|
+
var _a, _b;
|
|
2388
2516
|
const timestamp = Date.now();
|
|
2389
|
-
const apiKey = config.apiKey
|
|
2390
|
-
const modelName = config.model;
|
|
2517
|
+
const apiKey = (_b = (_a = config.apiKey) != null ? _a : process.env.GOOGLE_GENAI_API_KEY) != null ? _b : "";
|
|
2518
|
+
const modelName = sanitizeModel(config.model);
|
|
2391
2519
|
try {
|
|
2392
|
-
const {
|
|
2393
|
-
const
|
|
2394
|
-
|
|
2520
|
+
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import("@google/generative-ai");
|
|
2521
|
+
const ai = new GoogleGenerativeAI2(apiKey);
|
|
2522
|
+
const model = ai.getGenerativeModel({ model: DEFAULT_EMBEDDING_MODEL });
|
|
2523
|
+
await model.embedContent("health-check");
|
|
2395
2524
|
return {
|
|
2396
2525
|
healthy: true,
|
|
2397
2526
|
provider: "gemini",
|
|
@@ -2409,70 +2538,90 @@ var GeminiProvider = class {
|
|
|
2409
2538
|
}
|
|
2410
2539
|
};
|
|
2411
2540
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2541
|
+
// -------------------------------------------------------------------------
|
|
2542
|
+
// Private utilities
|
|
2543
|
+
// -------------------------------------------------------------------------
|
|
2544
|
+
/** Resolve the embedding client — uses a separate client when the embedding
|
|
2545
|
+
* API key differs from the LLM API key. */
|
|
2546
|
+
get embeddingClient() {
|
|
2547
|
+
var _a;
|
|
2548
|
+
if (((_a = this.embeddingConfig) == null ? void 0 : _a.apiKey) && this.embeddingConfig.apiKey !== this.llmConfig.apiKey) {
|
|
2549
|
+
return buildClient(this.embeddingConfig.apiKey);
|
|
2550
|
+
}
|
|
2551
|
+
return this.client;
|
|
2415
2552
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2553
|
+
/** Resolve the embedding model to use, in order of specificity. */
|
|
2554
|
+
resolveEmbeddingModel(optionsModel) {
|
|
2555
|
+
var _a, _b;
|
|
2556
|
+
return sanitizeModel((_b = optionsModel != null ? optionsModel : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _b : DEFAULT_EMBEDDING_MODEL);
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* Build the system instruction string, inserting the RAG context either via
|
|
2560
|
+
* the `{{context}}` placeholder or by appending it.
|
|
2561
|
+
*/
|
|
2562
|
+
buildSystemInstruction(context) {
|
|
2563
|
+
var _a;
|
|
2564
|
+
const base = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
|
|
2419
2565
|
|
|
2420
2566
|
Context:
|
|
2421
2567
|
${context}`;
|
|
2422
|
-
|
|
2568
|
+
return base.includes("{{context}}") ? base.replace("{{context}}", context) : `${base}
|
|
2423
2569
|
|
|
2424
2570
|
Context:
|
|
2425
2571
|
${context}`;
|
|
2426
|
-
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* Convert ChatMessage[] to the Gemini contents format.
|
|
2575
|
+
*
|
|
2576
|
+
* Rules enforced by the Gemini API:
|
|
2577
|
+
* - Only `user` and `model` roles are allowed in `contents`.
|
|
2578
|
+
* - System messages must be passed via `systemInstruction`, not here.
|
|
2579
|
+
* - Messages must strictly alternate between `user` and `model`.
|
|
2580
|
+
*/
|
|
2581
|
+
buildGeminiContents(messages) {
|
|
2582
|
+
return messages.filter((m) => m.role !== "system").map((m) => ({
|
|
2427
2583
|
role: m.role === "assistant" ? "model" : "user",
|
|
2428
2584
|
parts: [{ text: m.content }]
|
|
2429
2585
|
}));
|
|
2430
|
-
|
|
2586
|
+
}
|
|
2587
|
+
// -------------------------------------------------------------------------
|
|
2588
|
+
// ILLMProvider — chat
|
|
2589
|
+
// -------------------------------------------------------------------------
|
|
2590
|
+
async chat(messages, context, options) {
|
|
2591
|
+
var _a, _b, _c, _d;
|
|
2592
|
+
const model = this.client.getGenerativeModel({
|
|
2431
2593
|
model: this.llmConfig.model,
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2594
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
2595
|
+
});
|
|
2596
|
+
const result = await model.generateContent({
|
|
2597
|
+
contents: this.buildGeminiContents(messages),
|
|
2598
|
+
generationConfig: {
|
|
2599
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
2600
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
2437
2601
|
stopSequences: options == null ? void 0 : options.stop
|
|
2438
2602
|
}
|
|
2439
2603
|
});
|
|
2440
|
-
|
|
2441
|
-
return text;
|
|
2604
|
+
return result.response.text();
|
|
2442
2605
|
}
|
|
2443
2606
|
chatStream(messages, context, options) {
|
|
2444
2607
|
return __asyncGenerator(this, null, function* () {
|
|
2445
|
-
var _a, _b, _c, _d
|
|
2446
|
-
const
|
|
2447
|
-
|
|
2448
|
-
Context:
|
|
2449
|
-
${context}`;
|
|
2450
|
-
const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
|
|
2451
|
-
|
|
2452
|
-
Context:
|
|
2453
|
-
${context}`;
|
|
2454
|
-
const geminiMessages = messages.map((m) => ({
|
|
2455
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
2456
|
-
parts: [{ text: m.content }]
|
|
2457
|
-
}));
|
|
2458
|
-
const result = yield new __await(this.client.models.generateContentStream({
|
|
2608
|
+
var _a, _b, _c, _d;
|
|
2609
|
+
const model = this.client.getGenerativeModel({
|
|
2459
2610
|
model: this.llmConfig.model,
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2611
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
2612
|
+
});
|
|
2613
|
+
const result = yield new __await(model.generateContentStream({
|
|
2614
|
+
contents: this.buildGeminiContents(messages),
|
|
2615
|
+
generationConfig: {
|
|
2616
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
2617
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
2465
2618
|
stopSequences: options == null ? void 0 : options.stop
|
|
2466
2619
|
}
|
|
2467
2620
|
}));
|
|
2468
|
-
const stream = (_f = result == null ? void 0 : result.stream) != null ? _f : result;
|
|
2469
|
-
if (!stream) {
|
|
2470
|
-
throw new Error("[GeminiProvider] generateContentStream returned undefined");
|
|
2471
|
-
}
|
|
2472
2621
|
try {
|
|
2473
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2622
|
+
for (var iter = __forAwait(result.stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2474
2623
|
const chunk = temp.value;
|
|
2475
|
-
const text =
|
|
2624
|
+
const text = chunk.text();
|
|
2476
2625
|
if (text) yield text;
|
|
2477
2626
|
}
|
|
2478
2627
|
} catch (temp) {
|
|
@@ -2487,69 +2636,69 @@ ${context}`;
|
|
|
2487
2636
|
}
|
|
2488
2637
|
});
|
|
2489
2638
|
}
|
|
2639
|
+
// -------------------------------------------------------------------------
|
|
2640
|
+
// ILLMProvider — embeddings
|
|
2641
|
+
// -------------------------------------------------------------------------
|
|
2490
2642
|
async embed(text, options) {
|
|
2491
|
-
var _a, _b
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2643
|
+
var _a, _b;
|
|
2644
|
+
const content = applyPrefix(
|
|
2645
|
+
text,
|
|
2646
|
+
options == null ? void 0 : options.taskType,
|
|
2647
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
2648
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
2494
2649
|
);
|
|
2495
|
-
const
|
|
2496
|
-
const
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
|
|
2500
|
-
if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
|
|
2501
|
-
if (!content.startsWith(queryPrefix)) {
|
|
2502
|
-
content = `${queryPrefix}${text}`;
|
|
2503
|
-
}
|
|
2504
|
-
} else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
|
|
2505
|
-
if (!content.startsWith(docPrefix)) {
|
|
2506
|
-
content = `${docPrefix}${text}`;
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
const response = await client.models.embedContent({
|
|
2510
|
-
model,
|
|
2511
|
-
contents: content,
|
|
2512
|
-
config: ((_h = this.embeddingConfig) == null ? void 0 : _h.dimensions) ? { outputDimensionality: this.embeddingConfig.dimensions } : void 0
|
|
2650
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
2651
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
2652
|
+
const response = await model.embedContent({
|
|
2653
|
+
content: { role: "user", parts: [{ text: content }] }
|
|
2513
2654
|
}).catch((err) => {
|
|
2514
|
-
console.error(`[GeminiProvider] Embedding failed for model "${
|
|
2655
|
+
console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
|
|
2515
2656
|
throw err;
|
|
2516
2657
|
});
|
|
2517
|
-
return
|
|
2658
|
+
return response.embedding.values;
|
|
2518
2659
|
}
|
|
2519
2660
|
async batchEmbed(texts, options) {
|
|
2520
|
-
|
|
2521
|
-
const model = this.
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2661
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
2662
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
2663
|
+
const requests = texts.map((text) => {
|
|
2664
|
+
var _a, _b;
|
|
2665
|
+
return {
|
|
2666
|
+
content: {
|
|
2667
|
+
role: "user",
|
|
2668
|
+
parts: [{
|
|
2669
|
+
text: applyPrefix(
|
|
2670
|
+
text,
|
|
2671
|
+
options == null ? void 0 : options.taskType,
|
|
2672
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
2673
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
2674
|
+
)
|
|
2675
|
+
}]
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
});
|
|
2679
|
+
const response = await model.batchEmbedContents({
|
|
2680
|
+
requests: requests.map((r) => ({
|
|
2681
|
+
model: `models/${modelName}`,
|
|
2682
|
+
content: r.content
|
|
2683
|
+
}))
|
|
2538
2684
|
}).catch((err) => {
|
|
2539
|
-
console.error(`[GeminiProvider] Batch embedding failed for model "${
|
|
2685
|
+
console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
|
|
2540
2686
|
throw err;
|
|
2541
2687
|
});
|
|
2542
|
-
return
|
|
2543
|
-
var
|
|
2544
|
-
return (
|
|
2545
|
-
})
|
|
2688
|
+
return response.embeddings.map((e) => {
|
|
2689
|
+
var _a;
|
|
2690
|
+
return (_a = e.values) != null ? _a : [];
|
|
2691
|
+
});
|
|
2546
2692
|
}
|
|
2693
|
+
// -------------------------------------------------------------------------
|
|
2694
|
+
// ILLMProvider — health
|
|
2695
|
+
// -------------------------------------------------------------------------
|
|
2547
2696
|
async ping() {
|
|
2548
2697
|
try {
|
|
2549
|
-
|
|
2550
|
-
model:
|
|
2551
|
-
contents: "ping"
|
|
2698
|
+
const model = this.embeddingClient.getGenerativeModel({
|
|
2699
|
+
model: this.resolveEmbeddingModel()
|
|
2552
2700
|
});
|
|
2701
|
+
await model.embedContent("ping");
|
|
2553
2702
|
return true;
|
|
2554
2703
|
} catch (err) {
|
|
2555
2704
|
console.error("[GeminiProvider] Ping failed:", err);
|
|
@@ -2706,9 +2855,51 @@ ${context != null ? context : "None"}` },
|
|
|
2706
2855
|
};
|
|
2707
2856
|
|
|
2708
2857
|
// src/llm/LLMFactory.ts
|
|
2858
|
+
var customProviders = /* @__PURE__ */ new Map();
|
|
2709
2859
|
var LLMFactory = class _LLMFactory {
|
|
2860
|
+
/**
|
|
2861
|
+
* Register a custom LLM provider factory at runtime.
|
|
2862
|
+
*
|
|
2863
|
+
* Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
|
|
2864
|
+
* Bedrock, etc.) without modifying this library's source code.
|
|
2865
|
+
*
|
|
2866
|
+
* @example
|
|
2867
|
+
* // In your Next.js app initialization:
|
|
2868
|
+
* import { LLMFactory } from '@retrivora-ai/rag-engine/server';
|
|
2869
|
+
* import { MyCustomProvider } from './providers/MyCustomProvider';
|
|
2870
|
+
*
|
|
2871
|
+
* LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
|
|
2872
|
+
*
|
|
2873
|
+
* // Then set in your .env.local:
|
|
2874
|
+
* // LLM_PROVIDER=my-provider
|
|
2875
|
+
*/
|
|
2876
|
+
static register(name, factory) {
|
|
2877
|
+
customProviders.set(name.toLowerCase(), factory);
|
|
2878
|
+
console.log(`[LLMFactory] Registered custom provider: "${name}"`);
|
|
2879
|
+
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Unregister a previously registered custom provider.
|
|
2882
|
+
*/
|
|
2883
|
+
static unregister(name) {
|
|
2884
|
+
customProviders.delete(name.toLowerCase());
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* List all registered provider names (built-in + custom).
|
|
2888
|
+
*/
|
|
2889
|
+
static listProviders() {
|
|
2890
|
+
return [
|
|
2891
|
+
"openai",
|
|
2892
|
+
"anthropic",
|
|
2893
|
+
"ollama",
|
|
2894
|
+
"gemini",
|
|
2895
|
+
"rest",
|
|
2896
|
+
"universal_rest",
|
|
2897
|
+
"custom",
|
|
2898
|
+
...Array.from(customProviders.keys())
|
|
2899
|
+
];
|
|
2900
|
+
}
|
|
2710
2901
|
static create(llmConfig, embeddingConfig) {
|
|
2711
|
-
var _a;
|
|
2902
|
+
var _a, _b;
|
|
2712
2903
|
switch (llmConfig.provider) {
|
|
2713
2904
|
case "openai":
|
|
2714
2905
|
return new OpenAIProvider(llmConfig, embeddingConfig);
|
|
@@ -2722,11 +2913,19 @@ var LLMFactory = class _LLMFactory {
|
|
|
2722
2913
|
case "universal_rest":
|
|
2723
2914
|
case "custom":
|
|
2724
2915
|
return new UniversalLLMAdapter(llmConfig);
|
|
2725
|
-
default:
|
|
2726
|
-
|
|
2916
|
+
default: {
|
|
2917
|
+
const providerName = String((_a = llmConfig.provider) != null ? _a : "").toLowerCase();
|
|
2918
|
+
const customFactory = customProviders.get(providerName);
|
|
2919
|
+
if (customFactory) {
|
|
2920
|
+
return customFactory(llmConfig);
|
|
2921
|
+
}
|
|
2922
|
+
if (llmConfig.baseUrl || ((_b = llmConfig.options) == null ? void 0 : _b.baseUrl)) {
|
|
2727
2923
|
return new UniversalLLMAdapter(llmConfig);
|
|
2728
2924
|
}
|
|
2729
|
-
throw new Error(
|
|
2925
|
+
throw new Error(
|
|
2926
|
+
`[LLMFactory] Unknown provider "${llmConfig.provider}". Built-in providers: ${_LLMFactory.listProviders().join(", ")}. Register a custom provider with LLMFactory.register().`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2730
2929
|
}
|
|
2731
2930
|
}
|
|
2732
2931
|
static getValidator(provider) {
|
|
@@ -2816,8 +3015,8 @@ var ProviderRegistry = class {
|
|
|
2816
3015
|
}
|
|
2817
3016
|
case "pgvector":
|
|
2818
3017
|
case "postgresql": {
|
|
2819
|
-
const {
|
|
2820
|
-
return
|
|
3018
|
+
const { MultiTablePostgresProvider: MultiTablePostgresProvider2 } = await Promise.resolve().then(() => (init_MultiTablePostgresProvider(), MultiTablePostgresProvider_exports));
|
|
3019
|
+
return MultiTablePostgresProvider2;
|
|
2821
3020
|
}
|
|
2822
3021
|
case "mongodb": {
|
|
2823
3022
|
const { MongoDBProvider: MongoDBProvider2 } = await Promise.resolve().then(() => (init_MongoDBProvider(), MongoDBProvider_exports));
|
|
@@ -3775,73 +3974,102 @@ var QueryProcessor = class {
|
|
|
3775
3974
|
}
|
|
3776
3975
|
};
|
|
3777
3976
|
|
|
3977
|
+
// src/utils/synonyms.ts
|
|
3978
|
+
var FIELD_SYNONYMS = {
|
|
3979
|
+
name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
|
|
3980
|
+
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
|
|
3981
|
+
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
3982
|
+
image: [
|
|
3983
|
+
"imageUrl",
|
|
3984
|
+
"thumbnail",
|
|
3985
|
+
"img",
|
|
3986
|
+
"url",
|
|
3987
|
+
"photo",
|
|
3988
|
+
"picture",
|
|
3989
|
+
"media",
|
|
3990
|
+
"image_url",
|
|
3991
|
+
"main_image",
|
|
3992
|
+
"product_image",
|
|
3993
|
+
"thumb"
|
|
3994
|
+
],
|
|
3995
|
+
stock: [
|
|
3996
|
+
"inventory",
|
|
3997
|
+
"quantity",
|
|
3998
|
+
"count",
|
|
3999
|
+
"availability",
|
|
4000
|
+
"stock_level",
|
|
4001
|
+
"inStock",
|
|
4002
|
+
"is_available",
|
|
4003
|
+
"in stock",
|
|
4004
|
+
"status"
|
|
4005
|
+
],
|
|
4006
|
+
description: ["summary", "content", "body", "text", "info", "details"],
|
|
4007
|
+
link: ["url", "href", "product_url", "page_url", "link"]
|
|
4008
|
+
};
|
|
4009
|
+
function resolveMetadataValue(meta, uiKey) {
|
|
4010
|
+
var _a;
|
|
4011
|
+
const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
|
|
4012
|
+
const keys = Object.keys(meta);
|
|
4013
|
+
const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
|
|
4014
|
+
let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
|
|
4015
|
+
if (match !== void 0) return meta[match];
|
|
4016
|
+
match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
|
|
4017
|
+
if (match !== void 0) return meta[match];
|
|
4018
|
+
const isBlacklisted = (kl) => {
|
|
4019
|
+
return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
|
|
4020
|
+
};
|
|
4021
|
+
match = keys.find((k) => {
|
|
4022
|
+
const kl = k.toLowerCase();
|
|
4023
|
+
if (isBlacklisted(kl)) return false;
|
|
4024
|
+
return kl.includes(uiKey.toLowerCase());
|
|
4025
|
+
});
|
|
4026
|
+
if (match !== void 0) return meta[match];
|
|
4027
|
+
match = keys.find((k) => {
|
|
4028
|
+
const kl = k.toLowerCase();
|
|
4029
|
+
if (isBlacklisted(kl)) return false;
|
|
4030
|
+
return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
|
|
4031
|
+
});
|
|
4032
|
+
return match !== void 0 ? meta[match] : void 0;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3778
4035
|
// src/utils/UITransformer.ts
|
|
3779
4036
|
var UITransformer = class {
|
|
3780
4037
|
/**
|
|
3781
4038
|
* Main transformation method
|
|
3782
|
-
* Analyzes user query and retrieved data to determine
|
|
3783
|
-
*
|
|
3784
|
-
* @param userQuery - The original user question/query
|
|
3785
|
-
* @param retrievedData - Vector database retrieval results
|
|
3786
|
-
* @returns Structured JSON response for UI rendering
|
|
4039
|
+
* Analyzes user query and retrieved data to determine if a product carousel is needed.
|
|
3787
4040
|
*/
|
|
3788
|
-
static transform(userQuery, retrievedData) {
|
|
4041
|
+
static transform(userQuery, retrievedData, config, trainedSchema) {
|
|
3789
4042
|
if (!retrievedData || retrievedData.length === 0) {
|
|
3790
4043
|
return this.createTextResponse("No data available", "No relevant data found for your query.");
|
|
3791
4044
|
}
|
|
3792
|
-
const
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
case "line_chart":
|
|
3801
|
-
return this.transformToLineChart(retrievedData);
|
|
3802
|
-
case "table":
|
|
3803
|
-
return this.transformToTable(retrievedData);
|
|
3804
|
-
default:
|
|
3805
|
-
return this.transformToText(retrievedData);
|
|
4045
|
+
const isStockRequest = this.isStockQuery(userQuery);
|
|
4046
|
+
const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
|
|
4047
|
+
const categories = this.detectCategories(filteredData);
|
|
4048
|
+
const hasProducts = filteredData.some((item) => this.isProductData(item));
|
|
4049
|
+
const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
|
|
4050
|
+
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
4051
|
+
if (isTrendQuery && isTimeSeries) {
|
|
4052
|
+
return this.transformToLineChart(filteredData);
|
|
3806
4053
|
}
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
* Analyzes the data and query to suggest the best visualization type
|
|
3810
|
-
*/
|
|
3811
|
-
static analyzeData(query, data) {
|
|
3812
|
-
const queryLower = query.toLowerCase();
|
|
3813
|
-
const hasProducts = data.some(
|
|
3814
|
-
(item) => this.isProductData(item)
|
|
3815
|
-
);
|
|
3816
|
-
const hasTimeSeries = data.some(
|
|
3817
|
-
(item) => this.isTimeSeriesData(item)
|
|
3818
|
-
);
|
|
3819
|
-
const hasCategories = this.detectCategories(data).length > 0;
|
|
3820
|
-
const isDistributionQuery = queryLower.includes("distribution") || queryLower.includes("breakdown") || queryLower.includes("percentage") || queryLower.includes("proportion");
|
|
3821
|
-
const isComparisonQuery = queryLower.includes("compare") || queryLower.includes("vs") || queryLower.includes("difference");
|
|
3822
|
-
const isTrendQuery = queryLower.includes("trend") || queryLower.includes("over time") || queryLower.includes("growth") || queryLower.includes("decline");
|
|
3823
|
-
if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
|
|
3824
|
-
return { suggestedType: "product_carousel", confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
|
|
3825
|
-
}
|
|
3826
|
-
if (isTrendQuery && hasTimeSeries) {
|
|
3827
|
-
return { suggestedType: "line_chart", confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
|
|
4054
|
+
if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
|
|
4055
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
3828
4056
|
}
|
|
3829
|
-
if (
|
|
3830
|
-
return
|
|
4057
|
+
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
4058
|
+
return this.transformToPieChart(filteredData);
|
|
3831
4059
|
}
|
|
3832
|
-
if (
|
|
3833
|
-
return
|
|
4060
|
+
if (hasProducts) {
|
|
4061
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
3834
4062
|
}
|
|
3835
|
-
if (
|
|
3836
|
-
return
|
|
4063
|
+
if (this.hasMultipleFields(filteredData)) {
|
|
4064
|
+
return this.transformToTable(filteredData);
|
|
3837
4065
|
}
|
|
3838
|
-
return
|
|
4066
|
+
return this.transformToText(filteredData);
|
|
3839
4067
|
}
|
|
3840
4068
|
/**
|
|
3841
4069
|
* Transform data to product carousel format
|
|
3842
4070
|
*/
|
|
3843
|
-
static transformToProductCarousel(data) {
|
|
3844
|
-
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item)).filter((p) => p !== null);
|
|
4071
|
+
static transformToProductCarousel(data, config, trainedSchema) {
|
|
4072
|
+
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
|
|
3845
4073
|
return {
|
|
3846
4074
|
type: "product_carousel",
|
|
3847
4075
|
title: "Recommended Products",
|
|
@@ -3871,28 +4099,6 @@ var UITransformer = class {
|
|
|
3871
4099
|
data: pieData
|
|
3872
4100
|
};
|
|
3873
4101
|
}
|
|
3874
|
-
/**
|
|
3875
|
-
* Transform data to bar chart format
|
|
3876
|
-
*/
|
|
3877
|
-
static transformToBarChart(data) {
|
|
3878
|
-
const categories = this.detectCategories(data);
|
|
3879
|
-
const categoryData = this.aggregateByCategory(data, categories);
|
|
3880
|
-
const barData = Object.entries(categoryData).map(([category, value]) => {
|
|
3881
|
-
const { inStockCount, outOfStockCount } = this.calculateStockCounts(category, data);
|
|
3882
|
-
return {
|
|
3883
|
-
category,
|
|
3884
|
-
value,
|
|
3885
|
-
inStockCount,
|
|
3886
|
-
outOfStockCount
|
|
3887
|
-
};
|
|
3888
|
-
});
|
|
3889
|
-
return {
|
|
3890
|
-
type: "bar_chart",
|
|
3891
|
-
title: "Comparison by Category",
|
|
3892
|
-
description: `Comparing ${categories.length} categories`,
|
|
3893
|
-
data: barData
|
|
3894
|
-
};
|
|
3895
|
-
}
|
|
3896
4102
|
/**
|
|
3897
4103
|
* Transform data to line chart format
|
|
3898
4104
|
*/
|
|
@@ -3949,49 +4155,253 @@ var UITransformer = class {
|
|
|
3949
4155
|
data: { content }
|
|
3950
4156
|
};
|
|
3951
4157
|
}
|
|
4158
|
+
static parseTransformationResponse(raw) {
|
|
4159
|
+
const payloadText = this.extractJsonCandidate(raw);
|
|
4160
|
+
if (!payloadText) return null;
|
|
4161
|
+
try {
|
|
4162
|
+
const parsed = JSON.parse(payloadText);
|
|
4163
|
+
return this.normalizeTransformation(parsed);
|
|
4164
|
+
} catch (e) {
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
static extractJsonCandidate(raw) {
|
|
4169
|
+
if (!raw) return null;
|
|
4170
|
+
const cleaned = raw.replace(/```(?:json|chart|ui)?\s*/gi, "").replace(/```/g, "").trim();
|
|
4171
|
+
const start = cleaned.indexOf("{");
|
|
4172
|
+
if (start === -1) return null;
|
|
4173
|
+
let depth = 0;
|
|
4174
|
+
let inString = false;
|
|
4175
|
+
let escape = false;
|
|
4176
|
+
for (let i = start; i < cleaned.length; i += 1) {
|
|
4177
|
+
const char = cleaned[i];
|
|
4178
|
+
if (escape) {
|
|
4179
|
+
escape = false;
|
|
4180
|
+
continue;
|
|
4181
|
+
}
|
|
4182
|
+
if (char === "\\") {
|
|
4183
|
+
escape = true;
|
|
4184
|
+
continue;
|
|
4185
|
+
}
|
|
4186
|
+
if (char === '"') {
|
|
4187
|
+
inString = !inString;
|
|
4188
|
+
continue;
|
|
4189
|
+
}
|
|
4190
|
+
if (inString) continue;
|
|
4191
|
+
if (char === "{") depth += 1;
|
|
4192
|
+
if (char === "}") {
|
|
4193
|
+
depth -= 1;
|
|
4194
|
+
if (depth === 0) {
|
|
4195
|
+
return cleaned.slice(start, i + 1);
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
return null;
|
|
4200
|
+
}
|
|
4201
|
+
static normalizeTransformation(payload) {
|
|
4202
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
4203
|
+
if (!payload || typeof payload !== "object") return null;
|
|
4204
|
+
const payloadObj = payload;
|
|
4205
|
+
const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
|
|
4206
|
+
if (!type) return null;
|
|
4207
|
+
const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
|
|
4208
|
+
const description = payloadObj.description ? String(payloadObj.description) : void 0;
|
|
4209
|
+
const rawData = (_j = (_i = (_h = (_g = (_f = payloadObj.data) != null ? _f : payloadObj.table) != null ? _g : payloadObj.rows) != null ? _h : payloadObj.items) != null ? _i : payloadObj.content) != null ? _j : null;
|
|
4210
|
+
const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
|
|
4211
|
+
const transformation = {
|
|
4212
|
+
type,
|
|
4213
|
+
title,
|
|
4214
|
+
description,
|
|
4215
|
+
data
|
|
4216
|
+
};
|
|
4217
|
+
return this.validateTransformation(transformation) ? transformation : null;
|
|
4218
|
+
}
|
|
4219
|
+
static normalizeVisualizationType(type) {
|
|
4220
|
+
var _a;
|
|
4221
|
+
const mapping = {
|
|
4222
|
+
pie: "pie_chart",
|
|
4223
|
+
pie_chart: "pie_chart",
|
|
4224
|
+
bar: "bar_chart",
|
|
4225
|
+
bar_chart: "bar_chart",
|
|
4226
|
+
line: "line_chart",
|
|
4227
|
+
line_chart: "line_chart",
|
|
4228
|
+
radar: "radar_chart",
|
|
4229
|
+
radar_chart: "radar_chart",
|
|
4230
|
+
table: "table",
|
|
4231
|
+
text: "text",
|
|
4232
|
+
product_carousel: "product_carousel",
|
|
4233
|
+
carousel: "carousel"
|
|
4234
|
+
};
|
|
4235
|
+
return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
|
|
4236
|
+
}
|
|
4237
|
+
static validateTransformation(transformation) {
|
|
4238
|
+
const { type, data } = transformation;
|
|
4239
|
+
switch (type) {
|
|
4240
|
+
case "pie_chart":
|
|
4241
|
+
case "bar_chart":
|
|
4242
|
+
return Array.isArray(data) && data.every(
|
|
4243
|
+
(item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
4244
|
+
);
|
|
4245
|
+
case "line_chart":
|
|
4246
|
+
return Array.isArray(data) && data.every(
|
|
4247
|
+
(item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
4248
|
+
);
|
|
4249
|
+
case "radar_chart":
|
|
4250
|
+
return Array.isArray(data) && data.every(
|
|
4251
|
+
(item) => item !== null && typeof item === "object" && "attribute" in item
|
|
4252
|
+
);
|
|
4253
|
+
case "table":
|
|
4254
|
+
return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
|
|
4255
|
+
case "text":
|
|
4256
|
+
return typeof data === "object" && data !== null && typeof data.content === "string";
|
|
4257
|
+
case "product_carousel":
|
|
4258
|
+
case "carousel":
|
|
4259
|
+
return Array.isArray(data) && data.every(
|
|
4260
|
+
(item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
|
|
4261
|
+
);
|
|
4262
|
+
default:
|
|
4263
|
+
return false;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
3952
4266
|
/**
|
|
3953
4267
|
* Helper: Check if data item is product-related
|
|
3954
4268
|
*/
|
|
3955
4269
|
static isProductData(item) {
|
|
3956
4270
|
const content = (item.content || "").toLowerCase();
|
|
3957
|
-
const productKeywords = [
|
|
3958
|
-
|
|
3959
|
-
|
|
4271
|
+
const productKeywords = [
|
|
4272
|
+
"product",
|
|
4273
|
+
"price",
|
|
4274
|
+
"stock",
|
|
4275
|
+
"item",
|
|
4276
|
+
"sku",
|
|
4277
|
+
"brand",
|
|
4278
|
+
"model",
|
|
4279
|
+
"msrp",
|
|
4280
|
+
"inventory",
|
|
4281
|
+
"buy",
|
|
4282
|
+
"shop",
|
|
4283
|
+
"beauty",
|
|
4284
|
+
"care",
|
|
4285
|
+
"cosmetic",
|
|
4286
|
+
"facial",
|
|
4287
|
+
"cream",
|
|
4288
|
+
"serum",
|
|
4289
|
+
"mask",
|
|
4290
|
+
"makeup",
|
|
4291
|
+
"fragrance"
|
|
4292
|
+
];
|
|
4293
|
+
const hasKeywords = productKeywords.some((kw) => content.includes(kw));
|
|
4294
|
+
const hasMetadataKey = Object.keys(item.metadata || {}).some(
|
|
4295
|
+
(k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
|
|
3960
4296
|
);
|
|
4297
|
+
const hasPricePattern = /\$\s*\d+/.test(content);
|
|
4298
|
+
return hasKeywords || hasMetadataKey || hasPricePattern;
|
|
3961
4299
|
}
|
|
3962
4300
|
/**
|
|
3963
4301
|
* Helper: Check if data contains time series
|
|
3964
4302
|
*/
|
|
3965
4303
|
static isTimeSeriesData(item) {
|
|
3966
4304
|
const content = (item.content || "").toLowerCase();
|
|
3967
|
-
const timeKeywords = ["
|
|
3968
|
-
|
|
4305
|
+
const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
|
|
4306
|
+
const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
|
|
4307
|
+
const metadata = item.metadata || {};
|
|
4308
|
+
const maybeDateKeys = Object.keys(metadata).filter(
|
|
3969
4309
|
(k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
|
|
3970
4310
|
);
|
|
4311
|
+
const hasValidDateValue = maybeDateKeys.some((key) => {
|
|
4312
|
+
const value = metadata[key];
|
|
4313
|
+
if (typeof value === "string") {
|
|
4314
|
+
return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
4315
|
+
}
|
|
4316
|
+
return typeof value === "number";
|
|
4317
|
+
});
|
|
4318
|
+
return hasTimeKeyword || hasValidDateValue;
|
|
4319
|
+
}
|
|
4320
|
+
static shouldShowCategoryChart(query, categories) {
|
|
4321
|
+
if (categories.length < 2) {
|
|
4322
|
+
return false;
|
|
4323
|
+
}
|
|
4324
|
+
const normalized = query.toLowerCase();
|
|
4325
|
+
const chartKeywords = [
|
|
4326
|
+
"distribution",
|
|
4327
|
+
"breakdown",
|
|
4328
|
+
"by category",
|
|
4329
|
+
"by type",
|
|
4330
|
+
"compare",
|
|
4331
|
+
"share",
|
|
4332
|
+
"percentage",
|
|
4333
|
+
"segmentation",
|
|
4334
|
+
"split",
|
|
4335
|
+
"category breakdown",
|
|
4336
|
+
"category distribution"
|
|
4337
|
+
];
|
|
4338
|
+
return chartKeywords.some((keyword) => normalized.includes(keyword));
|
|
4339
|
+
}
|
|
4340
|
+
static isTrendQuery(query) {
|
|
4341
|
+
const normalized = query.toLowerCase();
|
|
4342
|
+
const trendKeywords = [
|
|
4343
|
+
"trend",
|
|
4344
|
+
"over time",
|
|
4345
|
+
"historical",
|
|
4346
|
+
"growth",
|
|
4347
|
+
"decline",
|
|
4348
|
+
"increase",
|
|
4349
|
+
"decrease",
|
|
4350
|
+
"year",
|
|
4351
|
+
"month",
|
|
4352
|
+
"week",
|
|
4353
|
+
"day",
|
|
4354
|
+
"comparison",
|
|
4355
|
+
"compare",
|
|
4356
|
+
"changes",
|
|
4357
|
+
"timeline"
|
|
4358
|
+
];
|
|
4359
|
+
return trendKeywords.some((keyword) => normalized.includes(keyword));
|
|
4360
|
+
}
|
|
4361
|
+
static isStockQuery(query) {
|
|
4362
|
+
const normalized = query.toLowerCase();
|
|
4363
|
+
return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
|
|
3971
4364
|
}
|
|
3972
4365
|
/**
|
|
3973
|
-
* Helper: Extract
|
|
4366
|
+
* Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
|
|
3974
4367
|
*/
|
|
3975
|
-
static
|
|
3976
|
-
|
|
3977
|
-
if (meta
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
};
|
|
4368
|
+
static getDynamicVal(meta, uiKey, config, trainedSchema) {
|
|
4369
|
+
var _a;
|
|
4370
|
+
if (!meta) return void 0;
|
|
4371
|
+
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
4372
|
+
if (mapping && mapping[uiKey]) {
|
|
4373
|
+
const mappedKey = mapping[uiKey];
|
|
4374
|
+
if (meta[mappedKey] !== void 0) return meta[mappedKey];
|
|
4375
|
+
}
|
|
4376
|
+
if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
|
|
4377
|
+
const trainedKey = trainedSchema[uiKey];
|
|
4378
|
+
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
3987
4379
|
}
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
4380
|
+
return resolveMetadataValue(meta, uiKey);
|
|
4381
|
+
}
|
|
4382
|
+
static extractProductInfo(item, config, trainedSchema) {
|
|
4383
|
+
const meta = item.metadata || {};
|
|
4384
|
+
const name = this.getDynamicVal(meta, "name", config, trainedSchema);
|
|
4385
|
+
const price = this.getDynamicVal(meta, "price", config, trainedSchema);
|
|
4386
|
+
const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
|
|
4387
|
+
if (name || this.isProductData(item)) {
|
|
4388
|
+
let finalName = name ? String(name) : void 0;
|
|
4389
|
+
if (!finalName) {
|
|
4390
|
+
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
4391
|
+
finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
|
|
4392
|
+
}
|
|
4393
|
+
let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
|
|
4394
|
+
if (!finalPrice) {
|
|
4395
|
+
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
|
|
4396
|
+
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
|
|
4397
|
+
}
|
|
4398
|
+
const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
|
|
3991
4399
|
return {
|
|
3992
4400
|
id: item.id,
|
|
3993
|
-
name:
|
|
3994
|
-
price:
|
|
4401
|
+
name: finalName,
|
|
4402
|
+
price: finalPrice,
|
|
4403
|
+
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
4404
|
+
brand: brand ? String(brand) : void 0,
|
|
3995
4405
|
description: item.content,
|
|
3996
4406
|
inStock: this.determineStockStatus(item)
|
|
3997
4407
|
};
|
|
@@ -4015,6 +4425,10 @@ var UITransformer = class {
|
|
|
4015
4425
|
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
4016
4426
|
tags.forEach((t) => categories.add(String(t)));
|
|
4017
4427
|
}
|
|
4428
|
+
const contentCategories = Array.from(new Set(
|
|
4429
|
+
Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
|
|
4430
|
+
));
|
|
4431
|
+
contentCategories.forEach((category) => categories.add(category));
|
|
4018
4432
|
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
4019
4433
|
if (categoryMatch) {
|
|
4020
4434
|
categories.add(categoryMatch[1].trim());
|
|
@@ -4033,7 +4447,7 @@ var UITransformer = class {
|
|
|
4033
4447
|
data.forEach((item) => {
|
|
4034
4448
|
const meta = item.metadata || {};
|
|
4035
4449
|
const itemCategory = meta.category || meta.type || "Other";
|
|
4036
|
-
if (
|
|
4450
|
+
if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
|
|
4037
4451
|
result[itemCategory]++;
|
|
4038
4452
|
} else {
|
|
4039
4453
|
result["Other"] = (result["Other"] || 0) + 1;
|
|
@@ -4135,6 +4549,213 @@ var UITransformer = class {
|
|
|
4135
4549
|
});
|
|
4136
4550
|
return fieldCount.size > 2;
|
|
4137
4551
|
}
|
|
4552
|
+
// ─── LLM-Driven Visualization Decision ────────────────────────────────────
|
|
4553
|
+
/**
|
|
4554
|
+
* analyzeAndDecide — sends user question + RAG data to the LLM with a
|
|
4555
|
+
* structured system prompt and parses the JSON response into a
|
|
4556
|
+
* UITransformationResponse.
|
|
4557
|
+
*
|
|
4558
|
+
* This is the recommended entry point for production use. The heuristic
|
|
4559
|
+
* `transform()` method is used as a fallback if the LLM call fails.
|
|
4560
|
+
*
|
|
4561
|
+
* System prompt instructs the LLM to:
|
|
4562
|
+
* - Analyze the question and retrieved data
|
|
4563
|
+
* - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
|
|
4564
|
+
* - Return a strict JSON object — no prose, no markdown fences
|
|
4565
|
+
*
|
|
4566
|
+
* @param query - the original user question
|
|
4567
|
+
* @param sources - vector DB matches returned by RAG retrieval
|
|
4568
|
+
* @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
|
|
4569
|
+
* @returns - a validated UITransformationResponse (type + title + description + data)
|
|
4570
|
+
*/
|
|
4571
|
+
static async analyzeAndDecide(query, sources, llm) {
|
|
4572
|
+
try {
|
|
4573
|
+
const context = this.buildContextSummary(sources);
|
|
4574
|
+
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
4575
|
+
const userPrompt = [
|
|
4576
|
+
`USER QUESTION: ${query}`,
|
|
4577
|
+
"",
|
|
4578
|
+
"RETRIEVED DATA (JSON):",
|
|
4579
|
+
context
|
|
4580
|
+
].join("\n");
|
|
4581
|
+
const rawResponse = await llm.chat(
|
|
4582
|
+
[{ role: "user", content: userPrompt }],
|
|
4583
|
+
"",
|
|
4584
|
+
{ systemPrompt, temperature: 0 }
|
|
4585
|
+
);
|
|
4586
|
+
const parsed = this.parseTransformationResponse(rawResponse);
|
|
4587
|
+
if (parsed) {
|
|
4588
|
+
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
4589
|
+
return parsed;
|
|
4590
|
+
}
|
|
4591
|
+
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
4592
|
+
} catch (err) {
|
|
4593
|
+
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
4594
|
+
}
|
|
4595
|
+
return this.transform(query, sources);
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Build the system prompt that instructs the LLM to return a visualization JSON.
|
|
4599
|
+
*/
|
|
4600
|
+
static buildVisualizationSystemPrompt() {
|
|
4601
|
+
return `You are a data visualization expert embedded in a RAG chat system.
|
|
4602
|
+
You will receive a user question and structured data retrieved from a vector database.
|
|
4603
|
+
Your ONLY job is to analyze this information and return a single JSON object that tells
|
|
4604
|
+
the frontend how to visualize it.
|
|
4605
|
+
|
|
4606
|
+
Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
|
|
4607
|
+
|
|
4608
|
+
The JSON must have this exact shape:
|
|
4609
|
+
{
|
|
4610
|
+
"type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
|
|
4611
|
+
"title": "A concise, descriptive title for the visualization",
|
|
4612
|
+
"description": "One sentence describing what the visualization shows",
|
|
4613
|
+
"data": <structured data \u2014 see rules below>
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
DATA SHAPE per type:
|
|
4617
|
+
- bar_chart: array of { "category": string, "value": number }
|
|
4618
|
+
- line_chart: array of { "timestamp": string, "value": number, "label": string }
|
|
4619
|
+
- pie_chart: array of { "label": string, "value": number }
|
|
4620
|
+
- radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
|
|
4621
|
+
Example for radar_chart:
|
|
4622
|
+
[
|
|
4623
|
+
{ "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
|
|
4624
|
+
{ "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
|
|
4625
|
+
{ "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
|
|
4626
|
+
]
|
|
4627
|
+
- table: { "columns": string[], "rows": (string|number)[][] }
|
|
4628
|
+
- text: { "content": "<prose answer>" }
|
|
4629
|
+
|
|
4630
|
+
DECISION RULES (follow strictly):
|
|
4631
|
+
1. bar_chart \u2192 comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
|
|
4632
|
+
2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
|
|
4633
|
+
3. pie_chart \u2192 proportional breakdown or percentage distribution
|
|
4634
|
+
4. radar_chart \u2192 comparing 2 or more products across MULTIPLE attributes (e.g. comparing features, ratings, or characteristics of 2 specific products). If the user asks to "compare" products and the data contains multiple dimensions or ratings for them, you MUST use this.
|
|
4635
|
+
5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
|
|
4636
|
+
6. text \u2192 conversational or free-form answers where no chart adds value
|
|
4637
|
+
|
|
4638
|
+
IMPORTANT:
|
|
4639
|
+
- Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
|
|
4640
|
+
- Ensure all "value" fields are numbers, not strings.
|
|
4641
|
+
- For bar/line/pie, keep at most 12 data points for readability.
|
|
4642
|
+
- Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
|
|
4643
|
+
}
|
|
4644
|
+
/**
|
|
4645
|
+
* Serialize retrieved vector matches into a compact JSON context string.
|
|
4646
|
+
* Limits the total character count to avoid exceeding LLM context windows.
|
|
4647
|
+
*/
|
|
4648
|
+
static buildContextSummary(sources, maxChars = 6e3) {
|
|
4649
|
+
const items = sources.map((s, i) => {
|
|
4650
|
+
var _a, _b, _c, _d;
|
|
4651
|
+
return {
|
|
4652
|
+
index: i + 1,
|
|
4653
|
+
content: (_b = (_a = s.content) == null ? void 0 : _a.substring(0, 400)) != null ? _b : "",
|
|
4654
|
+
metadata: (_c = s.metadata) != null ? _c : {},
|
|
4655
|
+
score: (_d = s.score) != null ? _d : 0
|
|
4656
|
+
};
|
|
4657
|
+
});
|
|
4658
|
+
const full = JSON.stringify(items, null, 2);
|
|
4659
|
+
if (full.length <= maxChars) return full;
|
|
4660
|
+
const partial = [];
|
|
4661
|
+
let chars = 2;
|
|
4662
|
+
for (const item of items) {
|
|
4663
|
+
const chunk = JSON.stringify(item);
|
|
4664
|
+
if (chars + chunk.length + 2 > maxChars) break;
|
|
4665
|
+
partial.push(item);
|
|
4666
|
+
chars += chunk.length + 2;
|
|
4667
|
+
}
|
|
4668
|
+
return JSON.stringify(partial, null, 2) + "\n// ... truncated";
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
|
|
4672
|
+
// src/utils/SchemaMapper.ts
|
|
4673
|
+
var SchemaMapper = class {
|
|
4674
|
+
/**
|
|
4675
|
+
* Trains the plugin on a set of keys.
|
|
4676
|
+
* This is done once per schema and cached.
|
|
4677
|
+
*/
|
|
4678
|
+
static async train(llm, projectId, keys) {
|
|
4679
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
4680
|
+
if (this.cache.has(cacheKey)) {
|
|
4681
|
+
return this.cache.get(cacheKey);
|
|
4682
|
+
}
|
|
4683
|
+
console.log(`[SchemaMapper] \u{1F9E0} Training AI on new schema keys: ${keys.join(", ")}`);
|
|
4684
|
+
const propertyList = Object.entries(this.TARGET_PROPERTIES).map(([prop, desc]) => `- ${prop} (${desc})`).join("\n");
|
|
4685
|
+
const prompt = `
|
|
4686
|
+
Given these metadata keys from a database: [${keys.join(", ")}]
|
|
4687
|
+
|
|
4688
|
+
Identify which keys best correspond to these standard UI properties:
|
|
4689
|
+
${propertyList}
|
|
4690
|
+
|
|
4691
|
+
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
4692
|
+
If no good match is found for a property, omit it.
|
|
4693
|
+
|
|
4694
|
+
Example:
|
|
4695
|
+
{
|
|
4696
|
+
"name": "Product_Title",
|
|
4697
|
+
"price": "MSRP_USD",
|
|
4698
|
+
"brand": "VendorName"
|
|
4699
|
+
}
|
|
4700
|
+
`;
|
|
4701
|
+
try {
|
|
4702
|
+
const response = await llm.chat(
|
|
4703
|
+
[
|
|
4704
|
+
{ role: "system", content: "You are a database schema expert. You ONLY respond with valid JSON." },
|
|
4705
|
+
{ role: "user", content: prompt }
|
|
4706
|
+
],
|
|
4707
|
+
""
|
|
4708
|
+
);
|
|
4709
|
+
const startIdx = response.indexOf("{");
|
|
4710
|
+
if (startIdx !== -1) {
|
|
4711
|
+
let braceCount = 0;
|
|
4712
|
+
let endIdx = -1;
|
|
4713
|
+
for (let i = startIdx; i < response.length; i++) {
|
|
4714
|
+
if (response[i] === "{") braceCount++;
|
|
4715
|
+
else if (response[i] === "}") {
|
|
4716
|
+
braceCount--;
|
|
4717
|
+
if (braceCount === 0) {
|
|
4718
|
+
endIdx = i;
|
|
4719
|
+
break;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
if (endIdx !== -1) {
|
|
4724
|
+
const jsonContent = response.substring(startIdx, endIdx + 1);
|
|
4725
|
+
const cleanJson = this.sanitizeJson(jsonContent);
|
|
4726
|
+
const mapping = JSON.parse(cleanJson);
|
|
4727
|
+
this.cache.set(cacheKey, mapping);
|
|
4728
|
+
return mapping;
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
} catch (error) {
|
|
4732
|
+
console.warn("[SchemaMapper] AI training failed, falling back to heuristics:", error);
|
|
4733
|
+
}
|
|
4734
|
+
return {};
|
|
4735
|
+
}
|
|
4736
|
+
/**
|
|
4737
|
+
* Forgiving JSON parser that fixes common AI formatting mistakes.
|
|
4738
|
+
*/
|
|
4739
|
+
static sanitizeJson(s) {
|
|
4740
|
+
return s.replace(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "").replace(/[\u201C\u201D\u2018\u2019]/g, '"').replace(/'/g, '"').replace(/([{,]\s*)([a-zA-Z0-9_]+?)\s*:/g, '$1"$2":').replace(/,\s*([}\]])/g, "$1").trim();
|
|
4741
|
+
}
|
|
4742
|
+
static getCached(projectId, keys) {
|
|
4743
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
4744
|
+
return this.cache.get(cacheKey);
|
|
4745
|
+
}
|
|
4746
|
+
};
|
|
4747
|
+
SchemaMapper.cache = /* @__PURE__ */ new Map();
|
|
4748
|
+
/**
|
|
4749
|
+
* Descriptions of standard UI properties to help the AI map fields accurately.
|
|
4750
|
+
*/
|
|
4751
|
+
SchemaMapper.TARGET_PROPERTIES = {
|
|
4752
|
+
name: "The primary title, name, or label of the item",
|
|
4753
|
+
price: "The numeric cost, price, MSRP, or amount",
|
|
4754
|
+
brand: "The manufacturer, vendor, brand, or maker",
|
|
4755
|
+
image: "The URL to an image, thumbnail, photo, or picture",
|
|
4756
|
+
stock: "The availability, inventory count, quantity, or stock level",
|
|
4757
|
+
description: "The detailed text content, summary, or body info",
|
|
4758
|
+
category: "The group, department, type, or category name"
|
|
4138
4759
|
};
|
|
4139
4760
|
|
|
4140
4761
|
// src/core/Pipeline.ts
|
|
@@ -4167,6 +4788,57 @@ var LRUEmbeddingCache = class {
|
|
|
4167
4788
|
return this.cache.size;
|
|
4168
4789
|
}
|
|
4169
4790
|
};
|
|
4791
|
+
function estimateTokens(text) {
|
|
4792
|
+
return Math.ceil(text.length / 4);
|
|
4793
|
+
}
|
|
4794
|
+
var MODEL_COST_PER_1K = {
|
|
4795
|
+
"gpt-4o": { input: 25e-4, output: 0.01 },
|
|
4796
|
+
"gpt-4o-mini": { input: 15e-5, output: 6e-4 },
|
|
4797
|
+
"gpt-4-turbo": { input: 0.01, output: 0.03 },
|
|
4798
|
+
"gpt-3.5-turbo": { input: 5e-4, output: 15e-4 },
|
|
4799
|
+
"claude-3-5-sonnet": { input: 3e-3, output: 0.015 },
|
|
4800
|
+
"claude-3-haiku": { input: 25e-5, output: 125e-5 },
|
|
4801
|
+
"gemini-1.5-flash": { input: 75e-6, output: 3e-4 },
|
|
4802
|
+
"gemini-1.5-pro": { input: 125e-5, output: 5e-3 }
|
|
4803
|
+
};
|
|
4804
|
+
function estimateCostUsd(promptTokens, completionTokens, model) {
|
|
4805
|
+
if (!model) return void 0;
|
|
4806
|
+
const key = Object.keys(MODEL_COST_PER_1K).find((k) => model.toLowerCase().includes(k));
|
|
4807
|
+
if (!key) return void 0;
|
|
4808
|
+
const prices = MODEL_COST_PER_1K[key];
|
|
4809
|
+
return promptTokens / 1e3 * prices.input + completionTokens / 1e3 * prices.output;
|
|
4810
|
+
}
|
|
4811
|
+
async function scoreHallucination(llm, answer, context) {
|
|
4812
|
+
const maxContextChars = 3e3;
|
|
4813
|
+
const truncatedContext = context.length > maxContextChars ? context.slice(0, maxContextChars) + "\n...[truncated]" : context;
|
|
4814
|
+
const prompt = `You are an AI quality checker. Given the CONTEXT retrieved from a knowledge base and the ANSWER generated from it, rate how well-grounded the answer is.
|
|
4815
|
+
|
|
4816
|
+
CONTEXT:
|
|
4817
|
+
${truncatedContext}
|
|
4818
|
+
|
|
4819
|
+
ANSWER:
|
|
4820
|
+
${answer}
|
|
4821
|
+
|
|
4822
|
+
Return ONLY a valid JSON object with no markdown fences:
|
|
4823
|
+
{"score": <float 0-1>, "reason": "<one sentence>"}
|
|
4824
|
+
|
|
4825
|
+
Where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.`;
|
|
4826
|
+
try {
|
|
4827
|
+
const raw = await llm.chat(
|
|
4828
|
+
[{ role: "user", content: prompt }],
|
|
4829
|
+
"",
|
|
4830
|
+
{ temperature: 0, maxTokens: 120 }
|
|
4831
|
+
);
|
|
4832
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
4833
|
+
if (!jsonMatch) return void 0;
|
|
4834
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4835
|
+
if (typeof parsed.score === "number" && typeof parsed.reason === "string") {
|
|
4836
|
+
return { score: Math.min(1, Math.max(0, parsed.score)), reason: parsed.reason };
|
|
4837
|
+
}
|
|
4838
|
+
} catch (e) {
|
|
4839
|
+
}
|
|
4840
|
+
return void 0;
|
|
4841
|
+
}
|
|
4170
4842
|
var Pipeline = class {
|
|
4171
4843
|
constructor(config) {
|
|
4172
4844
|
this.config = config;
|
|
@@ -4183,206 +4855,30 @@ var Pipeline = class {
|
|
|
4183
4855
|
}
|
|
4184
4856
|
this.reranker = new Reranker();
|
|
4185
4857
|
}
|
|
4858
|
+
/**
|
|
4859
|
+
* Expose the underlying LLM provider (set after initialize()).
|
|
4860
|
+
* Used by the stream handler to pass to UITransformer.analyzeAndDecide().
|
|
4861
|
+
*/
|
|
4862
|
+
getLLMProvider() {
|
|
4863
|
+
return this.initialised ? this.llmProvider : void 0;
|
|
4864
|
+
}
|
|
4186
4865
|
async initialize() {
|
|
4187
|
-
var _a
|
|
4866
|
+
var _a;
|
|
4188
4867
|
if (this.initialised) return;
|
|
4189
|
-
const
|
|
4190
|
-
const chartInstruction = `
|
|
4191
|
-
|
|
4192
|
-
${CHART_MARKER}
|
|
4193
|
-
### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
|
|
4194
|
-
|
|
4195
|
-
You are responsible for returning a SINGLE structured UI block when visualization is required.
|
|
4196
|
-
|
|
4197
|
-
---
|
|
4198
|
-
|
|
4199
|
-
## 1. INTENT CLASSIFICATION (MANDATORY)
|
|
4200
|
-
|
|
4201
|
-
Classify the query into ONE of the following intents:
|
|
4202
|
-
|
|
4203
|
-
- "explore" \u2192 user wants to browse items (e.g., "show me products")
|
|
4204
|
-
- "analyze" \u2192 user wants aggregation/distribution (e.g., "distribution of products")
|
|
4205
|
-
- "compare" \u2192 user wants side-by-side comparison
|
|
4206
|
-
- "detail" \u2192 user wants structured/tabular data
|
|
4207
|
-
|
|
4208
|
-
---
|
|
4209
|
-
|
|
4210
|
-
## 2. VIEW SELECTION (DYNAMIC)
|
|
4211
|
-
|
|
4212
|
-
Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
|
|
4213
|
-
- TRENDS/GROWTH \u2192 "chart" (type: "line")
|
|
4214
|
-
- DISTRIBUTIONS/RATIOS \u2192 "chart" (type: "pie")
|
|
4215
|
-
- COMPARISONS \u2192 "chart" (type: "bar")
|
|
4216
|
-
- PRODUCT LISTS \u2192 "carousel"
|
|
4217
|
-
- RAW DATA/SPECS \u2192 "table"
|
|
4218
|
-
|
|
4219
|
-
\u26A0\uFE0F Dynamic Overrides:
|
|
4220
|
-
- If query is "show products", ALWAYS use carousel.
|
|
4221
|
-
- If data is statistical, ALWAYS use a chart.
|
|
4222
|
-
- NEVER explain why you chose a view.
|
|
4223
|
-
|
|
4224
|
-
---
|
|
4225
|
-
|
|
4226
|
-
## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
|
|
4227
|
-
|
|
4228
|
-
\`\`\`ui
|
|
4229
|
-
{
|
|
4230
|
-
"view": "carousel" | "chart" | "table",
|
|
4231
|
-
"title": "string",
|
|
4232
|
-
"description": "string",
|
|
4233
|
-
"metadata": {
|
|
4234
|
-
"intent": "explore" | "analyze" | "compare" | "detail",
|
|
4235
|
-
"confidence": 0.0-1.0
|
|
4236
|
-
},
|
|
4237
|
-
|
|
4238
|
-
// CHART ONLY
|
|
4239
|
-
"chart": {
|
|
4240
|
-
"type": "pie" | "bar" | "line",
|
|
4241
|
-
"xKey": "label",
|
|
4242
|
-
"yKeys": ["value"],
|
|
4243
|
-
"data": []
|
|
4244
|
-
},
|
|
4245
|
-
|
|
4246
|
-
// TABLE ONLY
|
|
4247
|
-
"table": {
|
|
4248
|
-
"columns": [],
|
|
4249
|
-
"rows": []
|
|
4250
|
-
},
|
|
4251
|
-
|
|
4252
|
-
// CAROUSEL ONLY
|
|
4253
|
-
"carousel": {
|
|
4254
|
-
"items": []
|
|
4255
|
-
},
|
|
4256
|
-
|
|
4257
|
-
"insights": []
|
|
4258
|
-
}
|
|
4259
|
-
\`\`\`
|
|
4260
|
-
|
|
4261
|
-
---
|
|
4262
|
-
|
|
4263
|
-
## 4. DATA CONTRACT (CRITICAL)
|
|
4264
|
-
|
|
4265
|
-
### \u{1F539} CAROUSEL ITEM FORMAT
|
|
4266
|
-
{
|
|
4267
|
-
"id": "string",
|
|
4268
|
-
"name": "string",
|
|
4269
|
-
"brand": "string",
|
|
4270
|
-
"price": number,
|
|
4271
|
-
"image": "url",
|
|
4272
|
-
"link": "url",
|
|
4273
|
-
"inStock": boolean
|
|
4274
|
-
}
|
|
4275
|
-
|
|
4276
|
-
### \u{1F539} CHART DATA FORMAT
|
|
4277
|
-
{
|
|
4278
|
-
"label": "string",
|
|
4279
|
-
"value": number,
|
|
4280
|
-
"inStockCount": number (optional),
|
|
4281
|
-
"outOfStockCount": number (optional)
|
|
4282
|
-
}
|
|
4283
|
-
|
|
4284
|
-
### \u{1F539} TABLE FORMAT
|
|
4285
|
-
- columns MUST match row keys
|
|
4286
|
-
- rows MUST be flat objects
|
|
4287
|
-
|
|
4288
|
-
---
|
|
4289
|
-
|
|
4290
|
-
## 5. DATA RULES
|
|
4291
|
-
|
|
4292
|
-
- NEVER hallucinate fields or data.
|
|
4293
|
-
- ONLY use data provided in the retrieved context.
|
|
4294
|
-
- STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
|
|
4295
|
-
- IF NO DATA IS FOUND: You MUST NOT generate a chart. You MUST NOT say "This example assumes" or provide hypothetical numbers. Simply state "No relevant data found in the database." and STOP.
|
|
4296
|
-
- NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
|
|
4297
|
-
- NEVER explain YOUR formatting choice.
|
|
4298
|
-
- Aggregate BEFORE rendering chart.
|
|
4299
|
-
- DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
|
|
4300
|
-
|
|
4301
|
-
---
|
|
4302
|
-
|
|
4303
|
-
## 6. SMART RULES (IMPORTANT)
|
|
4304
|
-
|
|
4305
|
-
- If user asks BOTH:
|
|
4306
|
-
"show products AND distribution"
|
|
4307
|
-
\u2192 PRIORITIZE "explore" \u2192 carousel
|
|
4308
|
-
|
|
4309
|
-
- If dataset size > 20:
|
|
4310
|
-
\u2192 aggregate \u2192 chart or table
|
|
4311
|
-
|
|
4312
|
-
---
|
|
4313
|
-
|
|
4314
|
-
## 7. OUTPUT RULES
|
|
4315
|
-
|
|
4316
|
-
- ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block.
|
|
4317
|
-
- JSON must be VALID.
|
|
4318
|
-
- FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
|
|
4319
|
-
- NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
|
|
4320
|
-
- SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
|
|
4321
|
-
- TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
|
|
4322
|
-
|
|
4323
|
-
---
|
|
4324
|
-
|
|
4325
|
-
## 8. EXAMPLES
|
|
4326
|
-
|
|
4327
|
-
User: "show me beauty products"
|
|
4328
|
-
|
|
4329
|
-
\`\`\`ui
|
|
4330
|
-
{
|
|
4331
|
-
"view": "carousel",
|
|
4332
|
-
"title": "TITLE",
|
|
4333
|
-
"description": "DESCRIPTION",
|
|
4334
|
-
"metadata": {
|
|
4335
|
-
"intent": "explore",
|
|
4336
|
-
"confidence": 0.95
|
|
4337
|
-
},
|
|
4338
|
-
"carousel": {
|
|
4339
|
-
"items": [
|
|
4340
|
-
{
|
|
4341
|
-
"id": "ID",
|
|
4342
|
-
"name": "PRODUCT_NAME",
|
|
4343
|
-
"brand": "BRAND",
|
|
4344
|
-
"price": 0,
|
|
4345
|
-
"image": "URL",
|
|
4346
|
-
"link": "URL",
|
|
4347
|
-
"inStock": true
|
|
4348
|
-
}
|
|
4349
|
-
]
|
|
4350
|
-
},
|
|
4351
|
-
"insights": ["INSIGHT"]
|
|
4352
|
-
}
|
|
4353
|
-
\`\`\`
|
|
4354
|
-
|
|
4355
|
-
---
|
|
4356
|
-
|
|
4868
|
+
const chartInstruction = `You are a helpful product assistant. Use the provided context to answer questions accurately.
|
|
4357
4869
|
|
|
4870
|
+
### UI STYLE RULES (CRITICAL):
|
|
4871
|
+
- NEVER generate markdown tables. If you do, the UI will break.
|
|
4872
|
+
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
4873
|
+
- NEVER generate text-based charts or graphs.
|
|
4874
|
+
- ONLY use plain text and bullet points.
|
|
4358
4875
|
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
"description": "DESCRIPTION",
|
|
4364
|
-
"metadata": {
|
|
4365
|
-
"intent": "analyze",
|
|
4366
|
-
"confidence": 0.98
|
|
4367
|
-
},
|
|
4368
|
-
"chart": {
|
|
4369
|
-
"type": "pie",
|
|
4370
|
-
"data": [
|
|
4371
|
-
{ "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
|
|
4372
|
-
{ "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
|
|
4373
|
-
]
|
|
4374
|
-
},
|
|
4375
|
-
"insights": ["INSIGHT"]
|
|
4376
|
-
}
|
|
4377
|
-
\`\`\`
|
|
4876
|
+
### PRODUCT DISPLAY:
|
|
4877
|
+
- When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
|
|
4878
|
+
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
4879
|
+
- Do NOT try to format product lists as tables.
|
|
4378
4880
|
`;
|
|
4379
|
-
|
|
4380
|
-
let cleanPrompt = this.config.llm.systemPrompt || "";
|
|
4381
|
-
cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
|
|
4382
|
-
cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
|
|
4383
|
-
this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
|
|
4384
|
-
}
|
|
4385
|
-
console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
|
|
4881
|
+
this.config.llm.systemPrompt = chartInstruction;
|
|
4386
4882
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
4387
4883
|
const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
|
|
4388
4884
|
this.config.llm,
|
|
@@ -4396,7 +4892,7 @@ User: "show me beauty products"
|
|
|
4396
4892
|
this.entityExtractor = new EntityExtractor(this.llmProvider);
|
|
4397
4893
|
}
|
|
4398
4894
|
await this.vectorDB.initialize();
|
|
4399
|
-
if (((
|
|
4895
|
+
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic") {
|
|
4400
4896
|
this.agent = new LangChainAgent(this, this.config);
|
|
4401
4897
|
await this.agent.initialize(this.llmProvider);
|
|
4402
4898
|
}
|
|
@@ -4404,7 +4900,6 @@ User: "show me beauty products"
|
|
|
4404
4900
|
}
|
|
4405
4901
|
/**
|
|
4406
4902
|
* Ingest documents with automatic chunking, embedding, and batch upsert.
|
|
4407
|
-
* Handles retries for transient failures.
|
|
4408
4903
|
*/
|
|
4409
4904
|
async ingest(documents, namespace) {
|
|
4410
4905
|
await this.initialize();
|
|
@@ -4421,10 +4916,7 @@ User: "show me beauty products"
|
|
|
4421
4916
|
metadata: chunk.metadata
|
|
4422
4917
|
}));
|
|
4423
4918
|
const totalProcessed = await this.processUpserts(upsertDocs, ns);
|
|
4424
|
-
results.push({
|
|
4425
|
-
docId: doc.docId,
|
|
4426
|
-
chunksIngested: totalProcessed
|
|
4427
|
-
});
|
|
4919
|
+
results.push({ docId: doc.docId, chunksIngested: totalProcessed });
|
|
4428
4920
|
if (this.graphDB && this.entityExtractor) {
|
|
4429
4921
|
await this.processGraphIngestion(doc.docId, chunks);
|
|
4430
4922
|
}
|
|
@@ -4435,23 +4927,18 @@ User: "show me beauty products"
|
|
|
4435
4927
|
}
|
|
4436
4928
|
return results;
|
|
4437
4929
|
}
|
|
4438
|
-
/**
|
|
4439
|
-
* Step 1: Chunk the document content.
|
|
4440
|
-
*/
|
|
4930
|
+
/** Step 1: Chunk the document content. */
|
|
4441
4931
|
async prepareChunks(doc) {
|
|
4442
4932
|
var _a, _b;
|
|
4443
4933
|
if (this.llamaIngestor) {
|
|
4444
|
-
return
|
|
4934
|
+
return this.llamaIngestor.chunk(doc.content, {
|
|
4445
4935
|
docId: doc.docId,
|
|
4446
4936
|
metadata: doc.metadata,
|
|
4447
4937
|
chunkSize: (_a = this.config.rag) == null ? void 0 : _a.chunkSize,
|
|
4448
4938
|
chunkOverlap: (_b = this.config.rag) == null ? void 0 : _b.chunkOverlap
|
|
4449
4939
|
});
|
|
4450
4940
|
}
|
|
4451
|
-
return this.chunker.chunk(doc.content, {
|
|
4452
|
-
docId: doc.docId,
|
|
4453
|
-
metadata: doc.metadata
|
|
4454
|
-
});
|
|
4941
|
+
return this.chunker.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata });
|
|
4455
4942
|
}
|
|
4456
4943
|
/**
|
|
4457
4944
|
* Step 2: Generate embeddings for chunks with retry logic.
|
|
@@ -4475,9 +4962,7 @@ User: "show me beauty products"
|
|
|
4475
4962
|
}
|
|
4476
4963
|
return vectors;
|
|
4477
4964
|
}
|
|
4478
|
-
/**
|
|
4479
|
-
* Step 3: Upsert chunks to vector database with retry logic.
|
|
4480
|
-
*/
|
|
4965
|
+
/** Step 3: Upsert chunks to vector database with retry logic. */
|
|
4481
4966
|
async processUpserts(upsertDocs, namespace) {
|
|
4482
4967
|
const upsertBatchOptions = {
|
|
4483
4968
|
batchSize: 100,
|
|
@@ -4494,14 +4979,10 @@ User: "show me beauty products"
|
|
|
4494
4979
|
}
|
|
4495
4980
|
return upsertResult.totalProcessed;
|
|
4496
4981
|
}
|
|
4497
|
-
/**
|
|
4498
|
-
* Step 4: Optional graph-based entity extraction and ingestion.
|
|
4499
|
-
*/
|
|
4982
|
+
/** Step 4: Optional graph-based entity extraction and ingestion. */
|
|
4500
4983
|
async processGraphIngestion(docId, chunks) {
|
|
4501
|
-
console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
|
|
4502
4984
|
const extractionOptions = {
|
|
4503
4985
|
batchSize: 2,
|
|
4504
|
-
// Low concurrency for LLM extraction
|
|
4505
4986
|
maxRetries: 1,
|
|
4506
4987
|
initialDelayMs: 500
|
|
4507
4988
|
};
|
|
@@ -4514,7 +4995,7 @@ User: "show me beauty products"
|
|
|
4514
4995
|
if (nodes.length > 0) await this.graphDB.addNodes(nodes);
|
|
4515
4996
|
if (edges.length > 0) await this.graphDB.addEdges(edges);
|
|
4516
4997
|
} catch (err) {
|
|
4517
|
-
console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
|
|
4998
|
+
console.warn(`[Pipeline] Entity extraction failed for chunk in doc ${docId}:`, err);
|
|
4518
4999
|
}
|
|
4519
5000
|
}
|
|
4520
5001
|
},
|
|
@@ -4525,7 +5006,6 @@ User: "show me beauty products"
|
|
|
4525
5006
|
var _a;
|
|
4526
5007
|
await this.initialize();
|
|
4527
5008
|
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic" && this.agent) {
|
|
4528
|
-
console.log("[Pipeline] \u{1F916} Executing in Agentic Mode...");
|
|
4529
5009
|
const agentReply = await this.agent.run(question, history);
|
|
4530
5010
|
return { reply: agentReply, sources: [] };
|
|
4531
5011
|
}
|
|
@@ -4534,6 +5014,7 @@ User: "show me beauty products"
|
|
|
4534
5014
|
let sources = [];
|
|
4535
5015
|
let graphData;
|
|
4536
5016
|
let uiTransformation;
|
|
5017
|
+
let trace;
|
|
4537
5018
|
try {
|
|
4538
5019
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
4539
5020
|
const chunk = temp.value;
|
|
@@ -4543,6 +5024,7 @@ User: "show me beauty products"
|
|
|
4543
5024
|
if ("sources" in chunk) sources = chunk.sources;
|
|
4544
5025
|
if ("graphData" in chunk) graphData = chunk.graphData;
|
|
4545
5026
|
if ("ui_transformation" in chunk) uiTransformation = chunk.ui_transformation;
|
|
5027
|
+
if ("trace" in chunk) trace = chunk.trace;
|
|
4546
5028
|
}
|
|
4547
5029
|
}
|
|
4548
5030
|
} catch (temp) {
|
|
@@ -4555,38 +5037,47 @@ User: "show me beauty products"
|
|
|
4555
5037
|
throw error[0];
|
|
4556
5038
|
}
|
|
4557
5039
|
}
|
|
4558
|
-
return { reply, sources, graphData, ui_transformation: uiTransformation };
|
|
5040
|
+
return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
|
|
4559
5041
|
}
|
|
4560
5042
|
/**
|
|
4561
5043
|
* High-performance streaming RAG flow.
|
|
4562
|
-
* Yields text chunks first, then the retrieval metadata at the end.
|
|
5044
|
+
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
4563
5045
|
*/
|
|
4564
5046
|
askStream(_0) {
|
|
4565
5047
|
return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
|
|
4566
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
5048
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
4567
5049
|
yield new __await(this.initialize());
|
|
4568
5050
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
4569
5051
|
const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
|
|
4570
5052
|
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
5053
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5054
|
+
const requestStart = performance.now();
|
|
4571
5055
|
try {
|
|
4572
5056
|
let searchQuery = question;
|
|
5057
|
+
let rewrittenQuery;
|
|
4573
5058
|
if ((_e = this.config.rag) == null ? void 0 : _e.useQueryTransformation) {
|
|
4574
5059
|
searchQuery = yield new __await(this.rewriteQuery(question, history));
|
|
5060
|
+
rewrittenQuery = searchQuery !== question ? searchQuery : void 0;
|
|
4575
5061
|
}
|
|
4576
5062
|
const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
|
|
4577
5063
|
const filter = QueryProcessor.buildQueryFilter(question, hints);
|
|
4578
|
-
|
|
5064
|
+
const embedStart = performance.now();
|
|
4579
5065
|
const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
|
|
4580
5066
|
namespace: ns,
|
|
4581
5067
|
topK: topK * 2,
|
|
4582
5068
|
filter
|
|
4583
5069
|
}));
|
|
5070
|
+
const retrieveEnd = performance.now();
|
|
5071
|
+
const embedMs = retrieveEnd - embedStart;
|
|
5072
|
+
const retrieveMs = retrieveEnd - embedStart;
|
|
5073
|
+
const rerankStart = performance.now();
|
|
4584
5074
|
let sources = rawSources.filter((m) => m.score >= scoreThreshold);
|
|
4585
5075
|
if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
|
|
4586
5076
|
sources = yield new __await(this.reranker.rerank(sources, question, topK));
|
|
4587
5077
|
} else {
|
|
4588
5078
|
sources = sources.slice(0, topK);
|
|
4589
5079
|
}
|
|
5080
|
+
const rerankMs = performance.now() - rerankStart;
|
|
4590
5081
|
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
4591
5082
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
4592
5083
|
if (graphData && graphData.nodes.length > 0) {
|
|
@@ -4599,7 +5090,16 @@ ${graphContext}
|
|
|
4599
5090
|
VECTOR CONTEXT:
|
|
4600
5091
|
${context}`;
|
|
4601
5092
|
}
|
|
4602
|
-
const
|
|
5093
|
+
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
5094
|
+
const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
|
|
5095
|
+
const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
|
|
5096
|
+
const hardenedHistory = [...history];
|
|
5097
|
+
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
5098
|
+
const messages = [...hardenedHistory, userQuestion];
|
|
5099
|
+
const systemPrompt = (_h = this.config.llm.systemPrompt) != null ? _h : "";
|
|
5100
|
+
const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
5101
|
+
let fullReply = "";
|
|
5102
|
+
const generateStart = performance.now();
|
|
4603
5103
|
if (this.llmProvider.chatStream) {
|
|
4604
5104
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
4605
5105
|
if (!stream) {
|
|
@@ -4608,6 +5108,7 @@ ${context}`;
|
|
|
4608
5108
|
try {
|
|
4609
5109
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
4610
5110
|
const chunk = temp.value;
|
|
5111
|
+
fullReply += chunk;
|
|
4611
5112
|
yield chunk;
|
|
4612
5113
|
}
|
|
4613
5114
|
} catch (temp) {
|
|
@@ -4622,14 +5123,58 @@ ${context}`;
|
|
|
4622
5123
|
}
|
|
4623
5124
|
} else {
|
|
4624
5125
|
const reply = yield new __await(this.llmProvider.chat(messages, context));
|
|
5126
|
+
fullReply = reply;
|
|
4625
5127
|
yield reply;
|
|
4626
5128
|
}
|
|
4627
|
-
const
|
|
5129
|
+
const generateMs = performance.now() - generateStart;
|
|
5130
|
+
const totalMs = performance.now() - requestStart;
|
|
5131
|
+
const latency = {
|
|
5132
|
+
embedMs: Math.round(embedMs),
|
|
5133
|
+
retrieveMs: Math.round(retrieveMs),
|
|
5134
|
+
rerankMs: ((_i = this.config.rag) == null ? void 0 : _i.useReranking) ? Math.round(rerankMs) : void 0,
|
|
5135
|
+
generateMs: Math.round(generateMs),
|
|
5136
|
+
totalMs: Math.round(totalMs)
|
|
5137
|
+
};
|
|
5138
|
+
const promptText = systemPrompt + "\n" + context + "\n" + userPrompt;
|
|
5139
|
+
const promptTokens = estimateTokens(promptText);
|
|
5140
|
+
const completionTokens = estimateTokens(fullReply);
|
|
5141
|
+
const tokens = {
|
|
5142
|
+
promptTokens,
|
|
5143
|
+
completionTokens,
|
|
5144
|
+
totalTokens: promptTokens + completionTokens,
|
|
5145
|
+
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
|
|
5146
|
+
};
|
|
5147
|
+
const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
|
|
5148
|
+
const trainedSchema = yield new __await(trainingPromise);
|
|
5149
|
+
const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, context, trainedSchema));
|
|
5150
|
+
const trace = {
|
|
5151
|
+
requestId,
|
|
5152
|
+
query: question,
|
|
5153
|
+
rewrittenQuery,
|
|
5154
|
+
systemPrompt,
|
|
5155
|
+
userPrompt: question + restrictionSuffix,
|
|
5156
|
+
chunks: sources.map((s) => {
|
|
5157
|
+
var _a2;
|
|
5158
|
+
return {
|
|
5159
|
+
id: s.id,
|
|
5160
|
+
score: s.score,
|
|
5161
|
+
content: s.content,
|
|
5162
|
+
metadata: (_a2 = s.metadata) != null ? _a2 : {},
|
|
5163
|
+
namespace: ns
|
|
5164
|
+
};
|
|
5165
|
+
}),
|
|
5166
|
+
latency,
|
|
5167
|
+
tokens,
|
|
5168
|
+
hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
|
|
5169
|
+
hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
|
|
5170
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5171
|
+
};
|
|
4628
5172
|
yield {
|
|
4629
5173
|
reply: "",
|
|
4630
5174
|
sources,
|
|
4631
5175
|
graphData,
|
|
4632
|
-
ui_transformation: uiTransformation
|
|
5176
|
+
ui_transformation: uiTransformation,
|
|
5177
|
+
trace
|
|
4633
5178
|
};
|
|
4634
5179
|
} catch (error2) {
|
|
4635
5180
|
throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
@@ -4640,6 +5185,17 @@ ${context}`;
|
|
|
4640
5185
|
* Universal retrieval method combining all enabled providers.
|
|
4641
5186
|
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
4642
5187
|
*/
|
|
5188
|
+
async generateUiTransformation(question, sources, _context, trainedSchema) {
|
|
5189
|
+
if (!sources || sources.length === 0) {
|
|
5190
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5191
|
+
}
|
|
5192
|
+
try {
|
|
5193
|
+
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
5194
|
+
} catch (err) {
|
|
5195
|
+
console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
|
|
5196
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
4643
5199
|
async retrieve(query, options) {
|
|
4644
5200
|
var _a, _b, _c;
|
|
4645
5201
|
const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
|
|
@@ -4657,12 +5213,9 @@ ${context}`;
|
|
|
4657
5213
|
const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
|
|
4658
5214
|
return { sources, graphData };
|
|
4659
5215
|
}
|
|
4660
|
-
/**
|
|
4661
|
-
* Rewrite the user query for better retrieval performance.
|
|
4662
|
-
*/
|
|
5216
|
+
/** Rewrite the user query for better retrieval performance. */
|
|
4663
5217
|
async rewriteQuery(question, history) {
|
|
4664
|
-
const prompt = `
|
|
4665
|
-
Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
5218
|
+
const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
4666
5219
|
Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
|
|
4667
5220
|
|
|
4668
5221
|
History:
|
|
@@ -4680,23 +5233,16 @@ Optimized Search Query:`;
|
|
|
4680
5233
|
);
|
|
4681
5234
|
return rewrite.trim() || question;
|
|
4682
5235
|
}
|
|
4683
|
-
/**
|
|
4684
|
-
* Generate 3-5 short, relevant questions based on the vector database content.
|
|
4685
|
-
*/
|
|
5236
|
+
/** Generate 3-5 short, relevant questions based on the vector database content. */
|
|
4686
5237
|
async getSuggestions(query, namespace) {
|
|
4687
|
-
if (!query || query.trim().length < 3)
|
|
4688
|
-
return [];
|
|
4689
|
-
}
|
|
5238
|
+
if (!query || query.trim().length < 3) return [];
|
|
4690
5239
|
await this.initialize();
|
|
4691
5240
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
4692
5241
|
try {
|
|
4693
5242
|
const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
|
|
4694
|
-
if (sources.length === 0)
|
|
4695
|
-
return [];
|
|
4696
|
-
}
|
|
5243
|
+
if (sources.length === 0) return [];
|
|
4697
5244
|
const context = sources.map((s) => s.content).join("\n\n---\n\n");
|
|
4698
|
-
const prompt = `
|
|
4699
|
-
Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
5245
|
+
const prompt = `Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
4700
5246
|
Focus on questions that can be answered by the context.
|
|
4701
5247
|
Keep each question under 10 words and make them very specific to the content.
|
|
4702
5248
|
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
@@ -4842,6 +5388,14 @@ var VectorPlugin = class {
|
|
|
4842
5388
|
getConfig() {
|
|
4843
5389
|
return this.config;
|
|
4844
5390
|
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Get the initialized LLM provider (available after the first request).
|
|
5393
|
+
* Used by handlers to pass to UITransformer.analyzeAndDecide() for
|
|
5394
|
+
* LLM-driven visualization decisions.
|
|
5395
|
+
*/
|
|
5396
|
+
getLLMProvider() {
|
|
5397
|
+
return this.pipeline.getLLMProvider();
|
|
5398
|
+
}
|
|
4845
5399
|
/**
|
|
4846
5400
|
* Perform pre-flight health checks on all configured providers.
|
|
4847
5401
|
* Useful to verify connectivity before running operations.
|
|
@@ -4963,7 +5517,12 @@ function sseMetaFrame(meta) {
|
|
|
4963
5517
|
`;
|
|
4964
5518
|
}
|
|
4965
5519
|
function sseUIFrame(uiTransformation) {
|
|
4966
|
-
return `data: ${JSON.stringify(
|
|
5520
|
+
return `data: ${JSON.stringify({ type: "ui_transformation", data: uiTransformation })}
|
|
5521
|
+
|
|
5522
|
+
`;
|
|
5523
|
+
}
|
|
5524
|
+
function sseObservabilityFrame(trace) {
|
|
5525
|
+
return `data: ${JSON.stringify({ type: "observability", data: trace })}
|
|
4967
5526
|
|
|
4968
5527
|
`;
|
|
4969
5528
|
}
|
|
@@ -5018,6 +5577,7 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5018
5577
|
const encoder = new TextEncoder();
|
|
5019
5578
|
const stream = new ReadableStream({
|
|
5020
5579
|
async start(controller) {
|
|
5580
|
+
var _a, _b;
|
|
5021
5581
|
const enqueue = (text) => controller.enqueue(encoder.encode(text));
|
|
5022
5582
|
try {
|
|
5023
5583
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
@@ -5028,13 +5588,25 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5028
5588
|
enqueue(sseTextFrame(chunk));
|
|
5029
5589
|
} else {
|
|
5030
5590
|
enqueue(sseMetaFrame(chunk));
|
|
5031
|
-
const
|
|
5032
|
-
|
|
5591
|
+
const responseChunk = chunk;
|
|
5592
|
+
const sources = (responseChunk == null ? void 0 : responseChunk.sources) || [];
|
|
5593
|
+
if (responseChunk == null ? void 0 : responseChunk.trace) {
|
|
5594
|
+
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
5595
|
+
}
|
|
5596
|
+
if (sources.length > 0) {
|
|
5033
5597
|
try {
|
|
5034
|
-
const
|
|
5035
|
-
|
|
5598
|
+
const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
|
|
5599
|
+
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());
|
|
5600
|
+
if (uiTransformation) {
|
|
5601
|
+
enqueue(sseUIFrame(uiTransformation));
|
|
5602
|
+
}
|
|
5036
5603
|
} catch (transformError) {
|
|
5037
5604
|
console.warn("[createStreamHandler] UI transformation warning:", transformError);
|
|
5605
|
+
try {
|
|
5606
|
+
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
5607
|
+
if (fallback) enqueue(sseUIFrame(fallback));
|
|
5608
|
+
} catch (e) {
|
|
5609
|
+
}
|
|
5038
5610
|
}
|
|
5039
5611
|
}
|
|
5040
5612
|
}
|
|
@@ -5102,24 +5674,64 @@ function createUploadHandler(configOrPlugin) {
|
|
|
5102
5674
|
const formData = await req.formData();
|
|
5103
5675
|
const files = formData.getAll("files");
|
|
5104
5676
|
const namespace = formData.get("namespace") || void 0;
|
|
5677
|
+
const dimensionRaw = formData.get("dimension");
|
|
5678
|
+
const dimension = dimensionRaw ? parseInt(dimensionRaw, 10) : void 0;
|
|
5105
5679
|
if (!files || files.length === 0) {
|
|
5106
5680
|
return import_server.NextResponse.json({ error: "No files provided" }, { status: 400 });
|
|
5107
5681
|
}
|
|
5108
|
-
const documents =
|
|
5109
|
-
|
|
5682
|
+
const documents = [];
|
|
5683
|
+
for (const file of files) {
|
|
5684
|
+
if (file.name.toLowerCase().endsWith(".csv") || file.type === "text/csv") {
|
|
5685
|
+
const text = await file.text();
|
|
5686
|
+
const Papa = await import("papaparse");
|
|
5687
|
+
const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
5688
|
+
if (parsed.data && parsed.data.length > 0) {
|
|
5689
|
+
let i = 0;
|
|
5690
|
+
let lastRowData = null;
|
|
5691
|
+
for (const row of parsed.data) {
|
|
5692
|
+
i++;
|
|
5693
|
+
const rowData = row;
|
|
5694
|
+
const groupingKey = Object.keys(rowData)[0];
|
|
5695
|
+
if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
|
|
5696
|
+
for (const key of Object.keys(rowData)) {
|
|
5697
|
+
if (!rowData[key] && lastRowData[key]) {
|
|
5698
|
+
rowData[key] = lastRowData[key];
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
lastRowData = __spreadValues({}, rowData);
|
|
5703
|
+
const contentParts = [];
|
|
5704
|
+
for (const [key, val] of Object.entries(rowData)) {
|
|
5705
|
+
if (key && val) {
|
|
5706
|
+
contentParts.push(`${key}: ${val}`);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
documents.push({
|
|
5710
|
+
docId: `${file.name}-row-${i}`,
|
|
5711
|
+
content: contentParts.join(", "),
|
|
5712
|
+
metadata: __spreadValues(__spreadValues({
|
|
5713
|
+
fileName: file.name,
|
|
5714
|
+
fileSize: file.size,
|
|
5715
|
+
fileType: file.type,
|
|
5716
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5717
|
+
}, dimension ? { dimension } : {}), rowData)
|
|
5718
|
+
});
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
} else {
|
|
5110
5722
|
const content = await DocumentParser.parse(file, file.name, file.type);
|
|
5111
|
-
|
|
5723
|
+
documents.push({
|
|
5112
5724
|
docId: file.name,
|
|
5113
5725
|
content,
|
|
5114
|
-
metadata: {
|
|
5726
|
+
metadata: __spreadValues({
|
|
5115
5727
|
fileName: file.name,
|
|
5116
5728
|
fileSize: file.size,
|
|
5117
5729
|
fileType: file.type,
|
|
5118
5730
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5119
|
-
}
|
|
5120
|
-
};
|
|
5121
|
-
}
|
|
5122
|
-
|
|
5731
|
+
}, dimension ? { dimension } : {})
|
|
5732
|
+
});
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5123
5735
|
const results = await plugin.ingest(documents, namespace);
|
|
5124
5736
|
return import_server.NextResponse.json({ message: "Upload successful", results });
|
|
5125
5737
|
} catch (err) {
|
|
@@ -5156,6 +5768,7 @@ function createSuggestionsHandler(configOrPlugin) {
|
|
|
5156
5768
|
sseErrorFrame,
|
|
5157
5769
|
sseFrame,
|
|
5158
5770
|
sseMetaFrame,
|
|
5771
|
+
sseObservabilityFrame,
|
|
5159
5772
|
sseTextFrame,
|
|
5160
5773
|
sseUIFrame
|
|
5161
5774
|
});
|