@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/server.js
CHANGED
|
@@ -334,81 +334,31 @@ var init_PineconeProvider = __esm({
|
|
|
334
334
|
}
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
-
// src/providers/vectordb/
|
|
338
|
-
var
|
|
339
|
-
__export(
|
|
340
|
-
|
|
337
|
+
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
338
|
+
var MultiTablePostgresProvider_exports = {};
|
|
339
|
+
__export(MultiTablePostgresProvider_exports, {
|
|
340
|
+
MultiTablePostgresProvider: () => MultiTablePostgresProvider
|
|
341
341
|
});
|
|
342
|
-
var import_pg,
|
|
343
|
-
var
|
|
344
|
-
"src/providers/vectordb/
|
|
342
|
+
var import_pg, MultiTablePostgresProvider;
|
|
343
|
+
var init_MultiTablePostgresProvider = __esm({
|
|
344
|
+
"src/providers/vectordb/MultiTablePostgresProvider.ts"() {
|
|
345
345
|
"use strict";
|
|
346
346
|
import_pg = require("pg");
|
|
347
347
|
init_BaseVectorProvider();
|
|
348
|
-
|
|
348
|
+
MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
349
349
|
constructor(config) {
|
|
350
|
-
var _a;
|
|
350
|
+
var _a, _b, _c;
|
|
351
351
|
super(config);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
const opts = config.options || {};
|
|
353
|
+
if (!opts.connectionString) {
|
|
354
|
+
throw new Error("[MultiTablePostgresProvider] options.connectionString is required");
|
|
355
|
+
}
|
|
355
356
|
this.connectionString = opts.connectionString;
|
|
356
|
-
this.dimensions = (_a = opts.dimensions) != null ? _a :
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const errors = [];
|
|
362
|
-
const opts = config.options || {};
|
|
363
|
-
if (!opts.connectionString) {
|
|
364
|
-
errors.push({
|
|
365
|
-
field: "vectorDb.options.connectionString",
|
|
366
|
-
message: "PostgreSQL connection string is required",
|
|
367
|
-
suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
|
|
368
|
-
severity: "error"
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
|
|
372
|
-
errors.push({
|
|
373
|
-
field: "vectorDb.options.tables",
|
|
374
|
-
message: "PostgreSQL tables must be a string or a string array",
|
|
375
|
-
severity: "error"
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
return errors;
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
static getHealthChecker() {
|
|
383
|
-
return {
|
|
384
|
-
async check(config) {
|
|
385
|
-
const opts = config.options || {};
|
|
386
|
-
const timestamp = Date.now();
|
|
387
|
-
try {
|
|
388
|
-
const { Client } = await import("pg");
|
|
389
|
-
const client = new Client({ connectionString: opts.connectionString });
|
|
390
|
-
await client.connect();
|
|
391
|
-
const result = await client.query(`
|
|
392
|
-
SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
|
|
393
|
-
`);
|
|
394
|
-
const hasVector = result.rows[0].exists;
|
|
395
|
-
await client.end();
|
|
396
|
-
return {
|
|
397
|
-
healthy: true,
|
|
398
|
-
provider: "postgresql",
|
|
399
|
-
capabilities: { pgvectorInstalled: hasVector },
|
|
400
|
-
timestamp
|
|
401
|
-
};
|
|
402
|
-
} catch (error) {
|
|
403
|
-
return {
|
|
404
|
-
healthy: false,
|
|
405
|
-
provider: "postgresql",
|
|
406
|
-
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
407
|
-
timestamp
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
};
|
|
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";
|
|
412
362
|
}
|
|
413
363
|
async initialize() {
|
|
414
364
|
this.pool = new import_pg.Pool({ connectionString: this.connectionString });
|
|
@@ -416,7 +366,7 @@ var init_PostgreSQLProvider = __esm({
|
|
|
416
366
|
try {
|
|
417
367
|
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
418
368
|
await client.query(`
|
|
419
|
-
CREATE TABLE IF NOT EXISTS ${this.
|
|
369
|
+
CREATE TABLE IF NOT EXISTS ${this.uploadTable} (
|
|
420
370
|
id TEXT PRIMARY KEY,
|
|
421
371
|
namespace TEXT NOT NULL DEFAULT '',
|
|
422
372
|
content TEXT NOT NULL,
|
|
@@ -425,53 +375,115 @@ var init_PostgreSQLProvider = __esm({
|
|
|
425
375
|
)
|
|
426
376
|
`);
|
|
427
377
|
await client.query(`
|
|
428
|
-
CREATE INDEX IF NOT EXISTS ${this.
|
|
429
|
-
ON ${this.
|
|
378
|
+
CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
|
|
379
|
+
ON ${this.uploadTable}
|
|
430
380
|
USING hnsw (embedding vector_cosine_ops)
|
|
431
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
|
+
}
|
|
432
396
|
} finally {
|
|
433
397
|
client.release();
|
|
434
398
|
}
|
|
435
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Upsert a document by dynamically provisioning a table and columns.
|
|
402
|
+
*/
|
|
436
403
|
async upsert(doc, namespace = "") {
|
|
437
|
-
|
|
438
|
-
const vectorLiteral = `[${doc.vector.join(",")}]`;
|
|
439
|
-
await this.pool.query(
|
|
440
|
-
`INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
441
|
-
VALUES ($1, $2, $3, $4, $5::vector)
|
|
442
|
-
ON CONFLICT (id) DO UPDATE
|
|
443
|
-
SET namespace = EXCLUDED.namespace,
|
|
444
|
-
content = EXCLUDED.content,
|
|
445
|
-
metadata = EXCLUDED.metadata,
|
|
446
|
-
embedding = EXCLUDED.embedding`,
|
|
447
|
-
[doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
|
|
448
|
-
);
|
|
404
|
+
await this.batchUpsert([doc], namespace);
|
|
449
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* Batch upsert documents by dynamically provisioning tables and columns based on CSV headers.
|
|
408
|
+
*/
|
|
450
409
|
async batchUpsert(docs, namespace = "") {
|
|
410
|
+
var _a;
|
|
451
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
|
+
}
|
|
452
418
|
const client = await this.pool.connect();
|
|
453
419
|
try {
|
|
454
420
|
await client.query("BEGIN");
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}).join("
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
+
)
|
|
473
440
|
`;
|
|
474
|
-
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
|
+
}
|
|
475
487
|
}
|
|
476
488
|
await client.query("COMMIT");
|
|
477
489
|
} catch (error) {
|
|
@@ -481,49 +493,119 @@ var init_PostgreSQLProvider = __esm({
|
|
|
481
493
|
client.release();
|
|
482
494
|
}
|
|
483
495
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
|
|
492
|
-
const paramIdx = params.length + 1;
|
|
493
|
-
params.push(JSON.stringify(val));
|
|
494
|
-
return `metadata->>'${key}' = $${paramIdx}`;
|
|
495
|
-
}).join(" AND ");
|
|
496
|
-
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.");
|
|
497
503
|
}
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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);
|
|
518
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));
|
|
519
603
|
}
|
|
520
|
-
async delete(
|
|
521
|
-
|
|
522
|
-
const params = namespace ? [id, namespace] : [id];
|
|
523
|
-
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.");
|
|
524
606
|
}
|
|
525
|
-
async deleteNamespace(
|
|
526
|
-
|
|
607
|
+
async deleteNamespace(_namespace) {
|
|
608
|
+
console.warn("[MultiTablePostgresProvider] deleteNamespace() is a no-op for multi-table mode.");
|
|
527
609
|
}
|
|
528
610
|
async ping() {
|
|
529
611
|
try {
|
|
@@ -534,7 +616,9 @@ var init_PostgreSQLProvider = __esm({
|
|
|
534
616
|
}
|
|
535
617
|
}
|
|
536
618
|
async disconnect() {
|
|
537
|
-
|
|
619
|
+
if (this.pool) {
|
|
620
|
+
await this.pool.end();
|
|
621
|
+
}
|
|
538
622
|
}
|
|
539
623
|
};
|
|
540
624
|
}
|
|
@@ -1736,7 +1820,7 @@ function getRagConfig(baseConfig, env = process.env) {
|
|
|
1736
1820
|
return getEnvConfig(env, baseConfig);
|
|
1737
1821
|
}
|
|
1738
1822
|
function getEnvConfig(env = process.env, base) {
|
|
1739
|
-
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;
|
|
1823
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va, _wa, _xa, _ya, _za, _Aa, _Ba, _Ca, _Da, _Ea, _Fa;
|
|
1740
1824
|
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__";
|
|
1741
1825
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
1742
1826
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -1747,33 +1831,35 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1747
1831
|
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 : "";
|
|
1748
1832
|
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;
|
|
1749
1833
|
} else if (vectorProvider === "pgvector" || vectorProvider === "postgresql") {
|
|
1750
|
-
vectorDbOptions.connectionString = (_p = (
|
|
1834
|
+
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 : "";
|
|
1835
|
+
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;
|
|
1836
|
+
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;
|
|
1751
1837
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1752
1838
|
} else if (vectorProvider === "mongodb") {
|
|
1753
|
-
vectorDbOptions.uri = (
|
|
1754
|
-
vectorDbOptions.database = (
|
|
1755
|
-
vectorDbOptions.collection = (
|
|
1756
|
-
vectorDbOptions.indexName = (
|
|
1839
|
+
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 : "";
|
|
1840
|
+
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 : "";
|
|
1841
|
+
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 : "";
|
|
1842
|
+
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";
|
|
1757
1843
|
} else if (vectorProvider === "qdrant") {
|
|
1758
|
-
vectorDbOptions.baseUrl = (
|
|
1759
|
-
vectorDbOptions.apiKey = (
|
|
1844
|
+
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";
|
|
1845
|
+
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;
|
|
1760
1846
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1761
1847
|
} else if (vectorProvider === "milvus") {
|
|
1762
|
-
vectorDbOptions.baseUrl = (
|
|
1848
|
+
vectorDbOptions.baseUrl = (_Z = readString(env, "MILVUS_URL")) != null ? _Z : "http://localhost:19530";
|
|
1763
1849
|
vectorDbOptions.apiKey = readString(env, "MILVUS_API_KEY");
|
|
1764
1850
|
} else if (vectorProvider === "chromadb") {
|
|
1765
|
-
vectorDbOptions.baseUrl = (
|
|
1851
|
+
vectorDbOptions.baseUrl = (__ = readString(env, "CHROMADB_URL")) != null ? __ : "http://localhost:8000";
|
|
1766
1852
|
} else if (vectorProvider === "weaviate") {
|
|
1767
|
-
vectorDbOptions.baseUrl = (
|
|
1853
|
+
vectorDbOptions.baseUrl = (_$ = readString(env, "WEAVIATE_URL")) != null ? _$ : "http://localhost:8080";
|
|
1768
1854
|
vectorDbOptions.apiKey = readString(env, "WEAVIATE_API_KEY");
|
|
1769
1855
|
} else if (vectorProvider === "redis") {
|
|
1770
|
-
vectorDbOptions.baseUrl = (
|
|
1856
|
+
vectorDbOptions.baseUrl = (_aa = readString(env, "REDIS_URL")) != null ? _aa : "";
|
|
1771
1857
|
vectorDbOptions.apiKey = readString(env, "REDIS_API_KEY");
|
|
1772
1858
|
} else if (vectorProvider === "rest") {
|
|
1773
|
-
vectorDbOptions.baseUrl = (
|
|
1859
|
+
vectorDbOptions.baseUrl = (_ba = readString(env, "VECTOR_DB_REST_URL")) != null ? _ba : "";
|
|
1774
1860
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { "api-key": readString(env, "VECTOR_DB_REST_API_KEY") } : {};
|
|
1775
1861
|
} else if (vectorProvider === "universal_rest") {
|
|
1776
|
-
vectorDbOptions.baseUrl = (
|
|
1862
|
+
vectorDbOptions.baseUrl = (_da = (_ca = readString(env, "VECTOR_BASE_URL")) != null ? _ca : readString(env, "VECTOR_DB_REST_URL")) != null ? _da : "";
|
|
1777
1863
|
vectorDbOptions.profile = readString(env, "VECTOR_UNIVERSAL_PROFILE");
|
|
1778
1864
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { Authorization: `Bearer ${readString(env, "VECTOR_DB_REST_API_KEY")}` } : {};
|
|
1779
1865
|
}
|
|
@@ -1790,20 +1876,20 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1790
1876
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
1791
1877
|
gemini: readString(env, "GEMINI_API_KEY"),
|
|
1792
1878
|
ollama: void 0,
|
|
1793
|
-
universal_rest: (
|
|
1794
|
-
custom: (
|
|
1879
|
+
universal_rest: (_ea = readString(env, "EMBEDDING_API_KEY")) != null ? _ea : readString(env, "OPENAI_API_KEY"),
|
|
1880
|
+
custom: (_fa = readString(env, "EMBEDDING_API_KEY")) != null ? _fa : readString(env, "OPENAI_API_KEY")
|
|
1795
1881
|
};
|
|
1796
1882
|
return {
|
|
1797
1883
|
projectId,
|
|
1798
1884
|
vectorDb: {
|
|
1799
1885
|
provider: vectorProvider,
|
|
1800
|
-
indexName: (
|
|
1886
|
+
indexName: (_ha = (_ga = readString(env, "VECTOR_DB_INDEX")) != null ? _ga : vectorDbOptions.indexName) != null ? _ha : "rag-index",
|
|
1801
1887
|
options: vectorDbOptions
|
|
1802
1888
|
},
|
|
1803
1889
|
llm: {
|
|
1804
1890
|
provider: llmProvider,
|
|
1805
|
-
model: (
|
|
1806
|
-
apiKey: (
|
|
1891
|
+
model: (_ia = readString(env, "LLM_MODEL")) != null ? _ia : "gpt-4o",
|
|
1892
|
+
apiKey: (_ja = llmApiKeyByProvider[llmProvider]) != null ? _ja : "",
|
|
1807
1893
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
1808
1894
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
1809
1895
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 4096),
|
|
@@ -1814,7 +1900,7 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1814
1900
|
},
|
|
1815
1901
|
embedding: {
|
|
1816
1902
|
provider: embeddingProvider,
|
|
1817
|
-
model: (
|
|
1903
|
+
model: (_ka = readString(env, "EMBEDDING_MODEL")) != null ? _ka : "text-embedding-3-small",
|
|
1818
1904
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
1819
1905
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
1820
1906
|
dimensions: embeddingDimensions,
|
|
@@ -1825,17 +1911,17 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1825
1911
|
}
|
|
1826
1912
|
},
|
|
1827
1913
|
ui: {
|
|
1828
|
-
title: (
|
|
1829
|
-
subtitle: (
|
|
1830
|
-
primaryColor: (
|
|
1831
|
-
accentColor: (
|
|
1832
|
-
logoUrl: (
|
|
1833
|
-
placeholder: (
|
|
1834
|
-
showSources: ((
|
|
1835
|
-
welcomeMessage: (
|
|
1836
|
-
visualStyle: (
|
|
1837
|
-
borderRadius: (
|
|
1838
|
-
allowUpload: ((
|
|
1914
|
+
title: (_ma = (_la = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _la : readString(env, "UI_TITLE")) != null ? _ma : "AI Assistant",
|
|
1915
|
+
subtitle: (_oa = (_na = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _na : readString(env, "UI_SUBTITLE")) != null ? _oa : "Powered by RAG",
|
|
1916
|
+
primaryColor: (_qa = (_pa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _pa : readString(env, "UI_PRIMARY_COLOR")) != null ? _qa : "#10b981",
|
|
1917
|
+
accentColor: (_sa = (_ra = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ra : readString(env, "UI_ACCENT_COLOR")) != null ? _sa : "#3b82f6",
|
|
1918
|
+
logoUrl: (_ta = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ta : readString(env, "UI_LOGO_URL"),
|
|
1919
|
+
placeholder: (_va = (_ua = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ua : readString(env, "UI_PLACEHOLDER")) != null ? _va : "Ask me anything\u2026",
|
|
1920
|
+
showSources: ((_xa = (_wa = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _wa : readString(env, "UI_SHOW_SOURCES")) != null ? _xa : "true") !== "false",
|
|
1921
|
+
welcomeMessage: (_za = (_ya = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _ya : readString(env, "UI_WELCOME_MESSAGE")) != null ? _za : "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
1922
|
+
visualStyle: (_Ba = (_Aa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _Aa : readString(env, "UI_VISUAL_STYLE")) != null ? _Ba : "glass",
|
|
1923
|
+
borderRadius: (_Da = (_Ca = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _Ca : readString(env, "UI_BORDER_RADIUS")) != null ? _Da : "xl",
|
|
1924
|
+
allowUpload: ((_Fa = (_Ea = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _Ea : readString(env, "UI_ALLOW_UPLOAD")) != null ? _Fa : "false") === "true"
|
|
1839
1925
|
},
|
|
1840
1926
|
rag: {
|
|
1841
1927
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
|
@@ -1952,14 +2038,14 @@ var OpenAIProvider = class {
|
|
|
1952
2038
|
};
|
|
1953
2039
|
}
|
|
1954
2040
|
async chat(messages, context, options) {
|
|
1955
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1956
|
-
const
|
|
2041
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
2042
|
+
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.
|
|
1957
2043
|
|
|
1958
2044
|
Context:
|
|
1959
2045
|
${context}`;
|
|
1960
2046
|
const systemMessage = {
|
|
1961
2047
|
role: "system",
|
|
1962
|
-
content:
|
|
2048
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
1963
2049
|
|
|
1964
2050
|
Context:
|
|
1965
2051
|
${context}`
|
|
@@ -1974,22 +2060,22 @@ ${context}`
|
|
|
1974
2060
|
const completion = await this.client.chat.completions.create({
|
|
1975
2061
|
model: this.llmConfig.model,
|
|
1976
2062
|
messages: formattedMessages,
|
|
1977
|
-
max_tokens: (
|
|
1978
|
-
temperature: (
|
|
2063
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2064
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
1979
2065
|
stop: options == null ? void 0 : options.stop
|
|
1980
2066
|
});
|
|
1981
|
-
return (
|
|
2067
|
+
return (_i = (_h = (_g = completion.choices[0]) == null ? void 0 : _g.message) == null ? void 0 : _h.content) != null ? _i : "";
|
|
1982
2068
|
}
|
|
1983
2069
|
chatStream(messages, context, options) {
|
|
1984
2070
|
return __asyncGenerator(this, null, function* () {
|
|
1985
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1986
|
-
const
|
|
2071
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2072
|
+
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.
|
|
1987
2073
|
|
|
1988
2074
|
Context:
|
|
1989
2075
|
${context}`;
|
|
1990
2076
|
const systemMessage = {
|
|
1991
2077
|
role: "system",
|
|
1992
|
-
content:
|
|
2078
|
+
content: resolvedSystemPrompt.includes("{{context}}") ? resolvedSystemPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedSystemPrompt : `${resolvedSystemPrompt}
|
|
1993
2079
|
|
|
1994
2080
|
Context:
|
|
1995
2081
|
${context}`
|
|
@@ -2004,8 +2090,8 @@ ${context}`
|
|
|
2004
2090
|
const stream = yield new __await(this.client.chat.completions.create({
|
|
2005
2091
|
model: this.llmConfig.model,
|
|
2006
2092
|
messages: formattedMessages,
|
|
2007
|
-
max_tokens: (
|
|
2008
|
-
temperature: (
|
|
2093
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2094
|
+
temperature: (_f = (_e = options == null ? void 0 : options.temperature) != null ? _e : this.llmConfig.temperature) != null ? _f : 0.7,
|
|
2009
2095
|
stop: options == null ? void 0 : options.stop,
|
|
2010
2096
|
stream: true
|
|
2011
2097
|
}));
|
|
@@ -2015,7 +2101,7 @@ ${context}`
|
|
|
2015
2101
|
try {
|
|
2016
2102
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2017
2103
|
const chunk = temp.value;
|
|
2018
|
-
const content = ((
|
|
2104
|
+
const content = ((_h = (_g = chunk.choices[0]) == null ? void 0 : _g.delta) == null ? void 0 : _h.content) || "";
|
|
2019
2105
|
if (content) yield content;
|
|
2020
2106
|
}
|
|
2021
2107
|
} catch (temp) {
|
|
@@ -2113,12 +2199,12 @@ var AnthropicProvider = class {
|
|
|
2113
2199
|
};
|
|
2114
2200
|
}
|
|
2115
2201
|
async chat(messages, context, options) {
|
|
2116
|
-
var _a, _b, _c;
|
|
2117
|
-
const
|
|
2202
|
+
var _a, _b, _c, _d;
|
|
2203
|
+
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.
|
|
2118
2204
|
|
|
2119
2205
|
Context:
|
|
2120
2206
|
${context}`;
|
|
2121
|
-
const system =
|
|
2207
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
2122
2208
|
|
|
2123
2209
|
Context:
|
|
2124
2210
|
${context}`;
|
|
@@ -2128,7 +2214,7 @@ ${context}`;
|
|
|
2128
2214
|
}));
|
|
2129
2215
|
const response = await this.client.messages.create({
|
|
2130
2216
|
model: this.llmConfig.model,
|
|
2131
|
-
max_tokens: (
|
|
2217
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2132
2218
|
system,
|
|
2133
2219
|
messages: anthropicMessages
|
|
2134
2220
|
});
|
|
@@ -2137,12 +2223,12 @@ ${context}`;
|
|
|
2137
2223
|
}
|
|
2138
2224
|
chatStream(messages, context, options) {
|
|
2139
2225
|
return __asyncGenerator(this, null, function* () {
|
|
2140
|
-
var _a, _b, _c;
|
|
2141
|
-
const
|
|
2226
|
+
var _a, _b, _c, _d;
|
|
2227
|
+
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.
|
|
2142
2228
|
|
|
2143
2229
|
Context:
|
|
2144
2230
|
${context}`;
|
|
2145
|
-
const system =
|
|
2231
|
+
const system = resolvedPrompt.includes("{{context}}") ? resolvedPrompt.replace("{{context}}", context) : (options == null ? void 0 : options.systemPrompt) ? resolvedPrompt : `${resolvedPrompt}
|
|
2146
2232
|
|
|
2147
2233
|
Context:
|
|
2148
2234
|
${context}`;
|
|
@@ -2152,7 +2238,7 @@ ${context}`;
|
|
|
2152
2238
|
}));
|
|
2153
2239
|
const stream = yield new __await(this.client.messages.create({
|
|
2154
2240
|
model: this.llmConfig.model,
|
|
2155
|
-
max_tokens: (
|
|
2241
|
+
max_tokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : 1024,
|
|
2156
2242
|
system,
|
|
2157
2243
|
messages: anthropicMessages,
|
|
2158
2244
|
stream: true
|
|
@@ -2267,7 +2353,7 @@ var OllamaProvider = class {
|
|
|
2267
2353
|
}
|
|
2268
2354
|
async chat(messages, context, options) {
|
|
2269
2355
|
var _a, _b, _c, _d;
|
|
2270
|
-
const system = this.buildSystemPrompt(context);
|
|
2356
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
2271
2357
|
const { data } = await this.http.post("/api/chat", {
|
|
2272
2358
|
model: this.llmConfig.model,
|
|
2273
2359
|
stream: false,
|
|
@@ -2285,7 +2371,7 @@ var OllamaProvider = class {
|
|
|
2285
2371
|
chatStream(messages, context, options) {
|
|
2286
2372
|
return __asyncGenerator(this, null, function* () {
|
|
2287
2373
|
var _a, _b, _c, _d, _e, _f;
|
|
2288
|
-
const system = this.buildSystemPrompt(context);
|
|
2374
|
+
const system = this.buildSystemPrompt(context, options == null ? void 0 : options.systemPrompt);
|
|
2289
2375
|
const response = yield new __await(this.http.post("/api/chat", {
|
|
2290
2376
|
model: this.llmConfig.model,
|
|
2291
2377
|
stream: true,
|
|
@@ -2341,8 +2427,11 @@ var OllamaProvider = class {
|
|
|
2341
2427
|
}
|
|
2342
2428
|
});
|
|
2343
2429
|
}
|
|
2344
|
-
buildSystemPrompt(context) {
|
|
2430
|
+
buildSystemPrompt(context, overridePrompt) {
|
|
2345
2431
|
var _a;
|
|
2432
|
+
if (overridePrompt) {
|
|
2433
|
+
return overridePrompt;
|
|
2434
|
+
}
|
|
2346
2435
|
const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Use the provided context to answer the user's question.
|
|
2347
2436
|
|
|
2348
2437
|
Context:
|
|
@@ -2394,20 +2483,41 @@ ${context}`;
|
|
|
2394
2483
|
};
|
|
2395
2484
|
|
|
2396
2485
|
// src/llm/providers/GeminiProvider.ts
|
|
2397
|
-
var
|
|
2486
|
+
var import_generative_ai = require("@google/generative-ai");
|
|
2487
|
+
var DEFAULT_EMBEDDING_MODEL = "text-embedding-004";
|
|
2488
|
+
var DEFAULT_TEMPERATURE = 0.7;
|
|
2489
|
+
var DEFAULT_MAX_TOKENS = 1024;
|
|
2490
|
+
function sanitizeModel(model) {
|
|
2491
|
+
return model ? model.split(":")[0] : model;
|
|
2492
|
+
}
|
|
2493
|
+
function buildClient(apiKey) {
|
|
2494
|
+
return new import_generative_ai.GoogleGenerativeAI(apiKey);
|
|
2495
|
+
}
|
|
2496
|
+
function applyPrefix(text, taskType, queryPrefix, docPrefix) {
|
|
2497
|
+
if (taskType === "query" && queryPrefix && !text.startsWith(queryPrefix)) {
|
|
2498
|
+
return `${queryPrefix}${text}`;
|
|
2499
|
+
}
|
|
2500
|
+
if (taskType === "document" && docPrefix && !text.startsWith(docPrefix)) {
|
|
2501
|
+
return `${docPrefix}${text}`;
|
|
2502
|
+
}
|
|
2503
|
+
return text;
|
|
2504
|
+
}
|
|
2398
2505
|
var GeminiProvider = class {
|
|
2399
2506
|
constructor(llmConfig, embeddingConfig) {
|
|
2400
|
-
if (!llmConfig.apiKey)
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2507
|
+
if (!llmConfig.apiKey) {
|
|
2508
|
+
throw new Error("[GeminiProvider] llmConfig.apiKey is required");
|
|
2509
|
+
}
|
|
2510
|
+
this.llmConfig = __spreadProps(__spreadValues({}, llmConfig), { model: sanitizeModel(llmConfig.model) });
|
|
2511
|
+
this.client = buildClient(this.llmConfig.apiKey);
|
|
2405
2512
|
if (embeddingConfig) {
|
|
2406
2513
|
this.embeddingConfig = __spreadProps(__spreadValues({}, embeddingConfig), {
|
|
2407
|
-
model:
|
|
2514
|
+
model: sanitizeModel(embeddingConfig.model)
|
|
2408
2515
|
});
|
|
2409
2516
|
}
|
|
2410
2517
|
}
|
|
2518
|
+
// -------------------------------------------------------------------------
|
|
2519
|
+
// Static factory helpers
|
|
2520
|
+
// -------------------------------------------------------------------------
|
|
2411
2521
|
static getValidator() {
|
|
2412
2522
|
return {
|
|
2413
2523
|
validate(config) {
|
|
@@ -2421,7 +2531,11 @@ var GeminiProvider = class {
|
|
|
2421
2531
|
});
|
|
2422
2532
|
}
|
|
2423
2533
|
if (!config.model) {
|
|
2424
|
-
errors.push({
|
|
2534
|
+
errors.push({
|
|
2535
|
+
field: "llm.model",
|
|
2536
|
+
message: "Gemini model name is required",
|
|
2537
|
+
severity: "error"
|
|
2538
|
+
});
|
|
2425
2539
|
}
|
|
2426
2540
|
return errors;
|
|
2427
2541
|
}
|
|
@@ -2430,13 +2544,15 @@ var GeminiProvider = class {
|
|
|
2430
2544
|
static getHealthChecker() {
|
|
2431
2545
|
return {
|
|
2432
2546
|
async check(config) {
|
|
2547
|
+
var _a, _b;
|
|
2433
2548
|
const timestamp = Date.now();
|
|
2434
|
-
const apiKey = config.apiKey
|
|
2435
|
-
const modelName = config.model;
|
|
2549
|
+
const apiKey = (_b = (_a = config.apiKey) != null ? _a : process.env.GOOGLE_GENAI_API_KEY) != null ? _b : "";
|
|
2550
|
+
const modelName = sanitizeModel(config.model);
|
|
2436
2551
|
try {
|
|
2437
|
-
const {
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2552
|
+
const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import("@google/generative-ai");
|
|
2553
|
+
const ai = new GoogleGenerativeAI2(apiKey);
|
|
2554
|
+
const model = ai.getGenerativeModel({ model: DEFAULT_EMBEDDING_MODEL });
|
|
2555
|
+
await model.embedContent("health-check");
|
|
2440
2556
|
return {
|
|
2441
2557
|
healthy: true,
|
|
2442
2558
|
provider: "gemini",
|
|
@@ -2454,70 +2570,90 @@ var GeminiProvider = class {
|
|
|
2454
2570
|
}
|
|
2455
2571
|
};
|
|
2456
2572
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2573
|
+
// -------------------------------------------------------------------------
|
|
2574
|
+
// Private utilities
|
|
2575
|
+
// -------------------------------------------------------------------------
|
|
2576
|
+
/** Resolve the embedding client — uses a separate client when the embedding
|
|
2577
|
+
* API key differs from the LLM API key. */
|
|
2578
|
+
get embeddingClient() {
|
|
2579
|
+
var _a;
|
|
2580
|
+
if (((_a = this.embeddingConfig) == null ? void 0 : _a.apiKey) && this.embeddingConfig.apiKey !== this.llmConfig.apiKey) {
|
|
2581
|
+
return buildClient(this.embeddingConfig.apiKey);
|
|
2582
|
+
}
|
|
2583
|
+
return this.client;
|
|
2460
2584
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2585
|
+
/** Resolve the embedding model to use, in order of specificity. */
|
|
2586
|
+
resolveEmbeddingModel(optionsModel) {
|
|
2587
|
+
var _a, _b;
|
|
2588
|
+
return sanitizeModel((_b = optionsModel != null ? optionsModel : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _b : DEFAULT_EMBEDDING_MODEL);
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Build the system instruction string, inserting the RAG context either via
|
|
2592
|
+
* the `{{context}}` placeholder or by appending it.
|
|
2593
|
+
*/
|
|
2594
|
+
buildSystemInstruction(context) {
|
|
2595
|
+
var _a;
|
|
2596
|
+
const base = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
|
|
2464
2597
|
|
|
2465
2598
|
Context:
|
|
2466
2599
|
${context}`;
|
|
2467
|
-
|
|
2600
|
+
return base.includes("{{context}}") ? base.replace("{{context}}", context) : `${base}
|
|
2468
2601
|
|
|
2469
2602
|
Context:
|
|
2470
2603
|
${context}`;
|
|
2471
|
-
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
2606
|
+
* Convert ChatMessage[] to the Gemini contents format.
|
|
2607
|
+
*
|
|
2608
|
+
* Rules enforced by the Gemini API:
|
|
2609
|
+
* - Only `user` and `model` roles are allowed in `contents`.
|
|
2610
|
+
* - System messages must be passed via `systemInstruction`, not here.
|
|
2611
|
+
* - Messages must strictly alternate between `user` and `model`.
|
|
2612
|
+
*/
|
|
2613
|
+
buildGeminiContents(messages) {
|
|
2614
|
+
return messages.filter((m) => m.role !== "system").map((m) => ({
|
|
2472
2615
|
role: m.role === "assistant" ? "model" : "user",
|
|
2473
2616
|
parts: [{ text: m.content }]
|
|
2474
2617
|
}));
|
|
2475
|
-
|
|
2618
|
+
}
|
|
2619
|
+
// -------------------------------------------------------------------------
|
|
2620
|
+
// ILLMProvider — chat
|
|
2621
|
+
// -------------------------------------------------------------------------
|
|
2622
|
+
async chat(messages, context, options) {
|
|
2623
|
+
var _a, _b, _c, _d;
|
|
2624
|
+
const model = this.client.getGenerativeModel({
|
|
2476
2625
|
model: this.llmConfig.model,
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2626
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
2627
|
+
});
|
|
2628
|
+
const result = await model.generateContent({
|
|
2629
|
+
contents: this.buildGeminiContents(messages),
|
|
2630
|
+
generationConfig: {
|
|
2631
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
2632
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
2482
2633
|
stopSequences: options == null ? void 0 : options.stop
|
|
2483
2634
|
}
|
|
2484
2635
|
});
|
|
2485
|
-
|
|
2486
|
-
return text;
|
|
2636
|
+
return result.response.text();
|
|
2487
2637
|
}
|
|
2488
2638
|
chatStream(messages, context, options) {
|
|
2489
2639
|
return __asyncGenerator(this, null, function* () {
|
|
2490
|
-
var _a, _b, _c, _d
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
Context:
|
|
2494
|
-
${context}`;
|
|
2495
|
-
const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
|
|
2496
|
-
|
|
2497
|
-
Context:
|
|
2498
|
-
${context}`;
|
|
2499
|
-
const geminiMessages = messages.map((m) => ({
|
|
2500
|
-
role: m.role === "assistant" ? "model" : "user",
|
|
2501
|
-
parts: [{ text: m.content }]
|
|
2502
|
-
}));
|
|
2503
|
-
const result = yield new __await(this.client.models.generateContentStream({
|
|
2640
|
+
var _a, _b, _c, _d;
|
|
2641
|
+
const model = this.client.getGenerativeModel({
|
|
2504
2642
|
model: this.llmConfig.model,
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2643
|
+
systemInstruction: this.buildSystemInstruction(context)
|
|
2644
|
+
});
|
|
2645
|
+
const result = yield new __await(model.generateContentStream({
|
|
2646
|
+
contents: this.buildGeminiContents(messages),
|
|
2647
|
+
generationConfig: {
|
|
2648
|
+
temperature: (_b = (_a = options == null ? void 0 : options.temperature) != null ? _a : this.llmConfig.temperature) != null ? _b : DEFAULT_TEMPERATURE,
|
|
2649
|
+
maxOutputTokens: (_d = (_c = options == null ? void 0 : options.maxTokens) != null ? _c : this.llmConfig.maxTokens) != null ? _d : DEFAULT_MAX_TOKENS,
|
|
2510
2650
|
stopSequences: options == null ? void 0 : options.stop
|
|
2511
2651
|
}
|
|
2512
2652
|
}));
|
|
2513
|
-
const stream = (_f = result == null ? void 0 : result.stream) != null ? _f : result;
|
|
2514
|
-
if (!stream) {
|
|
2515
|
-
throw new Error("[GeminiProvider] generateContentStream returned undefined");
|
|
2516
|
-
}
|
|
2517
2653
|
try {
|
|
2518
|
-
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2654
|
+
for (var iter = __forAwait(result.stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
2519
2655
|
const chunk = temp.value;
|
|
2520
|
-
const text =
|
|
2656
|
+
const text = chunk.text();
|
|
2521
2657
|
if (text) yield text;
|
|
2522
2658
|
}
|
|
2523
2659
|
} catch (temp) {
|
|
@@ -2532,69 +2668,69 @@ ${context}`;
|
|
|
2532
2668
|
}
|
|
2533
2669
|
});
|
|
2534
2670
|
}
|
|
2671
|
+
// -------------------------------------------------------------------------
|
|
2672
|
+
// ILLMProvider — embeddings
|
|
2673
|
+
// -------------------------------------------------------------------------
|
|
2535
2674
|
async embed(text, options) {
|
|
2536
|
-
var _a, _b
|
|
2537
|
-
const
|
|
2538
|
-
|
|
2675
|
+
var _a, _b;
|
|
2676
|
+
const content = applyPrefix(
|
|
2677
|
+
text,
|
|
2678
|
+
options == null ? void 0 : options.taskType,
|
|
2679
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
2680
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
2539
2681
|
);
|
|
2540
|
-
const
|
|
2541
|
-
const
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
|
|
2545
|
-
if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
|
|
2546
|
-
if (!content.startsWith(queryPrefix)) {
|
|
2547
|
-
content = `${queryPrefix}${text}`;
|
|
2548
|
-
}
|
|
2549
|
-
} else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
|
|
2550
|
-
if (!content.startsWith(docPrefix)) {
|
|
2551
|
-
content = `${docPrefix}${text}`;
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
const response = await client.models.embedContent({
|
|
2555
|
-
model,
|
|
2556
|
-
contents: content,
|
|
2557
|
-
config: ((_h = this.embeddingConfig) == null ? void 0 : _h.dimensions) ? { outputDimensionality: this.embeddingConfig.dimensions } : void 0
|
|
2682
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
2683
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
2684
|
+
const response = await model.embedContent({
|
|
2685
|
+
content: { role: "user", parts: [{ text: content }] }
|
|
2558
2686
|
}).catch((err) => {
|
|
2559
|
-
console.error(`[GeminiProvider] Embedding failed for model "${
|
|
2687
|
+
console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
|
|
2560
2688
|
throw err;
|
|
2561
2689
|
});
|
|
2562
|
-
return
|
|
2690
|
+
return response.embedding.values;
|
|
2563
2691
|
}
|
|
2564
2692
|
async batchEmbed(texts, options) {
|
|
2565
|
-
|
|
2566
|
-
const model = this.
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2693
|
+
const modelName = this.resolveEmbeddingModel(options == null ? void 0 : options.model);
|
|
2694
|
+
const model = this.embeddingClient.getGenerativeModel({ model: modelName });
|
|
2695
|
+
const requests = texts.map((text) => {
|
|
2696
|
+
var _a, _b;
|
|
2697
|
+
return {
|
|
2698
|
+
content: {
|
|
2699
|
+
role: "user",
|
|
2700
|
+
parts: [{
|
|
2701
|
+
text: applyPrefix(
|
|
2702
|
+
text,
|
|
2703
|
+
options == null ? void 0 : options.taskType,
|
|
2704
|
+
(_a = this.embeddingConfig) == null ? void 0 : _a.queryPrefix,
|
|
2705
|
+
(_b = this.embeddingConfig) == null ? void 0 : _b.documentPrefix
|
|
2706
|
+
)
|
|
2707
|
+
}]
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
});
|
|
2711
|
+
const response = await model.batchEmbedContents({
|
|
2712
|
+
requests: requests.map((r) => ({
|
|
2713
|
+
model: `models/${modelName}`,
|
|
2714
|
+
content: r.content
|
|
2715
|
+
}))
|
|
2583
2716
|
}).catch((err) => {
|
|
2584
|
-
console.error(`[GeminiProvider] Batch embedding failed for model "${
|
|
2717
|
+
console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
|
|
2585
2718
|
throw err;
|
|
2586
2719
|
});
|
|
2587
|
-
return
|
|
2588
|
-
var
|
|
2589
|
-
return (
|
|
2590
|
-
})
|
|
2720
|
+
return response.embeddings.map((e) => {
|
|
2721
|
+
var _a;
|
|
2722
|
+
return (_a = e.values) != null ? _a : [];
|
|
2723
|
+
});
|
|
2591
2724
|
}
|
|
2725
|
+
// -------------------------------------------------------------------------
|
|
2726
|
+
// ILLMProvider — health
|
|
2727
|
+
// -------------------------------------------------------------------------
|
|
2592
2728
|
async ping() {
|
|
2593
2729
|
try {
|
|
2594
|
-
|
|
2595
|
-
model:
|
|
2596
|
-
contents: "ping"
|
|
2730
|
+
const model = this.embeddingClient.getGenerativeModel({
|
|
2731
|
+
model: this.resolveEmbeddingModel()
|
|
2597
2732
|
});
|
|
2733
|
+
await model.embedContent("ping");
|
|
2598
2734
|
return true;
|
|
2599
2735
|
} catch (err) {
|
|
2600
2736
|
console.error("[GeminiProvider] Ping failed:", err);
|
|
@@ -2790,9 +2926,51 @@ ${context != null ? context : "None"}` },
|
|
|
2790
2926
|
};
|
|
2791
2927
|
|
|
2792
2928
|
// src/llm/LLMFactory.ts
|
|
2929
|
+
var customProviders = /* @__PURE__ */ new Map();
|
|
2793
2930
|
var LLMFactory = class _LLMFactory {
|
|
2931
|
+
/**
|
|
2932
|
+
* Register a custom LLM provider factory at runtime.
|
|
2933
|
+
*
|
|
2934
|
+
* Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
|
|
2935
|
+
* Bedrock, etc.) without modifying this library's source code.
|
|
2936
|
+
*
|
|
2937
|
+
* @example
|
|
2938
|
+
* // In your Next.js app initialization:
|
|
2939
|
+
* import { LLMFactory } from '@retrivora-ai/rag-engine/server';
|
|
2940
|
+
* import { MyCustomProvider } from './providers/MyCustomProvider';
|
|
2941
|
+
*
|
|
2942
|
+
* LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
|
|
2943
|
+
*
|
|
2944
|
+
* // Then set in your .env.local:
|
|
2945
|
+
* // LLM_PROVIDER=my-provider
|
|
2946
|
+
*/
|
|
2947
|
+
static register(name, factory) {
|
|
2948
|
+
customProviders.set(name.toLowerCase(), factory);
|
|
2949
|
+
console.log(`[LLMFactory] Registered custom provider: "${name}"`);
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Unregister a previously registered custom provider.
|
|
2953
|
+
*/
|
|
2954
|
+
static unregister(name) {
|
|
2955
|
+
customProviders.delete(name.toLowerCase());
|
|
2956
|
+
}
|
|
2957
|
+
/**
|
|
2958
|
+
* List all registered provider names (built-in + custom).
|
|
2959
|
+
*/
|
|
2960
|
+
static listProviders() {
|
|
2961
|
+
return [
|
|
2962
|
+
"openai",
|
|
2963
|
+
"anthropic",
|
|
2964
|
+
"ollama",
|
|
2965
|
+
"gemini",
|
|
2966
|
+
"rest",
|
|
2967
|
+
"universal_rest",
|
|
2968
|
+
"custom",
|
|
2969
|
+
...Array.from(customProviders.keys())
|
|
2970
|
+
];
|
|
2971
|
+
}
|
|
2794
2972
|
static create(llmConfig, embeddingConfig) {
|
|
2795
|
-
var _a;
|
|
2973
|
+
var _a, _b;
|
|
2796
2974
|
switch (llmConfig.provider) {
|
|
2797
2975
|
case "openai":
|
|
2798
2976
|
return new OpenAIProvider(llmConfig, embeddingConfig);
|
|
@@ -2806,11 +2984,19 @@ var LLMFactory = class _LLMFactory {
|
|
|
2806
2984
|
case "universal_rest":
|
|
2807
2985
|
case "custom":
|
|
2808
2986
|
return new UniversalLLMAdapter(llmConfig);
|
|
2809
|
-
default:
|
|
2810
|
-
|
|
2987
|
+
default: {
|
|
2988
|
+
const providerName = String((_a = llmConfig.provider) != null ? _a : "").toLowerCase();
|
|
2989
|
+
const customFactory = customProviders.get(providerName);
|
|
2990
|
+
if (customFactory) {
|
|
2991
|
+
return customFactory(llmConfig);
|
|
2992
|
+
}
|
|
2993
|
+
if (llmConfig.baseUrl || ((_b = llmConfig.options) == null ? void 0 : _b.baseUrl)) {
|
|
2811
2994
|
return new UniversalLLMAdapter(llmConfig);
|
|
2812
2995
|
}
|
|
2813
|
-
throw new Error(
|
|
2996
|
+
throw new Error(
|
|
2997
|
+
`[LLMFactory] Unknown provider "${llmConfig.provider}". Built-in providers: ${_LLMFactory.listProviders().join(", ")}. Register a custom provider with LLMFactory.register().`
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
2814
3000
|
}
|
|
2815
3001
|
}
|
|
2816
3002
|
static getValidator(provider) {
|
|
@@ -2900,8 +3086,8 @@ var ProviderRegistry = class {
|
|
|
2900
3086
|
}
|
|
2901
3087
|
case "pgvector":
|
|
2902
3088
|
case "postgresql": {
|
|
2903
|
-
const {
|
|
2904
|
-
return
|
|
3089
|
+
const { MultiTablePostgresProvider: MultiTablePostgresProvider2 } = await Promise.resolve().then(() => (init_MultiTablePostgresProvider(), MultiTablePostgresProvider_exports));
|
|
3090
|
+
return MultiTablePostgresProvider2;
|
|
2905
3091
|
}
|
|
2906
3092
|
case "mongodb": {
|
|
2907
3093
|
const { MongoDBProvider: MongoDBProvider2 } = await Promise.resolve().then(() => (init_MongoDBProvider(), MongoDBProvider_exports));
|
|
@@ -3865,73 +4051,102 @@ var QueryProcessor = class {
|
|
|
3865
4051
|
}
|
|
3866
4052
|
};
|
|
3867
4053
|
|
|
4054
|
+
// src/utils/synonyms.ts
|
|
4055
|
+
var FIELD_SYNONYMS = {
|
|
4056
|
+
name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
|
|
4057
|
+
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
|
|
4058
|
+
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
4059
|
+
image: [
|
|
4060
|
+
"imageUrl",
|
|
4061
|
+
"thumbnail",
|
|
4062
|
+
"img",
|
|
4063
|
+
"url",
|
|
4064
|
+
"photo",
|
|
4065
|
+
"picture",
|
|
4066
|
+
"media",
|
|
4067
|
+
"image_url",
|
|
4068
|
+
"main_image",
|
|
4069
|
+
"product_image",
|
|
4070
|
+
"thumb"
|
|
4071
|
+
],
|
|
4072
|
+
stock: [
|
|
4073
|
+
"inventory",
|
|
4074
|
+
"quantity",
|
|
4075
|
+
"count",
|
|
4076
|
+
"availability",
|
|
4077
|
+
"stock_level",
|
|
4078
|
+
"inStock",
|
|
4079
|
+
"is_available",
|
|
4080
|
+
"in stock",
|
|
4081
|
+
"status"
|
|
4082
|
+
],
|
|
4083
|
+
description: ["summary", "content", "body", "text", "info", "details"],
|
|
4084
|
+
link: ["url", "href", "product_url", "page_url", "link"]
|
|
4085
|
+
};
|
|
4086
|
+
function resolveMetadataValue(meta, uiKey) {
|
|
4087
|
+
var _a;
|
|
4088
|
+
const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
|
|
4089
|
+
const keys = Object.keys(meta);
|
|
4090
|
+
const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
|
|
4091
|
+
let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
|
|
4092
|
+
if (match !== void 0) return meta[match];
|
|
4093
|
+
match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
|
|
4094
|
+
if (match !== void 0) return meta[match];
|
|
4095
|
+
const isBlacklisted = (kl) => {
|
|
4096
|
+
return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
|
|
4097
|
+
};
|
|
4098
|
+
match = keys.find((k) => {
|
|
4099
|
+
const kl = k.toLowerCase();
|
|
4100
|
+
if (isBlacklisted(kl)) return false;
|
|
4101
|
+
return kl.includes(uiKey.toLowerCase());
|
|
4102
|
+
});
|
|
4103
|
+
if (match !== void 0) return meta[match];
|
|
4104
|
+
match = keys.find((k) => {
|
|
4105
|
+
const kl = k.toLowerCase();
|
|
4106
|
+
if (isBlacklisted(kl)) return false;
|
|
4107
|
+
return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
|
|
4108
|
+
});
|
|
4109
|
+
return match !== void 0 ? meta[match] : void 0;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
3868
4112
|
// src/utils/UITransformer.ts
|
|
3869
4113
|
var UITransformer = class {
|
|
3870
4114
|
/**
|
|
3871
4115
|
* Main transformation method
|
|
3872
|
-
* Analyzes user query and retrieved data to determine
|
|
3873
|
-
*
|
|
3874
|
-
* @param userQuery - The original user question/query
|
|
3875
|
-
* @param retrievedData - Vector database retrieval results
|
|
3876
|
-
* @returns Structured JSON response for UI rendering
|
|
4116
|
+
* Analyzes user query and retrieved data to determine if a product carousel is needed.
|
|
3877
4117
|
*/
|
|
3878
|
-
static transform(userQuery, retrievedData) {
|
|
4118
|
+
static transform(userQuery, retrievedData, config, trainedSchema) {
|
|
3879
4119
|
if (!retrievedData || retrievedData.length === 0) {
|
|
3880
4120
|
return this.createTextResponse("No data available", "No relevant data found for your query.");
|
|
3881
4121
|
}
|
|
3882
|
-
const
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
case "line_chart":
|
|
3891
|
-
return this.transformToLineChart(retrievedData);
|
|
3892
|
-
case "table":
|
|
3893
|
-
return this.transformToTable(retrievedData);
|
|
3894
|
-
default:
|
|
3895
|
-
return this.transformToText(retrievedData);
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
/**
|
|
3899
|
-
* Analyzes the data and query to suggest the best visualization type
|
|
3900
|
-
*/
|
|
3901
|
-
static analyzeData(query, data) {
|
|
3902
|
-
const queryLower = query.toLowerCase();
|
|
3903
|
-
const hasProducts = data.some(
|
|
3904
|
-
(item) => this.isProductData(item)
|
|
3905
|
-
);
|
|
3906
|
-
const hasTimeSeries = data.some(
|
|
3907
|
-
(item) => this.isTimeSeriesData(item)
|
|
3908
|
-
);
|
|
3909
|
-
const hasCategories = this.detectCategories(data).length > 0;
|
|
3910
|
-
const isDistributionQuery = queryLower.includes("distribution") || queryLower.includes("breakdown") || queryLower.includes("percentage") || queryLower.includes("proportion");
|
|
3911
|
-
const isComparisonQuery = queryLower.includes("compare") || queryLower.includes("vs") || queryLower.includes("difference");
|
|
3912
|
-
const isTrendQuery = queryLower.includes("trend") || queryLower.includes("over time") || queryLower.includes("growth") || queryLower.includes("decline");
|
|
3913
|
-
if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
|
|
3914
|
-
return { suggestedType: "product_carousel", confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
|
|
4122
|
+
const isStockRequest = this.isStockQuery(userQuery);
|
|
4123
|
+
const filteredData = isStockRequest ? retrievedData.filter((item) => this.determineStockStatus(item)) : retrievedData;
|
|
4124
|
+
const categories = this.detectCategories(filteredData);
|
|
4125
|
+
const hasProducts = filteredData.some((item) => this.isProductData(item));
|
|
4126
|
+
const isTimeSeries = filteredData.some((item) => this.isTimeSeriesData(item));
|
|
4127
|
+
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
4128
|
+
if (isTrendQuery && isTimeSeries) {
|
|
4129
|
+
return this.transformToLineChart(filteredData);
|
|
3915
4130
|
}
|
|
3916
|
-
if (
|
|
3917
|
-
return
|
|
4131
|
+
if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
|
|
4132
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
3918
4133
|
}
|
|
3919
|
-
if (
|
|
3920
|
-
return
|
|
4134
|
+
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
4135
|
+
return this.transformToPieChart(filteredData);
|
|
3921
4136
|
}
|
|
3922
|
-
if (
|
|
3923
|
-
return
|
|
4137
|
+
if (hasProducts) {
|
|
4138
|
+
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
3924
4139
|
}
|
|
3925
|
-
if (
|
|
3926
|
-
return
|
|
4140
|
+
if (this.hasMultipleFields(filteredData)) {
|
|
4141
|
+
return this.transformToTable(filteredData);
|
|
3927
4142
|
}
|
|
3928
|
-
return
|
|
4143
|
+
return this.transformToText(filteredData);
|
|
3929
4144
|
}
|
|
3930
4145
|
/**
|
|
3931
4146
|
* Transform data to product carousel format
|
|
3932
4147
|
*/
|
|
3933
|
-
static transformToProductCarousel(data) {
|
|
3934
|
-
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item)).filter((p) => p !== null);
|
|
4148
|
+
static transformToProductCarousel(data, config, trainedSchema) {
|
|
4149
|
+
const products = data.filter((item) => this.isProductData(item)).map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
|
|
3935
4150
|
return {
|
|
3936
4151
|
type: "product_carousel",
|
|
3937
4152
|
title: "Recommended Products",
|
|
@@ -3962,29 +4177,7 @@ var UITransformer = class {
|
|
|
3962
4177
|
};
|
|
3963
4178
|
}
|
|
3964
4179
|
/**
|
|
3965
|
-
* Transform data to
|
|
3966
|
-
*/
|
|
3967
|
-
static transformToBarChart(data) {
|
|
3968
|
-
const categories = this.detectCategories(data);
|
|
3969
|
-
const categoryData = this.aggregateByCategory(data, categories);
|
|
3970
|
-
const barData = Object.entries(categoryData).map(([category, value]) => {
|
|
3971
|
-
const { inStockCount, outOfStockCount } = this.calculateStockCounts(category, data);
|
|
3972
|
-
return {
|
|
3973
|
-
category,
|
|
3974
|
-
value,
|
|
3975
|
-
inStockCount,
|
|
3976
|
-
outOfStockCount
|
|
3977
|
-
};
|
|
3978
|
-
});
|
|
3979
|
-
return {
|
|
3980
|
-
type: "bar_chart",
|
|
3981
|
-
title: "Comparison by Category",
|
|
3982
|
-
description: `Comparing ${categories.length} categories`,
|
|
3983
|
-
data: barData
|
|
3984
|
-
};
|
|
3985
|
-
}
|
|
3986
|
-
/**
|
|
3987
|
-
* Transform data to line chart format
|
|
4180
|
+
* Transform data to line chart format
|
|
3988
4181
|
*/
|
|
3989
4182
|
static transformToLineChart(data) {
|
|
3990
4183
|
const timePoints = this.extractTimeSeriesData(data);
|
|
@@ -4039,49 +4232,253 @@ var UITransformer = class {
|
|
|
4039
4232
|
data: { content }
|
|
4040
4233
|
};
|
|
4041
4234
|
}
|
|
4235
|
+
static parseTransformationResponse(raw) {
|
|
4236
|
+
const payloadText = this.extractJsonCandidate(raw);
|
|
4237
|
+
if (!payloadText) return null;
|
|
4238
|
+
try {
|
|
4239
|
+
const parsed = JSON.parse(payloadText);
|
|
4240
|
+
return this.normalizeTransformation(parsed);
|
|
4241
|
+
} catch (e) {
|
|
4242
|
+
return null;
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
static extractJsonCandidate(raw) {
|
|
4246
|
+
if (!raw) return null;
|
|
4247
|
+
const cleaned = raw.replace(/```(?:json|chart|ui)?\s*/gi, "").replace(/```/g, "").trim();
|
|
4248
|
+
const start = cleaned.indexOf("{");
|
|
4249
|
+
if (start === -1) return null;
|
|
4250
|
+
let depth = 0;
|
|
4251
|
+
let inString = false;
|
|
4252
|
+
let escape = false;
|
|
4253
|
+
for (let i = start; i < cleaned.length; i += 1) {
|
|
4254
|
+
const char = cleaned[i];
|
|
4255
|
+
if (escape) {
|
|
4256
|
+
escape = false;
|
|
4257
|
+
continue;
|
|
4258
|
+
}
|
|
4259
|
+
if (char === "\\") {
|
|
4260
|
+
escape = true;
|
|
4261
|
+
continue;
|
|
4262
|
+
}
|
|
4263
|
+
if (char === '"') {
|
|
4264
|
+
inString = !inString;
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
if (inString) continue;
|
|
4268
|
+
if (char === "{") depth += 1;
|
|
4269
|
+
if (char === "}") {
|
|
4270
|
+
depth -= 1;
|
|
4271
|
+
if (depth === 0) {
|
|
4272
|
+
return cleaned.slice(start, i + 1);
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return null;
|
|
4277
|
+
}
|
|
4278
|
+
static normalizeTransformation(payload) {
|
|
4279
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
4280
|
+
if (!payload || typeof payload !== "object") return null;
|
|
4281
|
+
const payloadObj = payload;
|
|
4282
|
+
const type = this.normalizeVisualizationType(String((_c = (_b = (_a = payloadObj.type) != null ? _a : payloadObj.view) != null ? _b : payloadObj.chartType) != null ? _c : ""));
|
|
4283
|
+
if (!type) return null;
|
|
4284
|
+
const title = String((_e = (_d = payloadObj.title) != null ? _d : payloadObj.heading) != null ? _e : "Visualization");
|
|
4285
|
+
const description = payloadObj.description ? String(payloadObj.description) : void 0;
|
|
4286
|
+
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;
|
|
4287
|
+
const data = type === "text" && typeof rawData === "string" ? { content: rawData } : rawData;
|
|
4288
|
+
const transformation = {
|
|
4289
|
+
type,
|
|
4290
|
+
title,
|
|
4291
|
+
description,
|
|
4292
|
+
data
|
|
4293
|
+
};
|
|
4294
|
+
return this.validateTransformation(transformation) ? transformation : null;
|
|
4295
|
+
}
|
|
4296
|
+
static normalizeVisualizationType(type) {
|
|
4297
|
+
var _a;
|
|
4298
|
+
const mapping = {
|
|
4299
|
+
pie: "pie_chart",
|
|
4300
|
+
pie_chart: "pie_chart",
|
|
4301
|
+
bar: "bar_chart",
|
|
4302
|
+
bar_chart: "bar_chart",
|
|
4303
|
+
line: "line_chart",
|
|
4304
|
+
line_chart: "line_chart",
|
|
4305
|
+
radar: "radar_chart",
|
|
4306
|
+
radar_chart: "radar_chart",
|
|
4307
|
+
table: "table",
|
|
4308
|
+
text: "text",
|
|
4309
|
+
product_carousel: "product_carousel",
|
|
4310
|
+
carousel: "carousel"
|
|
4311
|
+
};
|
|
4312
|
+
return (_a = mapping[type.toLowerCase()]) != null ? _a : null;
|
|
4313
|
+
}
|
|
4314
|
+
static validateTransformation(transformation) {
|
|
4315
|
+
const { type, data } = transformation;
|
|
4316
|
+
switch (type) {
|
|
4317
|
+
case "pie_chart":
|
|
4318
|
+
case "bar_chart":
|
|
4319
|
+
return Array.isArray(data) && data.every(
|
|
4320
|
+
(item) => item !== null && typeof item === "object" && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
4321
|
+
);
|
|
4322
|
+
case "line_chart":
|
|
4323
|
+
return Array.isArray(data) && data.every(
|
|
4324
|
+
(item) => item !== null && typeof item === "object" && "timestamp" in item && (typeof item.value === "number" || !Number.isNaN(Number(item.value)))
|
|
4325
|
+
);
|
|
4326
|
+
case "radar_chart":
|
|
4327
|
+
return Array.isArray(data) && data.every(
|
|
4328
|
+
(item) => item !== null && typeof item === "object" && "attribute" in item
|
|
4329
|
+
);
|
|
4330
|
+
case "table":
|
|
4331
|
+
return typeof data === "object" && data !== null && Array.isArray(data.columns) && Array.isArray(data.rows);
|
|
4332
|
+
case "text":
|
|
4333
|
+
return typeof data === "object" && data !== null && typeof data.content === "string";
|
|
4334
|
+
case "product_carousel":
|
|
4335
|
+
case "carousel":
|
|
4336
|
+
return Array.isArray(data) && data.every(
|
|
4337
|
+
(item) => item !== null && typeof item === "object" && ("id" in item || "name" in item)
|
|
4338
|
+
);
|
|
4339
|
+
default:
|
|
4340
|
+
return false;
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4042
4343
|
/**
|
|
4043
4344
|
* Helper: Check if data item is product-related
|
|
4044
4345
|
*/
|
|
4045
4346
|
static isProductData(item) {
|
|
4046
4347
|
const content = (item.content || "").toLowerCase();
|
|
4047
|
-
const productKeywords = [
|
|
4048
|
-
|
|
4049
|
-
|
|
4348
|
+
const productKeywords = [
|
|
4349
|
+
"product",
|
|
4350
|
+
"price",
|
|
4351
|
+
"stock",
|
|
4352
|
+
"item",
|
|
4353
|
+
"sku",
|
|
4354
|
+
"brand",
|
|
4355
|
+
"model",
|
|
4356
|
+
"msrp",
|
|
4357
|
+
"inventory",
|
|
4358
|
+
"buy",
|
|
4359
|
+
"shop",
|
|
4360
|
+
"beauty",
|
|
4361
|
+
"care",
|
|
4362
|
+
"cosmetic",
|
|
4363
|
+
"facial",
|
|
4364
|
+
"cream",
|
|
4365
|
+
"serum",
|
|
4366
|
+
"mask",
|
|
4367
|
+
"makeup",
|
|
4368
|
+
"fragrance"
|
|
4369
|
+
];
|
|
4370
|
+
const hasKeywords = productKeywords.some((kw) => content.includes(kw));
|
|
4371
|
+
const hasMetadataKey = Object.keys(item.metadata || {}).some(
|
|
4372
|
+
(k) => ["name", "price", "product", "sku", "brand", "model", "cost", "item"].includes(k.toLowerCase())
|
|
4050
4373
|
);
|
|
4374
|
+
const hasPricePattern = /\$\s*\d+/.test(content);
|
|
4375
|
+
return hasKeywords || hasMetadataKey || hasPricePattern;
|
|
4051
4376
|
}
|
|
4052
4377
|
/**
|
|
4053
4378
|
* Helper: Check if data contains time series
|
|
4054
4379
|
*/
|
|
4055
4380
|
static isTimeSeriesData(item) {
|
|
4056
4381
|
const content = (item.content || "").toLowerCase();
|
|
4057
|
-
const timeKeywords = ["
|
|
4058
|
-
|
|
4382
|
+
const timeKeywords = ["trend", "historical", "growth", "decline", "change", "increase", "decrease"];
|
|
4383
|
+
const hasTimeKeyword = timeKeywords.some((kw) => content.includes(kw));
|
|
4384
|
+
const metadata = item.metadata || {};
|
|
4385
|
+
const maybeDateKeys = Object.keys(metadata).filter(
|
|
4059
4386
|
(k) => ["date", "timestamp", "time", "period"].includes(k.toLowerCase())
|
|
4060
4387
|
);
|
|
4388
|
+
const hasValidDateValue = maybeDateKeys.some((key) => {
|
|
4389
|
+
const value = metadata[key];
|
|
4390
|
+
if (typeof value === "string") {
|
|
4391
|
+
return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
|
|
4392
|
+
}
|
|
4393
|
+
return typeof value === "number";
|
|
4394
|
+
});
|
|
4395
|
+
return hasTimeKeyword || hasValidDateValue;
|
|
4396
|
+
}
|
|
4397
|
+
static shouldShowCategoryChart(query, categories) {
|
|
4398
|
+
if (categories.length < 2) {
|
|
4399
|
+
return false;
|
|
4400
|
+
}
|
|
4401
|
+
const normalized = query.toLowerCase();
|
|
4402
|
+
const chartKeywords = [
|
|
4403
|
+
"distribution",
|
|
4404
|
+
"breakdown",
|
|
4405
|
+
"by category",
|
|
4406
|
+
"by type",
|
|
4407
|
+
"compare",
|
|
4408
|
+
"share",
|
|
4409
|
+
"percentage",
|
|
4410
|
+
"segmentation",
|
|
4411
|
+
"split",
|
|
4412
|
+
"category breakdown",
|
|
4413
|
+
"category distribution"
|
|
4414
|
+
];
|
|
4415
|
+
return chartKeywords.some((keyword) => normalized.includes(keyword));
|
|
4416
|
+
}
|
|
4417
|
+
static isTrendQuery(query) {
|
|
4418
|
+
const normalized = query.toLowerCase();
|
|
4419
|
+
const trendKeywords = [
|
|
4420
|
+
"trend",
|
|
4421
|
+
"over time",
|
|
4422
|
+
"historical",
|
|
4423
|
+
"growth",
|
|
4424
|
+
"decline",
|
|
4425
|
+
"increase",
|
|
4426
|
+
"decrease",
|
|
4427
|
+
"year",
|
|
4428
|
+
"month",
|
|
4429
|
+
"week",
|
|
4430
|
+
"day",
|
|
4431
|
+
"comparison",
|
|
4432
|
+
"compare",
|
|
4433
|
+
"changes",
|
|
4434
|
+
"timeline"
|
|
4435
|
+
];
|
|
4436
|
+
return trendKeywords.some((keyword) => normalized.includes(keyword));
|
|
4437
|
+
}
|
|
4438
|
+
static isStockQuery(query) {
|
|
4439
|
+
const normalized = query.toLowerCase();
|
|
4440
|
+
return normalized.includes("in stock") || normalized.includes("available") || normalized.includes("availability") || normalized.includes("inventory") || normalized.includes("stock status");
|
|
4061
4441
|
}
|
|
4062
4442
|
/**
|
|
4063
|
-
* Helper: Extract
|
|
4443
|
+
* Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
|
|
4064
4444
|
*/
|
|
4065
|
-
static
|
|
4066
|
-
|
|
4067
|
-
if (meta
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
};
|
|
4445
|
+
static getDynamicVal(meta, uiKey, config, trainedSchema) {
|
|
4446
|
+
var _a;
|
|
4447
|
+
if (!meta) return void 0;
|
|
4448
|
+
const mapping = (_a = config == null ? void 0 : config.rag) == null ? void 0 : _a.uiMapping;
|
|
4449
|
+
if (mapping && mapping[uiKey]) {
|
|
4450
|
+
const mappedKey = mapping[uiKey];
|
|
4451
|
+
if (meta[mappedKey] !== void 0) return meta[mappedKey];
|
|
4452
|
+
}
|
|
4453
|
+
if (trainedSchema && typeof trainedSchema === "object" && trainedSchema !== null) {
|
|
4454
|
+
const trainedKey = trainedSchema[uiKey];
|
|
4455
|
+
if (trainedKey && meta[trainedKey] !== void 0) return meta[trainedKey];
|
|
4077
4456
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4457
|
+
return resolveMetadataValue(meta, uiKey);
|
|
4458
|
+
}
|
|
4459
|
+
static extractProductInfo(item, config, trainedSchema) {
|
|
4460
|
+
const meta = item.metadata || {};
|
|
4461
|
+
const name = this.getDynamicVal(meta, "name", config, trainedSchema);
|
|
4462
|
+
const price = this.getDynamicVal(meta, "price", config, trainedSchema);
|
|
4463
|
+
const brand = this.getDynamicVal(meta, "brand", config, trainedSchema);
|
|
4464
|
+
if (name || this.isProductData(item)) {
|
|
4465
|
+
let finalName = name ? String(name) : void 0;
|
|
4466
|
+
if (!finalName) {
|
|
4467
|
+
const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
4468
|
+
finalName = nameMatch ? nameMatch[1].trim() : item.content.split("\n")[0].substring(0, 60);
|
|
4469
|
+
}
|
|
4470
|
+
let finalPrice = typeof price === "number" || typeof price === "string" ? price : void 0;
|
|
4471
|
+
if (!finalPrice) {
|
|
4472
|
+
const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
|
|
4473
|
+
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, "");
|
|
4474
|
+
}
|
|
4475
|
+
const imageValue = this.getDynamicVal(meta, "image", config, trainedSchema);
|
|
4081
4476
|
return {
|
|
4082
4477
|
id: item.id,
|
|
4083
|
-
name:
|
|
4084
|
-
price:
|
|
4478
|
+
name: finalName,
|
|
4479
|
+
price: finalPrice,
|
|
4480
|
+
image: typeof imageValue === "string" ? imageValue : void 0,
|
|
4481
|
+
brand: brand ? String(brand) : void 0,
|
|
4085
4482
|
description: item.content,
|
|
4086
4483
|
inStock: this.determineStockStatus(item)
|
|
4087
4484
|
};
|
|
@@ -4105,6 +4502,10 @@ var UITransformer = class {
|
|
|
4105
4502
|
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
4106
4503
|
tags.forEach((t) => categories.add(String(t)));
|
|
4107
4504
|
}
|
|
4505
|
+
const contentCategories = Array.from(new Set(
|
|
4506
|
+
Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm)).map((match) => match[1].trim()).filter(Boolean)
|
|
4507
|
+
));
|
|
4508
|
+
contentCategories.forEach((category) => categories.add(category));
|
|
4108
4509
|
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
4109
4510
|
if (categoryMatch) {
|
|
4110
4511
|
categories.add(categoryMatch[1].trim());
|
|
@@ -4123,7 +4524,7 @@ var UITransformer = class {
|
|
|
4123
4524
|
data.forEach((item) => {
|
|
4124
4525
|
const meta = item.metadata || {};
|
|
4125
4526
|
const itemCategory = meta.category || meta.type || "Other";
|
|
4126
|
-
if (
|
|
4527
|
+
if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
|
|
4127
4528
|
result[itemCategory]++;
|
|
4128
4529
|
} else {
|
|
4129
4530
|
result["Other"] = (result["Other"] || 0) + 1;
|
|
@@ -4225,6 +4626,213 @@ var UITransformer = class {
|
|
|
4225
4626
|
});
|
|
4226
4627
|
return fieldCount.size > 2;
|
|
4227
4628
|
}
|
|
4629
|
+
// ─── LLM-Driven Visualization Decision ────────────────────────────────────
|
|
4630
|
+
/**
|
|
4631
|
+
* analyzeAndDecide — sends user question + RAG data to the LLM with a
|
|
4632
|
+
* structured system prompt and parses the JSON response into a
|
|
4633
|
+
* UITransformationResponse.
|
|
4634
|
+
*
|
|
4635
|
+
* This is the recommended entry point for production use. The heuristic
|
|
4636
|
+
* `transform()` method is used as a fallback if the LLM call fails.
|
|
4637
|
+
*
|
|
4638
|
+
* System prompt instructs the LLM to:
|
|
4639
|
+
* - Analyze the question and retrieved data
|
|
4640
|
+
* - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
|
|
4641
|
+
* - Return a strict JSON object — no prose, no markdown fences
|
|
4642
|
+
*
|
|
4643
|
+
* @param query - the original user question
|
|
4644
|
+
* @param sources - vector DB matches returned by RAG retrieval
|
|
4645
|
+
* @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
|
|
4646
|
+
* @returns - a validated UITransformationResponse (type + title + description + data)
|
|
4647
|
+
*/
|
|
4648
|
+
static async analyzeAndDecide(query, sources, llm) {
|
|
4649
|
+
try {
|
|
4650
|
+
const context = this.buildContextSummary(sources);
|
|
4651
|
+
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
4652
|
+
const userPrompt = [
|
|
4653
|
+
`USER QUESTION: ${query}`,
|
|
4654
|
+
"",
|
|
4655
|
+
"RETRIEVED DATA (JSON):",
|
|
4656
|
+
context
|
|
4657
|
+
].join("\n");
|
|
4658
|
+
const rawResponse = await llm.chat(
|
|
4659
|
+
[{ role: "user", content: userPrompt }],
|
|
4660
|
+
"",
|
|
4661
|
+
{ systemPrompt, temperature: 0 }
|
|
4662
|
+
);
|
|
4663
|
+
const parsed = this.parseTransformationResponse(rawResponse);
|
|
4664
|
+
if (parsed) {
|
|
4665
|
+
console.debug("[UITransformer] LLM chose visualization type:", parsed.type);
|
|
4666
|
+
return parsed;
|
|
4667
|
+
}
|
|
4668
|
+
console.warn("[UITransformer] LLM returned unparseable response; falling back to heuristic.");
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
console.warn("[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.", err);
|
|
4671
|
+
}
|
|
4672
|
+
return this.transform(query, sources);
|
|
4673
|
+
}
|
|
4674
|
+
/**
|
|
4675
|
+
* Build the system prompt that instructs the LLM to return a visualization JSON.
|
|
4676
|
+
*/
|
|
4677
|
+
static buildVisualizationSystemPrompt() {
|
|
4678
|
+
return `You are a data visualization expert embedded in a RAG chat system.
|
|
4679
|
+
You will receive a user question and structured data retrieved from a vector database.
|
|
4680
|
+
Your ONLY job is to analyze this information and return a single JSON object that tells
|
|
4681
|
+
the frontend how to visualize it.
|
|
4682
|
+
|
|
4683
|
+
Return ONLY a valid JSON object \u2014 no markdown code fences, no explanation, no prose.
|
|
4684
|
+
|
|
4685
|
+
The JSON must have this exact shape:
|
|
4686
|
+
{
|
|
4687
|
+
"type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
|
|
4688
|
+
"title": "A concise, descriptive title for the visualization",
|
|
4689
|
+
"description": "One sentence describing what the visualization shows",
|
|
4690
|
+
"data": <structured data \u2014 see rules below>
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4693
|
+
DATA SHAPE per type:
|
|
4694
|
+
- bar_chart: array of { "category": string, "value": number }
|
|
4695
|
+
- line_chart: array of { "timestamp": string, "value": number, "label": string }
|
|
4696
|
+
- pie_chart: array of { "label": string, "value": number }
|
|
4697
|
+
- radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
|
|
4698
|
+
Example for radar_chart:
|
|
4699
|
+
[
|
|
4700
|
+
{ "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
|
|
4701
|
+
{ "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
|
|
4702
|
+
{ "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
|
|
4703
|
+
]
|
|
4704
|
+
- table: { "columns": string[], "rows": (string|number)[][] }
|
|
4705
|
+
- text: { "content": "<prose answer>" }
|
|
4706
|
+
|
|
4707
|
+
DECISION RULES (follow strictly):
|
|
4708
|
+
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.
|
|
4709
|
+
2. line_chart \u2192 trends or changes over time (dates, months, years, sequential events)
|
|
4710
|
+
3. pie_chart \u2192 proportional breakdown or percentage distribution
|
|
4711
|
+
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.
|
|
4712
|
+
5. table \u2192 multi-field structured records where each item has \u2265 3 attributes
|
|
4713
|
+
6. text \u2192 conversational or free-form answers where no chart adds value
|
|
4714
|
+
|
|
4715
|
+
IMPORTANT:
|
|
4716
|
+
- Aggregate numeric values from the raw data \u2014 do not pass raw object arrays as data.
|
|
4717
|
+
- Ensure all "value" fields are numbers, not strings.
|
|
4718
|
+
- For bar/line/pie, keep at most 12 data points for readability.
|
|
4719
|
+
- Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
|
|
4720
|
+
}
|
|
4721
|
+
/**
|
|
4722
|
+
* Serialize retrieved vector matches into a compact JSON context string.
|
|
4723
|
+
* Limits the total character count to avoid exceeding LLM context windows.
|
|
4724
|
+
*/
|
|
4725
|
+
static buildContextSummary(sources, maxChars = 6e3) {
|
|
4726
|
+
const items = sources.map((s, i) => {
|
|
4727
|
+
var _a, _b, _c, _d;
|
|
4728
|
+
return {
|
|
4729
|
+
index: i + 1,
|
|
4730
|
+
content: (_b = (_a = s.content) == null ? void 0 : _a.substring(0, 400)) != null ? _b : "",
|
|
4731
|
+
metadata: (_c = s.metadata) != null ? _c : {},
|
|
4732
|
+
score: (_d = s.score) != null ? _d : 0
|
|
4733
|
+
};
|
|
4734
|
+
});
|
|
4735
|
+
const full = JSON.stringify(items, null, 2);
|
|
4736
|
+
if (full.length <= maxChars) return full;
|
|
4737
|
+
const partial = [];
|
|
4738
|
+
let chars = 2;
|
|
4739
|
+
for (const item of items) {
|
|
4740
|
+
const chunk = JSON.stringify(item);
|
|
4741
|
+
if (chars + chunk.length + 2 > maxChars) break;
|
|
4742
|
+
partial.push(item);
|
|
4743
|
+
chars += chunk.length + 2;
|
|
4744
|
+
}
|
|
4745
|
+
return JSON.stringify(partial, null, 2) + "\n// ... truncated";
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
|
|
4749
|
+
// src/utils/SchemaMapper.ts
|
|
4750
|
+
var SchemaMapper = class {
|
|
4751
|
+
/**
|
|
4752
|
+
* Trains the plugin on a set of keys.
|
|
4753
|
+
* This is done once per schema and cached.
|
|
4754
|
+
*/
|
|
4755
|
+
static async train(llm, projectId, keys) {
|
|
4756
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
4757
|
+
if (this.cache.has(cacheKey)) {
|
|
4758
|
+
return this.cache.get(cacheKey);
|
|
4759
|
+
}
|
|
4760
|
+
console.log(`[SchemaMapper] \u{1F9E0} Training AI on new schema keys: ${keys.join(", ")}`);
|
|
4761
|
+
const propertyList = Object.entries(this.TARGET_PROPERTIES).map(([prop, desc]) => `- ${prop} (${desc})`).join("\n");
|
|
4762
|
+
const prompt = `
|
|
4763
|
+
Given these metadata keys from a database: [${keys.join(", ")}]
|
|
4764
|
+
|
|
4765
|
+
Identify which keys best correspond to these standard UI properties:
|
|
4766
|
+
${propertyList}
|
|
4767
|
+
|
|
4768
|
+
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
4769
|
+
If no good match is found for a property, omit it.
|
|
4770
|
+
|
|
4771
|
+
Example:
|
|
4772
|
+
{
|
|
4773
|
+
"name": "Product_Title",
|
|
4774
|
+
"price": "MSRP_USD",
|
|
4775
|
+
"brand": "VendorName"
|
|
4776
|
+
}
|
|
4777
|
+
`;
|
|
4778
|
+
try {
|
|
4779
|
+
const response = await llm.chat(
|
|
4780
|
+
[
|
|
4781
|
+
{ role: "system", content: "You are a database schema expert. You ONLY respond with valid JSON." },
|
|
4782
|
+
{ role: "user", content: prompt }
|
|
4783
|
+
],
|
|
4784
|
+
""
|
|
4785
|
+
);
|
|
4786
|
+
const startIdx = response.indexOf("{");
|
|
4787
|
+
if (startIdx !== -1) {
|
|
4788
|
+
let braceCount = 0;
|
|
4789
|
+
let endIdx = -1;
|
|
4790
|
+
for (let i = startIdx; i < response.length; i++) {
|
|
4791
|
+
if (response[i] === "{") braceCount++;
|
|
4792
|
+
else if (response[i] === "}") {
|
|
4793
|
+
braceCount--;
|
|
4794
|
+
if (braceCount === 0) {
|
|
4795
|
+
endIdx = i;
|
|
4796
|
+
break;
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
if (endIdx !== -1) {
|
|
4801
|
+
const jsonContent = response.substring(startIdx, endIdx + 1);
|
|
4802
|
+
const cleanJson = this.sanitizeJson(jsonContent);
|
|
4803
|
+
const mapping = JSON.parse(cleanJson);
|
|
4804
|
+
this.cache.set(cacheKey, mapping);
|
|
4805
|
+
return mapping;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
} catch (error) {
|
|
4809
|
+
console.warn("[SchemaMapper] AI training failed, falling back to heuristics:", error);
|
|
4810
|
+
}
|
|
4811
|
+
return {};
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* Forgiving JSON parser that fixes common AI formatting mistakes.
|
|
4815
|
+
*/
|
|
4816
|
+
static sanitizeJson(s) {
|
|
4817
|
+
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();
|
|
4818
|
+
}
|
|
4819
|
+
static getCached(projectId, keys) {
|
|
4820
|
+
const cacheKey = `${projectId}:${keys.sort().join(",")}`;
|
|
4821
|
+
return this.cache.get(cacheKey);
|
|
4822
|
+
}
|
|
4823
|
+
};
|
|
4824
|
+
SchemaMapper.cache = /* @__PURE__ */ new Map();
|
|
4825
|
+
/**
|
|
4826
|
+
* Descriptions of standard UI properties to help the AI map fields accurately.
|
|
4827
|
+
*/
|
|
4828
|
+
SchemaMapper.TARGET_PROPERTIES = {
|
|
4829
|
+
name: "The primary title, name, or label of the item",
|
|
4830
|
+
price: "The numeric cost, price, MSRP, or amount",
|
|
4831
|
+
brand: "The manufacturer, vendor, brand, or maker",
|
|
4832
|
+
image: "The URL to an image, thumbnail, photo, or picture",
|
|
4833
|
+
stock: "The availability, inventory count, quantity, or stock level",
|
|
4834
|
+
description: "The detailed text content, summary, or body info",
|
|
4835
|
+
category: "The group, department, type, or category name"
|
|
4228
4836
|
};
|
|
4229
4837
|
|
|
4230
4838
|
// src/core/Pipeline.ts
|
|
@@ -4257,6 +4865,57 @@ var LRUEmbeddingCache = class {
|
|
|
4257
4865
|
return this.cache.size;
|
|
4258
4866
|
}
|
|
4259
4867
|
};
|
|
4868
|
+
function estimateTokens(text) {
|
|
4869
|
+
return Math.ceil(text.length / 4);
|
|
4870
|
+
}
|
|
4871
|
+
var MODEL_COST_PER_1K = {
|
|
4872
|
+
"gpt-4o": { input: 25e-4, output: 0.01 },
|
|
4873
|
+
"gpt-4o-mini": { input: 15e-5, output: 6e-4 },
|
|
4874
|
+
"gpt-4-turbo": { input: 0.01, output: 0.03 },
|
|
4875
|
+
"gpt-3.5-turbo": { input: 5e-4, output: 15e-4 },
|
|
4876
|
+
"claude-3-5-sonnet": { input: 3e-3, output: 0.015 },
|
|
4877
|
+
"claude-3-haiku": { input: 25e-5, output: 125e-5 },
|
|
4878
|
+
"gemini-1.5-flash": { input: 75e-6, output: 3e-4 },
|
|
4879
|
+
"gemini-1.5-pro": { input: 125e-5, output: 5e-3 }
|
|
4880
|
+
};
|
|
4881
|
+
function estimateCostUsd(promptTokens, completionTokens, model) {
|
|
4882
|
+
if (!model) return void 0;
|
|
4883
|
+
const key = Object.keys(MODEL_COST_PER_1K).find((k) => model.toLowerCase().includes(k));
|
|
4884
|
+
if (!key) return void 0;
|
|
4885
|
+
const prices = MODEL_COST_PER_1K[key];
|
|
4886
|
+
return promptTokens / 1e3 * prices.input + completionTokens / 1e3 * prices.output;
|
|
4887
|
+
}
|
|
4888
|
+
async function scoreHallucination(llm, answer, context) {
|
|
4889
|
+
const maxContextChars = 3e3;
|
|
4890
|
+
const truncatedContext = context.length > maxContextChars ? context.slice(0, maxContextChars) + "\n...[truncated]" : context;
|
|
4891
|
+
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.
|
|
4892
|
+
|
|
4893
|
+
CONTEXT:
|
|
4894
|
+
${truncatedContext}
|
|
4895
|
+
|
|
4896
|
+
ANSWER:
|
|
4897
|
+
${answer}
|
|
4898
|
+
|
|
4899
|
+
Return ONLY a valid JSON object with no markdown fences:
|
|
4900
|
+
{"score": <float 0-1>, "reason": "<one sentence>"}
|
|
4901
|
+
|
|
4902
|
+
Where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.`;
|
|
4903
|
+
try {
|
|
4904
|
+
const raw = await llm.chat(
|
|
4905
|
+
[{ role: "user", content: prompt }],
|
|
4906
|
+
"",
|
|
4907
|
+
{ temperature: 0, maxTokens: 120 }
|
|
4908
|
+
);
|
|
4909
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
4910
|
+
if (!jsonMatch) return void 0;
|
|
4911
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4912
|
+
if (typeof parsed.score === "number" && typeof parsed.reason === "string") {
|
|
4913
|
+
return { score: Math.min(1, Math.max(0, parsed.score)), reason: parsed.reason };
|
|
4914
|
+
}
|
|
4915
|
+
} catch (e) {
|
|
4916
|
+
}
|
|
4917
|
+
return void 0;
|
|
4918
|
+
}
|
|
4260
4919
|
var Pipeline = class {
|
|
4261
4920
|
constructor(config) {
|
|
4262
4921
|
this.config = config;
|
|
@@ -4273,206 +4932,30 @@ var Pipeline = class {
|
|
|
4273
4932
|
}
|
|
4274
4933
|
this.reranker = new Reranker();
|
|
4275
4934
|
}
|
|
4935
|
+
/**
|
|
4936
|
+
* Expose the underlying LLM provider (set after initialize()).
|
|
4937
|
+
* Used by the stream handler to pass to UITransformer.analyzeAndDecide().
|
|
4938
|
+
*/
|
|
4939
|
+
getLLMProvider() {
|
|
4940
|
+
return this.initialised ? this.llmProvider : void 0;
|
|
4941
|
+
}
|
|
4276
4942
|
async initialize() {
|
|
4277
|
-
var _a
|
|
4943
|
+
var _a;
|
|
4278
4944
|
if (this.initialised) return;
|
|
4279
|
-
const
|
|
4280
|
-
const chartInstruction = `
|
|
4281
|
-
|
|
4282
|
-
${CHART_MARKER}
|
|
4283
|
-
### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
|
|
4284
|
-
|
|
4285
|
-
You are responsible for returning a SINGLE structured UI block when visualization is required.
|
|
4286
|
-
|
|
4287
|
-
---
|
|
4288
|
-
|
|
4289
|
-
## 1. INTENT CLASSIFICATION (MANDATORY)
|
|
4290
|
-
|
|
4291
|
-
Classify the query into ONE of the following intents:
|
|
4292
|
-
|
|
4293
|
-
- "explore" \u2192 user wants to browse items (e.g., "show me products")
|
|
4294
|
-
- "analyze" \u2192 user wants aggregation/distribution (e.g., "distribution of products")
|
|
4295
|
-
- "compare" \u2192 user wants side-by-side comparison
|
|
4296
|
-
- "detail" \u2192 user wants structured/tabular data
|
|
4297
|
-
|
|
4298
|
-
---
|
|
4299
|
-
|
|
4300
|
-
## 2. VIEW SELECTION (DYNAMIC)
|
|
4301
|
-
|
|
4302
|
-
Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
|
|
4303
|
-
- TRENDS/GROWTH \u2192 "chart" (type: "line")
|
|
4304
|
-
- DISTRIBUTIONS/RATIOS \u2192 "chart" (type: "pie")
|
|
4305
|
-
- COMPARISONS \u2192 "chart" (type: "bar")
|
|
4306
|
-
- PRODUCT LISTS \u2192 "carousel"
|
|
4307
|
-
- RAW DATA/SPECS \u2192 "table"
|
|
4308
|
-
|
|
4309
|
-
\u26A0\uFE0F Dynamic Overrides:
|
|
4310
|
-
- If query is "show products", ALWAYS use carousel.
|
|
4311
|
-
- If data is statistical, ALWAYS use a chart.
|
|
4312
|
-
- NEVER explain why you chose a view.
|
|
4313
|
-
|
|
4314
|
-
---
|
|
4315
|
-
|
|
4316
|
-
## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
|
|
4317
|
-
|
|
4318
|
-
\`\`\`ui
|
|
4319
|
-
{
|
|
4320
|
-
"view": "carousel" | "chart" | "table",
|
|
4321
|
-
"title": "string",
|
|
4322
|
-
"description": "string",
|
|
4323
|
-
"metadata": {
|
|
4324
|
-
"intent": "explore" | "analyze" | "compare" | "detail",
|
|
4325
|
-
"confidence": 0.0-1.0
|
|
4326
|
-
},
|
|
4945
|
+
const chartInstruction = `You are a helpful product assistant. Use the provided context to answer questions accurately.
|
|
4327
4946
|
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
"data": []
|
|
4334
|
-
},
|
|
4335
|
-
|
|
4336
|
-
// TABLE ONLY
|
|
4337
|
-
"table": {
|
|
4338
|
-
"columns": [],
|
|
4339
|
-
"rows": []
|
|
4340
|
-
},
|
|
4341
|
-
|
|
4342
|
-
// CAROUSEL ONLY
|
|
4343
|
-
"carousel": {
|
|
4344
|
-
"items": []
|
|
4345
|
-
},
|
|
4346
|
-
|
|
4347
|
-
"insights": []
|
|
4348
|
-
}
|
|
4349
|
-
\`\`\`
|
|
4350
|
-
|
|
4351
|
-
---
|
|
4352
|
-
|
|
4353
|
-
## 4. DATA CONTRACT (CRITICAL)
|
|
4354
|
-
|
|
4355
|
-
### \u{1F539} CAROUSEL ITEM FORMAT
|
|
4356
|
-
{
|
|
4357
|
-
"id": "string",
|
|
4358
|
-
"name": "string",
|
|
4359
|
-
"brand": "string",
|
|
4360
|
-
"price": number,
|
|
4361
|
-
"image": "url",
|
|
4362
|
-
"link": "url",
|
|
4363
|
-
"inStock": boolean
|
|
4364
|
-
}
|
|
4365
|
-
|
|
4366
|
-
### \u{1F539} CHART DATA FORMAT
|
|
4367
|
-
{
|
|
4368
|
-
"label": "string",
|
|
4369
|
-
"value": number,
|
|
4370
|
-
"inStockCount": number (optional),
|
|
4371
|
-
"outOfStockCount": number (optional)
|
|
4372
|
-
}
|
|
4373
|
-
|
|
4374
|
-
### \u{1F539} TABLE FORMAT
|
|
4375
|
-
- columns MUST match row keys
|
|
4376
|
-
- rows MUST be flat objects
|
|
4377
|
-
|
|
4378
|
-
---
|
|
4379
|
-
|
|
4380
|
-
## 5. DATA RULES
|
|
4381
|
-
|
|
4382
|
-
- NEVER hallucinate fields or data.
|
|
4383
|
-
- ONLY use data provided in the retrieved context.
|
|
4384
|
-
- STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
|
|
4385
|
-
- 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.
|
|
4386
|
-
- NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
|
|
4387
|
-
- NEVER explain YOUR formatting choice.
|
|
4388
|
-
- Aggregate BEFORE rendering chart.
|
|
4389
|
-
- DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
|
|
4390
|
-
|
|
4391
|
-
---
|
|
4392
|
-
|
|
4393
|
-
## 6. SMART RULES (IMPORTANT)
|
|
4394
|
-
|
|
4395
|
-
- If user asks BOTH:
|
|
4396
|
-
"show products AND distribution"
|
|
4397
|
-
\u2192 PRIORITIZE "explore" \u2192 carousel
|
|
4398
|
-
|
|
4399
|
-
- If dataset size > 20:
|
|
4400
|
-
\u2192 aggregate \u2192 chart or table
|
|
4401
|
-
|
|
4402
|
-
---
|
|
4947
|
+
### UI STYLE RULES (CRITICAL):
|
|
4948
|
+
- NEVER generate markdown tables. If you do, the UI will break.
|
|
4949
|
+
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
4950
|
+
- NEVER generate text-based charts or graphs.
|
|
4951
|
+
- ONLY use plain text and bullet points.
|
|
4403
4952
|
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
-
|
|
4407
|
-
-
|
|
4408
|
-
- FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
|
|
4409
|
-
- NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
|
|
4410
|
-
- SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
|
|
4411
|
-
- TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
|
|
4412
|
-
|
|
4413
|
-
---
|
|
4414
|
-
|
|
4415
|
-
## 8. EXAMPLES
|
|
4416
|
-
|
|
4417
|
-
User: "show me beauty products"
|
|
4418
|
-
|
|
4419
|
-
\`\`\`ui
|
|
4420
|
-
{
|
|
4421
|
-
"view": "carousel",
|
|
4422
|
-
"title": "TITLE",
|
|
4423
|
-
"description": "DESCRIPTION",
|
|
4424
|
-
"metadata": {
|
|
4425
|
-
"intent": "explore",
|
|
4426
|
-
"confidence": 0.95
|
|
4427
|
-
},
|
|
4428
|
-
"carousel": {
|
|
4429
|
-
"items": [
|
|
4430
|
-
{
|
|
4431
|
-
"id": "ID",
|
|
4432
|
-
"name": "PRODUCT_NAME",
|
|
4433
|
-
"brand": "BRAND",
|
|
4434
|
-
"price": 0,
|
|
4435
|
-
"image": "URL",
|
|
4436
|
-
"link": "URL",
|
|
4437
|
-
"inStock": true
|
|
4438
|
-
}
|
|
4439
|
-
]
|
|
4440
|
-
},
|
|
4441
|
-
"insights": ["INSIGHT"]
|
|
4442
|
-
}
|
|
4443
|
-
\`\`\`
|
|
4444
|
-
|
|
4445
|
-
---
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
\`\`\`ui
|
|
4450
|
-
{
|
|
4451
|
-
"view": "chart",
|
|
4452
|
-
"title": "TITLE",
|
|
4453
|
-
"description": "DESCRIPTION",
|
|
4454
|
-
"metadata": {
|
|
4455
|
-
"intent": "analyze",
|
|
4456
|
-
"confidence": 0.98
|
|
4457
|
-
},
|
|
4458
|
-
"chart": {
|
|
4459
|
-
"type": "pie",
|
|
4460
|
-
"data": [
|
|
4461
|
-
{ "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
|
|
4462
|
-
{ "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
|
|
4463
|
-
]
|
|
4464
|
-
},
|
|
4465
|
-
"insights": ["INSIGHT"]
|
|
4466
|
-
}
|
|
4467
|
-
\`\`\`
|
|
4953
|
+
### PRODUCT DISPLAY:
|
|
4954
|
+
- When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
|
|
4955
|
+
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
4956
|
+
- Do NOT try to format product lists as tables.
|
|
4468
4957
|
`;
|
|
4469
|
-
|
|
4470
|
-
let cleanPrompt = this.config.llm.systemPrompt || "";
|
|
4471
|
-
cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, "");
|
|
4472
|
-
cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, "");
|
|
4473
|
-
this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
|
|
4474
|
-
}
|
|
4475
|
-
console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
|
|
4958
|
+
this.config.llm.systemPrompt = chartInstruction;
|
|
4476
4959
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
4477
4960
|
const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
|
|
4478
4961
|
this.config.llm,
|
|
@@ -4486,7 +4969,7 @@ User: "show me beauty products"
|
|
|
4486
4969
|
this.entityExtractor = new EntityExtractor(this.llmProvider);
|
|
4487
4970
|
}
|
|
4488
4971
|
await this.vectorDB.initialize();
|
|
4489
|
-
if (((
|
|
4972
|
+
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic") {
|
|
4490
4973
|
this.agent = new LangChainAgent(this, this.config);
|
|
4491
4974
|
await this.agent.initialize(this.llmProvider);
|
|
4492
4975
|
}
|
|
@@ -4494,7 +4977,6 @@ User: "show me beauty products"
|
|
|
4494
4977
|
}
|
|
4495
4978
|
/**
|
|
4496
4979
|
* Ingest documents with automatic chunking, embedding, and batch upsert.
|
|
4497
|
-
* Handles retries for transient failures.
|
|
4498
4980
|
*/
|
|
4499
4981
|
async ingest(documents, namespace) {
|
|
4500
4982
|
await this.initialize();
|
|
@@ -4511,10 +4993,7 @@ User: "show me beauty products"
|
|
|
4511
4993
|
metadata: chunk.metadata
|
|
4512
4994
|
}));
|
|
4513
4995
|
const totalProcessed = await this.processUpserts(upsertDocs, ns);
|
|
4514
|
-
results.push({
|
|
4515
|
-
docId: doc.docId,
|
|
4516
|
-
chunksIngested: totalProcessed
|
|
4517
|
-
});
|
|
4996
|
+
results.push({ docId: doc.docId, chunksIngested: totalProcessed });
|
|
4518
4997
|
if (this.graphDB && this.entityExtractor) {
|
|
4519
4998
|
await this.processGraphIngestion(doc.docId, chunks);
|
|
4520
4999
|
}
|
|
@@ -4525,23 +5004,18 @@ User: "show me beauty products"
|
|
|
4525
5004
|
}
|
|
4526
5005
|
return results;
|
|
4527
5006
|
}
|
|
4528
|
-
/**
|
|
4529
|
-
* Step 1: Chunk the document content.
|
|
4530
|
-
*/
|
|
5007
|
+
/** Step 1: Chunk the document content. */
|
|
4531
5008
|
async prepareChunks(doc) {
|
|
4532
5009
|
var _a, _b;
|
|
4533
5010
|
if (this.llamaIngestor) {
|
|
4534
|
-
return
|
|
5011
|
+
return this.llamaIngestor.chunk(doc.content, {
|
|
4535
5012
|
docId: doc.docId,
|
|
4536
5013
|
metadata: doc.metadata,
|
|
4537
5014
|
chunkSize: (_a = this.config.rag) == null ? void 0 : _a.chunkSize,
|
|
4538
5015
|
chunkOverlap: (_b = this.config.rag) == null ? void 0 : _b.chunkOverlap
|
|
4539
5016
|
});
|
|
4540
5017
|
}
|
|
4541
|
-
return this.chunker.chunk(doc.content, {
|
|
4542
|
-
docId: doc.docId,
|
|
4543
|
-
metadata: doc.metadata
|
|
4544
|
-
});
|
|
5018
|
+
return this.chunker.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata });
|
|
4545
5019
|
}
|
|
4546
5020
|
/**
|
|
4547
5021
|
* Step 2: Generate embeddings for chunks with retry logic.
|
|
@@ -4565,9 +5039,7 @@ User: "show me beauty products"
|
|
|
4565
5039
|
}
|
|
4566
5040
|
return vectors;
|
|
4567
5041
|
}
|
|
4568
|
-
/**
|
|
4569
|
-
* Step 3: Upsert chunks to vector database with retry logic.
|
|
4570
|
-
*/
|
|
5042
|
+
/** Step 3: Upsert chunks to vector database with retry logic. */
|
|
4571
5043
|
async processUpserts(upsertDocs, namespace) {
|
|
4572
5044
|
const upsertBatchOptions = {
|
|
4573
5045
|
batchSize: 100,
|
|
@@ -4584,14 +5056,10 @@ User: "show me beauty products"
|
|
|
4584
5056
|
}
|
|
4585
5057
|
return upsertResult.totalProcessed;
|
|
4586
5058
|
}
|
|
4587
|
-
/**
|
|
4588
|
-
* Step 4: Optional graph-based entity extraction and ingestion.
|
|
4589
|
-
*/
|
|
5059
|
+
/** Step 4: Optional graph-based entity extraction and ingestion. */
|
|
4590
5060
|
async processGraphIngestion(docId, chunks) {
|
|
4591
|
-
console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
|
|
4592
5061
|
const extractionOptions = {
|
|
4593
5062
|
batchSize: 2,
|
|
4594
|
-
// Low concurrency for LLM extraction
|
|
4595
5063
|
maxRetries: 1,
|
|
4596
5064
|
initialDelayMs: 500
|
|
4597
5065
|
};
|
|
@@ -4604,7 +5072,7 @@ User: "show me beauty products"
|
|
|
4604
5072
|
if (nodes.length > 0) await this.graphDB.addNodes(nodes);
|
|
4605
5073
|
if (edges.length > 0) await this.graphDB.addEdges(edges);
|
|
4606
5074
|
} catch (err) {
|
|
4607
|
-
console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
|
|
5075
|
+
console.warn(`[Pipeline] Entity extraction failed for chunk in doc ${docId}:`, err);
|
|
4608
5076
|
}
|
|
4609
5077
|
}
|
|
4610
5078
|
},
|
|
@@ -4615,7 +5083,6 @@ User: "show me beauty products"
|
|
|
4615
5083
|
var _a;
|
|
4616
5084
|
await this.initialize();
|
|
4617
5085
|
if (((_a = this.config.rag) == null ? void 0 : _a.architecture) === "agentic" && this.agent) {
|
|
4618
|
-
console.log("[Pipeline] \u{1F916} Executing in Agentic Mode...");
|
|
4619
5086
|
const agentReply = await this.agent.run(question, history);
|
|
4620
5087
|
return { reply: agentReply, sources: [] };
|
|
4621
5088
|
}
|
|
@@ -4624,6 +5091,7 @@ User: "show me beauty products"
|
|
|
4624
5091
|
let sources = [];
|
|
4625
5092
|
let graphData;
|
|
4626
5093
|
let uiTransformation;
|
|
5094
|
+
let trace;
|
|
4627
5095
|
try {
|
|
4628
5096
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
4629
5097
|
const chunk = temp.value;
|
|
@@ -4633,6 +5101,7 @@ User: "show me beauty products"
|
|
|
4633
5101
|
if ("sources" in chunk) sources = chunk.sources;
|
|
4634
5102
|
if ("graphData" in chunk) graphData = chunk.graphData;
|
|
4635
5103
|
if ("ui_transformation" in chunk) uiTransformation = chunk.ui_transformation;
|
|
5104
|
+
if ("trace" in chunk) trace = chunk.trace;
|
|
4636
5105
|
}
|
|
4637
5106
|
}
|
|
4638
5107
|
} catch (temp) {
|
|
@@ -4645,38 +5114,47 @@ User: "show me beauty products"
|
|
|
4645
5114
|
throw error[0];
|
|
4646
5115
|
}
|
|
4647
5116
|
}
|
|
4648
|
-
return { reply, sources, graphData, ui_transformation: uiTransformation };
|
|
5117
|
+
return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
|
|
4649
5118
|
}
|
|
4650
5119
|
/**
|
|
4651
5120
|
* High-performance streaming RAG flow.
|
|
4652
|
-
* Yields text chunks first, then the retrieval metadata at the end.
|
|
5121
|
+
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
4653
5122
|
*/
|
|
4654
5123
|
askStream(_0) {
|
|
4655
5124
|
return __asyncGenerator(this, arguments, function* (question, history = [], namespace) {
|
|
4656
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
5125
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
4657
5126
|
yield new __await(this.initialize());
|
|
4658
5127
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
4659
5128
|
const topK = (_b = (_a = this.config.rag) == null ? void 0 : _a.topK) != null ? _b : 5;
|
|
4660
5129
|
const scoreThreshold = (_d = (_c = this.config.rag) == null ? void 0 : _c.scoreThreshold) != null ? _d : 0;
|
|
5130
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5131
|
+
const requestStart = performance.now();
|
|
4661
5132
|
try {
|
|
4662
5133
|
let searchQuery = question;
|
|
5134
|
+
let rewrittenQuery;
|
|
4663
5135
|
if ((_e = this.config.rag) == null ? void 0 : _e.useQueryTransformation) {
|
|
4664
5136
|
searchQuery = yield new __await(this.rewriteQuery(question, history));
|
|
5137
|
+
rewrittenQuery = searchQuery !== question ? searchQuery : void 0;
|
|
4665
5138
|
}
|
|
4666
5139
|
const hints = QueryProcessor.extractQueryFieldHints(question, (_f = this.config.rag) == null ? void 0 : _f.filterableFields);
|
|
4667
5140
|
const filter = QueryProcessor.buildQueryFilter(question, hints);
|
|
4668
|
-
|
|
5141
|
+
const embedStart = performance.now();
|
|
4669
5142
|
const { sources: rawSources, graphData } = yield new __await(this.retrieve(searchQuery, {
|
|
4670
5143
|
namespace: ns,
|
|
4671
5144
|
topK: topK * 2,
|
|
4672
5145
|
filter
|
|
4673
5146
|
}));
|
|
5147
|
+
const retrieveEnd = performance.now();
|
|
5148
|
+
const embedMs = retrieveEnd - embedStart;
|
|
5149
|
+
const retrieveMs = retrieveEnd - embedStart;
|
|
5150
|
+
const rerankStart = performance.now();
|
|
4674
5151
|
let sources = rawSources.filter((m) => m.score >= scoreThreshold);
|
|
4675
5152
|
if ((_g = this.config.rag) == null ? void 0 : _g.useReranking) {
|
|
4676
5153
|
sources = yield new __await(this.reranker.rerank(sources, question, topK));
|
|
4677
5154
|
} else {
|
|
4678
5155
|
sources = sources.slice(0, topK);
|
|
4679
5156
|
}
|
|
5157
|
+
const rerankMs = performance.now() - rerankStart;
|
|
4680
5158
|
let context = sources.length ? sources.map((m, i) => `[Source ${i + 1}]
|
|
4681
5159
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
4682
5160
|
if (graphData && graphData.nodes.length > 0) {
|
|
@@ -4689,7 +5167,16 @@ ${graphContext}
|
|
|
4689
5167
|
VECTOR CONTEXT:
|
|
4690
5168
|
${context}`;
|
|
4691
5169
|
}
|
|
4692
|
-
const
|
|
5170
|
+
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
5171
|
+
const trainingPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys) : Promise.resolve(void 0);
|
|
5172
|
+
const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
|
|
5173
|
+
const hardenedHistory = [...history];
|
|
5174
|
+
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
5175
|
+
const messages = [...hardenedHistory, userQuestion];
|
|
5176
|
+
const systemPrompt = (_h = this.config.llm.systemPrompt) != null ? _h : "";
|
|
5177
|
+
const userPrompt = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
|
|
5178
|
+
let fullReply = "";
|
|
5179
|
+
const generateStart = performance.now();
|
|
4693
5180
|
if (this.llmProvider.chatStream) {
|
|
4694
5181
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
4695
5182
|
if (!stream) {
|
|
@@ -4698,6 +5185,7 @@ ${context}`;
|
|
|
4698
5185
|
try {
|
|
4699
5186
|
for (var iter = __forAwait(stream), more, temp, error; more = !(temp = yield new __await(iter.next())).done; more = false) {
|
|
4700
5187
|
const chunk = temp.value;
|
|
5188
|
+
fullReply += chunk;
|
|
4701
5189
|
yield chunk;
|
|
4702
5190
|
}
|
|
4703
5191
|
} catch (temp) {
|
|
@@ -4712,14 +5200,58 @@ ${context}`;
|
|
|
4712
5200
|
}
|
|
4713
5201
|
} else {
|
|
4714
5202
|
const reply = yield new __await(this.llmProvider.chat(messages, context));
|
|
5203
|
+
fullReply = reply;
|
|
4715
5204
|
yield reply;
|
|
4716
5205
|
}
|
|
4717
|
-
const
|
|
5206
|
+
const generateMs = performance.now() - generateStart;
|
|
5207
|
+
const totalMs = performance.now() - requestStart;
|
|
5208
|
+
const latency = {
|
|
5209
|
+
embedMs: Math.round(embedMs),
|
|
5210
|
+
retrieveMs: Math.round(retrieveMs),
|
|
5211
|
+
rerankMs: ((_i = this.config.rag) == null ? void 0 : _i.useReranking) ? Math.round(rerankMs) : void 0,
|
|
5212
|
+
generateMs: Math.round(generateMs),
|
|
5213
|
+
totalMs: Math.round(totalMs)
|
|
5214
|
+
};
|
|
5215
|
+
const promptText = systemPrompt + "\n" + context + "\n" + userPrompt;
|
|
5216
|
+
const promptTokens = estimateTokens(promptText);
|
|
5217
|
+
const completionTokens = estimateTokens(fullReply);
|
|
5218
|
+
const tokens = {
|
|
5219
|
+
promptTokens,
|
|
5220
|
+
completionTokens,
|
|
5221
|
+
totalTokens: promptTokens + completionTokens,
|
|
5222
|
+
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model)
|
|
5223
|
+
};
|
|
5224
|
+
const hallucinationResult = yield new __await(scoreHallucination(this.llmProvider, fullReply, context).catch(() => void 0));
|
|
5225
|
+
const trainedSchema = yield new __await(trainingPromise);
|
|
5226
|
+
const uiTransformation = yield new __await(this.generateUiTransformation(question, sources, context, trainedSchema));
|
|
5227
|
+
const trace = {
|
|
5228
|
+
requestId,
|
|
5229
|
+
query: question,
|
|
5230
|
+
rewrittenQuery,
|
|
5231
|
+
systemPrompt,
|
|
5232
|
+
userPrompt: question + restrictionSuffix,
|
|
5233
|
+
chunks: sources.map((s) => {
|
|
5234
|
+
var _a2;
|
|
5235
|
+
return {
|
|
5236
|
+
id: s.id,
|
|
5237
|
+
score: s.score,
|
|
5238
|
+
content: s.content,
|
|
5239
|
+
metadata: (_a2 = s.metadata) != null ? _a2 : {},
|
|
5240
|
+
namespace: ns
|
|
5241
|
+
};
|
|
5242
|
+
}),
|
|
5243
|
+
latency,
|
|
5244
|
+
tokens,
|
|
5245
|
+
hallucinationScore: hallucinationResult == null ? void 0 : hallucinationResult.score,
|
|
5246
|
+
hallucinationReason: hallucinationResult == null ? void 0 : hallucinationResult.reason,
|
|
5247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5248
|
+
};
|
|
4718
5249
|
yield {
|
|
4719
5250
|
reply: "",
|
|
4720
5251
|
sources,
|
|
4721
5252
|
graphData,
|
|
4722
|
-
ui_transformation: uiTransformation
|
|
5253
|
+
ui_transformation: uiTransformation,
|
|
5254
|
+
trace
|
|
4723
5255
|
};
|
|
4724
5256
|
} catch (error2) {
|
|
4725
5257
|
throw new Error(`[Pipeline] Stream failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
@@ -4730,6 +5262,17 @@ ${context}`;
|
|
|
4730
5262
|
* Universal retrieval method combining all enabled providers.
|
|
4731
5263
|
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
4732
5264
|
*/
|
|
5265
|
+
async generateUiTransformation(question, sources, _context, trainedSchema) {
|
|
5266
|
+
if (!sources || sources.length === 0) {
|
|
5267
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5268
|
+
}
|
|
5269
|
+
try {
|
|
5270
|
+
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
5271
|
+
} catch (err) {
|
|
5272
|
+
console.warn("[Pipeline] generateUiTransformation failed, using heuristic fallback:", err);
|
|
5273
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
4733
5276
|
async retrieve(query, options) {
|
|
4734
5277
|
var _a, _b, _c;
|
|
4735
5278
|
const ns = (_a = options.namespace) != null ? _a : this.config.projectId;
|
|
@@ -4747,12 +5290,9 @@ ${context}`;
|
|
|
4747
5290
|
const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
|
|
4748
5291
|
return { sources, graphData };
|
|
4749
5292
|
}
|
|
4750
|
-
/**
|
|
4751
|
-
* Rewrite the user query for better retrieval performance.
|
|
4752
|
-
*/
|
|
5293
|
+
/** Rewrite the user query for better retrieval performance. */
|
|
4753
5294
|
async rewriteQuery(question, history) {
|
|
4754
|
-
const prompt = `
|
|
4755
|
-
Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
5295
|
+
const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
4756
5296
|
Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
|
|
4757
5297
|
|
|
4758
5298
|
History:
|
|
@@ -4770,23 +5310,16 @@ Optimized Search Query:`;
|
|
|
4770
5310
|
);
|
|
4771
5311
|
return rewrite.trim() || question;
|
|
4772
5312
|
}
|
|
4773
|
-
/**
|
|
4774
|
-
* Generate 3-5 short, relevant questions based on the vector database content.
|
|
4775
|
-
*/
|
|
5313
|
+
/** Generate 3-5 short, relevant questions based on the vector database content. */
|
|
4776
5314
|
async getSuggestions(query, namespace) {
|
|
4777
|
-
if (!query || query.trim().length < 3)
|
|
4778
|
-
return [];
|
|
4779
|
-
}
|
|
5315
|
+
if (!query || query.trim().length < 3) return [];
|
|
4780
5316
|
await this.initialize();
|
|
4781
5317
|
const ns = namespace != null ? namespace : this.config.projectId;
|
|
4782
5318
|
try {
|
|
4783
5319
|
const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
|
|
4784
|
-
if (sources.length === 0)
|
|
4785
|
-
return [];
|
|
4786
|
-
}
|
|
5320
|
+
if (sources.length === 0) return [];
|
|
4787
5321
|
const context = sources.map((s) => s.content).join("\n\n---\n\n");
|
|
4788
|
-
const prompt = `
|
|
4789
|
-
Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
5322
|
+
const prompt = `Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
4790
5323
|
Focus on questions that can be answered by the context.
|
|
4791
5324
|
Keep each question under 10 words and make them very specific to the content.
|
|
4792
5325
|
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
@@ -4932,6 +5465,14 @@ var VectorPlugin = class {
|
|
|
4932
5465
|
getConfig() {
|
|
4933
5466
|
return this.config;
|
|
4934
5467
|
}
|
|
5468
|
+
/**
|
|
5469
|
+
* Get the initialized LLM provider (available after the first request).
|
|
5470
|
+
* Used by handlers to pass to UITransformer.analyzeAndDecide() for
|
|
5471
|
+
* LLM-driven visualization decisions.
|
|
5472
|
+
*/
|
|
5473
|
+
getLLMProvider() {
|
|
5474
|
+
return this.pipeline.getLLMProvider();
|
|
5475
|
+
}
|
|
4935
5476
|
/**
|
|
4936
5477
|
* Perform pre-flight health checks on all configured providers.
|
|
4937
5478
|
* Useful to verify connectivity before running operations.
|
|
@@ -5211,199 +5752,189 @@ var DocumentParser = class {
|
|
|
5211
5752
|
// src/server.ts
|
|
5212
5753
|
init_BaseVectorProvider();
|
|
5213
5754
|
init_PineconeProvider();
|
|
5214
|
-
init_PostgreSQLProvider();
|
|
5215
5755
|
|
|
5216
|
-
// src/providers/vectordb/
|
|
5756
|
+
// src/providers/vectordb/PostgreSQLProvider.ts
|
|
5217
5757
|
var import_pg2 = require("pg");
|
|
5218
5758
|
init_BaseVectorProvider();
|
|
5219
|
-
var
|
|
5759
|
+
var PostgreSQLProvider = class extends BaseVectorProvider {
|
|
5220
5760
|
constructor(config) {
|
|
5221
|
-
var _a
|
|
5761
|
+
var _a;
|
|
5222
5762
|
super(config);
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
}
|
|
5763
|
+
this.tableName = this.indexName.replace(/[^a-z0-9_]/gi, "_");
|
|
5764
|
+
const opts = config.options;
|
|
5765
|
+
if (!opts.connectionString) throw new Error("[PostgreSQLProvider] options.connectionString is required");
|
|
5227
5766
|
this.connectionString = opts.connectionString;
|
|
5228
|
-
this.dimensions = (_a = opts.dimensions) != null ? _a :
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5767
|
+
this.dimensions = (_a = opts.dimensions) != null ? _a : 1536;
|
|
5768
|
+
}
|
|
5769
|
+
static getValidator() {
|
|
5770
|
+
return {
|
|
5771
|
+
validate(config) {
|
|
5772
|
+
const errors = [];
|
|
5773
|
+
const opts = config.options || {};
|
|
5774
|
+
if (!opts.connectionString) {
|
|
5775
|
+
errors.push({
|
|
5776
|
+
field: "vectorDb.options.connectionString",
|
|
5777
|
+
message: "PostgreSQL connection string is required",
|
|
5778
|
+
suggestion: "Set PGVECTOR_CONNECTION_STRING environment variable",
|
|
5779
|
+
severity: "error"
|
|
5780
|
+
});
|
|
5781
|
+
}
|
|
5782
|
+
if (opts.tables && typeof opts.tables !== "string" && !Array.isArray(opts.tables)) {
|
|
5783
|
+
errors.push({
|
|
5784
|
+
field: "vectorDb.options.tables",
|
|
5785
|
+
message: "PostgreSQL tables must be a string or a string array",
|
|
5786
|
+
severity: "error"
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
return errors;
|
|
5790
|
+
}
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
static getHealthChecker() {
|
|
5794
|
+
return {
|
|
5795
|
+
async check(config) {
|
|
5796
|
+
const opts = config.options || {};
|
|
5797
|
+
const timestamp = Date.now();
|
|
5798
|
+
try {
|
|
5799
|
+
const { Client } = await import("pg");
|
|
5800
|
+
const client = new Client({ connectionString: opts.connectionString });
|
|
5801
|
+
await client.connect();
|
|
5802
|
+
const result = await client.query(`
|
|
5803
|
+
SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector');
|
|
5804
|
+
`);
|
|
5805
|
+
const hasVector = result.rows[0].exists;
|
|
5806
|
+
await client.end();
|
|
5807
|
+
return {
|
|
5808
|
+
healthy: true,
|
|
5809
|
+
provider: "postgresql",
|
|
5810
|
+
capabilities: { pgvectorInstalled: hasVector },
|
|
5811
|
+
timestamp
|
|
5812
|
+
};
|
|
5813
|
+
} catch (error) {
|
|
5814
|
+
return {
|
|
5815
|
+
healthy: false,
|
|
5816
|
+
provider: "postgresql",
|
|
5817
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
5818
|
+
timestamp
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
};
|
|
5238
5823
|
}
|
|
5239
5824
|
async initialize() {
|
|
5240
5825
|
this.pool = new import_pg2.Pool({ connectionString: this.connectionString });
|
|
5241
5826
|
const client = await this.pool.connect();
|
|
5242
5827
|
try {
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5828
|
+
await client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
5829
|
+
await client.query(`
|
|
5830
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
5831
|
+
id TEXT PRIMARY KEY,
|
|
5832
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
5833
|
+
content TEXT NOT NULL,
|
|
5834
|
+
metadata JSONB,
|
|
5835
|
+
embedding VECTOR(${this.dimensions})
|
|
5836
|
+
)
|
|
5837
|
+
`);
|
|
5838
|
+
await client.query(`
|
|
5839
|
+
CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
|
|
5840
|
+
ON ${this.tableName}
|
|
5841
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
5248
5842
|
`);
|
|
5249
|
-
const discoveredTables = res.rows.map((r) => r.table_name);
|
|
5250
|
-
if (this.tables.length === 0) {
|
|
5251
|
-
this.tables = discoveredTables;
|
|
5252
|
-
} else {
|
|
5253
|
-
const staticTables = [...this.tables];
|
|
5254
|
-
this.tables = staticTables.filter((t) => discoveredTables.includes(t));
|
|
5255
|
-
if (this.tables.length < staticTables.length) {
|
|
5256
|
-
const missing = staticTables.filter((t) => !discoveredTables.includes(t));
|
|
5257
|
-
console.warn(`[MultiTablePostgresProvider] Some configured tables were skipped (missing 'embedding' column): ${missing.join(", ")}`);
|
|
5258
|
-
}
|
|
5259
|
-
}
|
|
5260
|
-
if (this.tables.length === 0) {
|
|
5261
|
-
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
5262
|
-
} else {
|
|
5263
|
-
console.log(
|
|
5264
|
-
`[MultiTablePostgresProvider] Connected. Searching across ${this.tables.length} table(s): ${this.tables.join(", ")}`
|
|
5265
|
-
);
|
|
5266
|
-
}
|
|
5267
5843
|
} finally {
|
|
5268
5844
|
client.release();
|
|
5269
5845
|
}
|
|
5270
5846
|
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
async batchUpsert(_docs, _namespace) {
|
|
5284
|
-
throw new Error(
|
|
5285
|
-
"[MultiTablePostgresProvider] batchUpsert() is not supported in multi-table mode."
|
|
5847
|
+
async upsert(doc, namespace = "") {
|
|
5848
|
+
var _a;
|
|
5849
|
+
const vectorLiteral = `[${doc.vector.join(",")}]`;
|
|
5850
|
+
await this.pool.query(
|
|
5851
|
+
`INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
5852
|
+
VALUES ($1, $2, $3, $4, $5::vector)
|
|
5853
|
+
ON CONFLICT (id) DO UPDATE
|
|
5854
|
+
SET namespace = EXCLUDED.namespace,
|
|
5855
|
+
content = EXCLUDED.content,
|
|
5856
|
+
metadata = EXCLUDED.metadata,
|
|
5857
|
+
embedding = EXCLUDED.embedding`,
|
|
5858
|
+
[doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), vectorLiteral]
|
|
5286
5859
|
);
|
|
5287
5860
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
};
|
|
5314
|
-
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
5315
|
-
const queryPromises = this.tables.map(async (table) => {
|
|
5316
|
-
try {
|
|
5317
|
-
let sqlQuery = "";
|
|
5318
|
-
let params = [];
|
|
5319
|
-
if (queryText) {
|
|
5320
|
-
const hasEntityHints = entityHints.length > 0;
|
|
5321
|
-
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
5322
|
-
SELECT COALESCE(MAX(
|
|
5323
|
-
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(", ")})
|
|
5324
|
-
THEN 1.0 ELSE 0.0 END
|
|
5325
|
-
), 0)
|
|
5326
|
-
FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
|
|
5327
|
-
WHERE key IN (${this.searchFields.map((f) => `'${f}'`).join(", ")})
|
|
5328
|
-
) * 3.0` : "";
|
|
5329
|
-
sqlQuery = `
|
|
5330
|
-
SELECT *,
|
|
5331
|
-
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
5332
|
-
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
5333
|
-
((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
|
|
5334
|
-
FROM "${table}" t
|
|
5335
|
-
ORDER BY hybrid_score DESC
|
|
5336
|
-
LIMIT 50
|
|
5337
|
-
`;
|
|
5338
|
-
params = [vectorLiteral, dynamicKeywordQuery];
|
|
5339
|
-
} else {
|
|
5340
|
-
sqlQuery = `
|
|
5341
|
-
SELECT *,
|
|
5342
|
-
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
5343
|
-
FROM "${table}" t
|
|
5344
|
-
ORDER BY hybrid_score DESC
|
|
5345
|
-
LIMIT 50
|
|
5346
|
-
`;
|
|
5347
|
-
params = [vectorLiteral];
|
|
5348
|
-
}
|
|
5349
|
-
const result = await this.pool.query(sqlQuery, params);
|
|
5350
|
-
if (result.rowCount && result.rowCount > 0) {
|
|
5351
|
-
console.log(` \u2705 Table "${table}": Found ${result.rowCount} potential matches. Top score: ${result.rows[0].hybrid_score.toFixed(4)}`);
|
|
5352
|
-
} else {
|
|
5353
|
-
console.log(` \u26AA Table "${table}": No relevant matches found.`);
|
|
5354
|
-
}
|
|
5355
|
-
const tableResults = [];
|
|
5356
|
-
for (const row of result.rows) {
|
|
5357
|
-
const _a2 = row, { hybrid_score, id } = _a2, rest = __objRest(_a2, ["hybrid_score", "id"]);
|
|
5358
|
-
delete rest.embedding;
|
|
5359
|
-
delete rest.vector_score;
|
|
5360
|
-
delete rest.keyword_score;
|
|
5361
|
-
delete rest.exact_name_score;
|
|
5362
|
-
const content = `[TYPE: ${table.replace(/s$/, "").toUpperCase()}]
|
|
5363
|
-
` + Object.entries(rest).filter(([k, v]) => v !== null && typeof v !== "object" && k !== "id").map(([k, v]) => `${k}: ${v}`).join("\n");
|
|
5364
|
-
tableResults.push({
|
|
5365
|
-
id: `${table}-${id}`,
|
|
5366
|
-
score: parseFloat(String(hybrid_score)),
|
|
5367
|
-
content,
|
|
5368
|
-
metadata: __spreadProps(__spreadValues({}, rest), { source_table: table })
|
|
5369
|
-
});
|
|
5370
|
-
}
|
|
5371
|
-
return tableResults;
|
|
5372
|
-
} catch (err) {
|
|
5373
|
-
console.error(
|
|
5374
|
-
`[MultiTablePostgresProvider] Error querying table "${table}":`,
|
|
5375
|
-
err.message
|
|
5376
|
-
);
|
|
5377
|
-
return [];
|
|
5861
|
+
async batchUpsert(docs, namespace = "") {
|
|
5862
|
+
if (docs.length === 0) return;
|
|
5863
|
+
const client = await this.pool.connect();
|
|
5864
|
+
try {
|
|
5865
|
+
await client.query("BEGIN");
|
|
5866
|
+
const BATCH_SIZE = 50;
|
|
5867
|
+
for (let i = 0; i < docs.length; i += BATCH_SIZE) {
|
|
5868
|
+
const batch = docs.slice(i, i + BATCH_SIZE);
|
|
5869
|
+
const values = [];
|
|
5870
|
+
const valuePlaceholders = batch.map((doc, idx) => {
|
|
5871
|
+
var _a;
|
|
5872
|
+
const offset = idx * 5;
|
|
5873
|
+
values.push(doc.id, namespace, doc.content, JSON.stringify((_a = doc.metadata) != null ? _a : {}), `[${doc.vector.join(",")}]`);
|
|
5874
|
+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}::vector)`;
|
|
5875
|
+
}).join(", ");
|
|
5876
|
+
const query = `
|
|
5877
|
+
INSERT INTO ${this.tableName} (id, namespace, content, metadata, embedding)
|
|
5878
|
+
VALUES ${valuePlaceholders}
|
|
5879
|
+
ON CONFLICT (id) DO UPDATE
|
|
5880
|
+
SET namespace = EXCLUDED.namespace,
|
|
5881
|
+
content = EXCLUDED.content,
|
|
5882
|
+
metadata = EXCLUDED.metadata,
|
|
5883
|
+
embedding = EXCLUDED.embedding
|
|
5884
|
+
`;
|
|
5885
|
+
await client.query(query, values);
|
|
5378
5886
|
}
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
}
|
|
5384
|
-
|
|
5385
|
-
for (const res of allResults) {
|
|
5386
|
-
const table = (_a = res.metadata) == null ? void 0 : _a.source_table;
|
|
5387
|
-
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
5388
|
-
resultsByTable[table].push(res);
|
|
5887
|
+
await client.query("COMMIT");
|
|
5888
|
+
} catch (error) {
|
|
5889
|
+
await client.query("ROLLBACK");
|
|
5890
|
+
throw error;
|
|
5891
|
+
} finally {
|
|
5892
|
+
client.release();
|
|
5389
5893
|
}
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5894
|
+
}
|
|
5895
|
+
async query(vector, topK, namespace, filter) {
|
|
5896
|
+
const vectorLiteral = `[${vector.join(",")}]`;
|
|
5897
|
+
let whereClause = namespace ? `WHERE namespace = $3` : "";
|
|
5898
|
+
const params = [vectorLiteral, topK];
|
|
5899
|
+
if (namespace) params.push(namespace);
|
|
5900
|
+
const publicFilter = this.sanitizeFilter(filter);
|
|
5901
|
+
if (Object.keys(publicFilter).length > 0) {
|
|
5902
|
+
const filterConditions = Object.entries(publicFilter).map(([key, val]) => {
|
|
5903
|
+
const paramIdx = params.length + 1;
|
|
5904
|
+
params.push(JSON.stringify(val));
|
|
5905
|
+
return `metadata->>'${key}' = $${paramIdx}`;
|
|
5906
|
+
}).join(" AND ");
|
|
5907
|
+
whereClause = whereClause ? `${whereClause} AND ${filterConditions}` : `WHERE ${filterConditions}`;
|
|
5394
5908
|
}
|
|
5395
|
-
const
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5909
|
+
const client = await this.pool.connect();
|
|
5910
|
+
try {
|
|
5911
|
+
const efSearch = this.config.options.efSearch || Math.max(topK * 10, 40);
|
|
5912
|
+
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
5913
|
+
const result = await client.query(
|
|
5914
|
+
`SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
|
|
5915
|
+
FROM ${this.tableName}
|
|
5916
|
+
${whereClause}
|
|
5917
|
+
ORDER BY embedding <=> $1::vector
|
|
5918
|
+
LIMIT $2`,
|
|
5919
|
+
params
|
|
5920
|
+
);
|
|
5921
|
+
return result.rows.map((row) => ({
|
|
5922
|
+
id: String(row["id"]),
|
|
5923
|
+
score: parseFloat(String(row["score"])),
|
|
5924
|
+
content: String(row["content"]),
|
|
5925
|
+
metadata: row["metadata"]
|
|
5926
|
+
}));
|
|
5927
|
+
} finally {
|
|
5928
|
+
client.release();
|
|
5399
5929
|
}
|
|
5400
|
-
return finalSorted.slice(0, Math.max(topK, 15));
|
|
5401
5930
|
}
|
|
5402
|
-
async delete(
|
|
5403
|
-
|
|
5931
|
+
async delete(id, namespace) {
|
|
5932
|
+
const where = namespace ? "WHERE id = $1 AND namespace = $2" : "WHERE id = $1";
|
|
5933
|
+
const params = namespace ? [id, namespace] : [id];
|
|
5934
|
+
await this.pool.query(`DELETE FROM ${this.tableName} ${where}`, params);
|
|
5404
5935
|
}
|
|
5405
|
-
async deleteNamespace(
|
|
5406
|
-
|
|
5936
|
+
async deleteNamespace(namespace) {
|
|
5937
|
+
await this.pool.query(`DELETE FROM ${this.tableName} WHERE namespace = $1`, [namespace]);
|
|
5407
5938
|
}
|
|
5408
5939
|
async ping() {
|
|
5409
5940
|
try {
|
|
@@ -5414,13 +5945,12 @@ var MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
|
5414
5945
|
}
|
|
5415
5946
|
}
|
|
5416
5947
|
async disconnect() {
|
|
5417
|
-
|
|
5418
|
-
await this.pool.end();
|
|
5419
|
-
}
|
|
5948
|
+
await this.pool.end();
|
|
5420
5949
|
}
|
|
5421
5950
|
};
|
|
5422
5951
|
|
|
5423
5952
|
// src/server.ts
|
|
5953
|
+
init_MultiTablePostgresProvider();
|
|
5424
5954
|
init_MongoDBProvider();
|
|
5425
5955
|
init_MilvusProvider();
|
|
5426
5956
|
init_QdrantProvider();
|
|
@@ -5447,7 +5977,12 @@ function sseMetaFrame(meta) {
|
|
|
5447
5977
|
`;
|
|
5448
5978
|
}
|
|
5449
5979
|
function sseUIFrame(uiTransformation) {
|
|
5450
|
-
return `data: ${JSON.stringify(
|
|
5980
|
+
return `data: ${JSON.stringify({ type: "ui_transformation", data: uiTransformation })}
|
|
5981
|
+
|
|
5982
|
+
`;
|
|
5983
|
+
}
|
|
5984
|
+
function sseObservabilityFrame(trace) {
|
|
5985
|
+
return `data: ${JSON.stringify({ type: "observability", data: trace })}
|
|
5451
5986
|
|
|
5452
5987
|
`;
|
|
5453
5988
|
}
|
|
@@ -5502,6 +6037,7 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5502
6037
|
const encoder = new TextEncoder();
|
|
5503
6038
|
const stream = new ReadableStream({
|
|
5504
6039
|
async start(controller) {
|
|
6040
|
+
var _a, _b;
|
|
5505
6041
|
const enqueue = (text) => controller.enqueue(encoder.encode(text));
|
|
5506
6042
|
try {
|
|
5507
6043
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
@@ -5512,13 +6048,25 @@ function createStreamHandler(configOrPlugin) {
|
|
|
5512
6048
|
enqueue(sseTextFrame(chunk));
|
|
5513
6049
|
} else {
|
|
5514
6050
|
enqueue(sseMetaFrame(chunk));
|
|
5515
|
-
const
|
|
5516
|
-
|
|
6051
|
+
const responseChunk = chunk;
|
|
6052
|
+
const sources = (responseChunk == null ? void 0 : responseChunk.sources) || [];
|
|
6053
|
+
if (responseChunk == null ? void 0 : responseChunk.trace) {
|
|
6054
|
+
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
6055
|
+
}
|
|
6056
|
+
if (sources.length > 0) {
|
|
5517
6057
|
try {
|
|
5518
|
-
const
|
|
5519
|
-
|
|
6058
|
+
const llmProvider = (_a = plugin.getLLMProvider) == null ? void 0 : _a.call(plugin);
|
|
6059
|
+
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());
|
|
6060
|
+
if (uiTransformation) {
|
|
6061
|
+
enqueue(sseUIFrame(uiTransformation));
|
|
6062
|
+
}
|
|
5520
6063
|
} catch (transformError) {
|
|
5521
6064
|
console.warn("[createStreamHandler] UI transformation warning:", transformError);
|
|
6065
|
+
try {
|
|
6066
|
+
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
6067
|
+
if (fallback) enqueue(sseUIFrame(fallback));
|
|
6068
|
+
} catch (e) {
|
|
6069
|
+
}
|
|
5522
6070
|
}
|
|
5523
6071
|
}
|
|
5524
6072
|
}
|
|
@@ -5586,24 +6134,64 @@ function createUploadHandler(configOrPlugin) {
|
|
|
5586
6134
|
const formData = await req.formData();
|
|
5587
6135
|
const files = formData.getAll("files");
|
|
5588
6136
|
const namespace = formData.get("namespace") || void 0;
|
|
6137
|
+
const dimensionRaw = formData.get("dimension");
|
|
6138
|
+
const dimension = dimensionRaw ? parseInt(dimensionRaw, 10) : void 0;
|
|
5589
6139
|
if (!files || files.length === 0) {
|
|
5590
6140
|
return import_server.NextResponse.json({ error: "No files provided" }, { status: 400 });
|
|
5591
6141
|
}
|
|
5592
|
-
const documents =
|
|
5593
|
-
|
|
6142
|
+
const documents = [];
|
|
6143
|
+
for (const file of files) {
|
|
6144
|
+
if (file.name.toLowerCase().endsWith(".csv") || file.type === "text/csv") {
|
|
6145
|
+
const text = await file.text();
|
|
6146
|
+
const Papa = await import("papaparse");
|
|
6147
|
+
const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
6148
|
+
if (parsed.data && parsed.data.length > 0) {
|
|
6149
|
+
let i = 0;
|
|
6150
|
+
let lastRowData = null;
|
|
6151
|
+
for (const row of parsed.data) {
|
|
6152
|
+
i++;
|
|
6153
|
+
const rowData = row;
|
|
6154
|
+
const groupingKey = Object.keys(rowData)[0];
|
|
6155
|
+
if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
|
|
6156
|
+
for (const key of Object.keys(rowData)) {
|
|
6157
|
+
if (!rowData[key] && lastRowData[key]) {
|
|
6158
|
+
rowData[key] = lastRowData[key];
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
lastRowData = __spreadValues({}, rowData);
|
|
6163
|
+
const contentParts = [];
|
|
6164
|
+
for (const [key, val] of Object.entries(rowData)) {
|
|
6165
|
+
if (key && val) {
|
|
6166
|
+
contentParts.push(`${key}: ${val}`);
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
documents.push({
|
|
6170
|
+
docId: `${file.name}-row-${i}`,
|
|
6171
|
+
content: contentParts.join(", "),
|
|
6172
|
+
metadata: __spreadValues(__spreadValues({
|
|
6173
|
+
fileName: file.name,
|
|
6174
|
+
fileSize: file.size,
|
|
6175
|
+
fileType: file.type,
|
|
6176
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6177
|
+
}, dimension ? { dimension } : {}), rowData)
|
|
6178
|
+
});
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
} else {
|
|
5594
6182
|
const content = await DocumentParser.parse(file, file.name, file.type);
|
|
5595
|
-
|
|
6183
|
+
documents.push({
|
|
5596
6184
|
docId: file.name,
|
|
5597
6185
|
content,
|
|
5598
|
-
metadata: {
|
|
6186
|
+
metadata: __spreadValues({
|
|
5599
6187
|
fileName: file.name,
|
|
5600
6188
|
fileSize: file.size,
|
|
5601
6189
|
fileType: file.type,
|
|
5602
6190
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5603
|
-
}
|
|
5604
|
-
};
|
|
5605
|
-
}
|
|
5606
|
-
|
|
6191
|
+
}, dimension ? { dimension } : {})
|
|
6192
|
+
});
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
5607
6195
|
const results = await plugin.ingest(documents, namespace);
|
|
5608
6196
|
return import_server.NextResponse.json({ message: "Upload successful", results });
|
|
5609
6197
|
} catch (err) {
|