@retrivora-ai/rag-engine 1.9.1 → 1.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ILLMProvider-BQyKZLnd.d.mts → ILLMProvider-Bw2A28nU.d.mts} +2 -0
- package/dist/{ILLMProvider-BQyKZLnd.d.ts → ILLMProvider-Bw2A28nU.d.ts} +2 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +193 -109
- package/dist/handlers/index.mjs +193 -109
- package/dist/{index-CDftK3qs.d.mts → index-B70ZLkfG.d.mts} +1 -1
- package/dist/{index-A0GqPsaG.d.ts → index-DVu-mkAM.d.ts} +1 -1
- package/dist/index.css +57 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +90 -36
- package/dist/index.mjs +91 -37
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/server.js +193 -109
- package/dist/server.mjs +193 -109
- package/package.json +1 -1
- package/src/components/ChatWindow.tsx +24 -14
- package/src/components/MessageBubble.tsx +41 -6
- package/src/components/ProductCard.tsx +4 -4
- package/src/core/Pipeline.ts +53 -25
- package/src/core/QueryProcessor.ts +10 -9
- package/src/handlers/index.ts +31 -8
- package/src/hooks/useRagChat.ts +24 -0
- package/src/providers/vectordb/MongoDBProvider.ts +13 -6
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +36 -4
- package/src/types/chat.ts +2 -0
- package/src/utils/SchemaMapper.ts +6 -4
- package/src/utils/UITransformer.ts +2 -1
- package/src/utils/synonyms.ts +5 -5
package/dist/server.mjs
CHANGED
|
@@ -519,6 +519,82 @@ var init_PostgreSQLProvider = __esm({
|
|
|
519
519
|
}
|
|
520
520
|
});
|
|
521
521
|
|
|
522
|
+
// src/utils/synonyms.ts
|
|
523
|
+
function resolveMetadataValue(meta, uiKey) {
|
|
524
|
+
var _a;
|
|
525
|
+
const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
|
|
526
|
+
const keys = Object.keys(meta);
|
|
527
|
+
const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
|
|
528
|
+
let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
|
|
529
|
+
if (match !== void 0) return meta[match];
|
|
530
|
+
match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
|
|
531
|
+
if (match !== void 0) return meta[match];
|
|
532
|
+
const isBlacklisted = (kl) => {
|
|
533
|
+
return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
|
|
534
|
+
};
|
|
535
|
+
match = keys.find((k) => {
|
|
536
|
+
const kl = k.toLowerCase();
|
|
537
|
+
if (isBlacklisted(kl)) return false;
|
|
538
|
+
return kl.includes(uiKey.toLowerCase());
|
|
539
|
+
});
|
|
540
|
+
if (match !== void 0) return meta[match];
|
|
541
|
+
match = keys.find((k) => {
|
|
542
|
+
const kl = k.toLowerCase();
|
|
543
|
+
if (isBlacklisted(kl)) return false;
|
|
544
|
+
return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
|
|
545
|
+
});
|
|
546
|
+
return match !== void 0 ? meta[match] : void 0;
|
|
547
|
+
}
|
|
548
|
+
var FIELD_SYNONYMS;
|
|
549
|
+
var init_synonyms = __esm({
|
|
550
|
+
"src/utils/synonyms.ts"() {
|
|
551
|
+
"use strict";
|
|
552
|
+
FIELD_SYNONYMS = {
|
|
553
|
+
name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
|
|
554
|
+
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd", "variant price"],
|
|
555
|
+
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
556
|
+
image: [
|
|
557
|
+
"imageUrl",
|
|
558
|
+
"thumbnail",
|
|
559
|
+
"img",
|
|
560
|
+
"url",
|
|
561
|
+
"photo",
|
|
562
|
+
"picture",
|
|
563
|
+
"media",
|
|
564
|
+
"image_url",
|
|
565
|
+
"main_image",
|
|
566
|
+
"product_image",
|
|
567
|
+
"thumb",
|
|
568
|
+
"image src",
|
|
569
|
+
"variant image"
|
|
570
|
+
],
|
|
571
|
+
stock: [
|
|
572
|
+
"inventory",
|
|
573
|
+
"quantity",
|
|
574
|
+
"count",
|
|
575
|
+
"availability",
|
|
576
|
+
"stock_level",
|
|
577
|
+
"inStock",
|
|
578
|
+
"is_available",
|
|
579
|
+
"in stock",
|
|
580
|
+
"status",
|
|
581
|
+
"variant inventory qty"
|
|
582
|
+
],
|
|
583
|
+
category: [
|
|
584
|
+
"product_category",
|
|
585
|
+
"product category",
|
|
586
|
+
"category_name",
|
|
587
|
+
"category name",
|
|
588
|
+
"department",
|
|
589
|
+
"collection",
|
|
590
|
+
"type"
|
|
591
|
+
],
|
|
592
|
+
description: ["summary", "content", "body", "text", "info", "details", "body (html)", "seo description"],
|
|
593
|
+
link: ["url", "href", "product_url", "page_url", "link"]
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
522
598
|
// src/providers/vectordb/MultiTablePostgresProvider.ts
|
|
523
599
|
var MultiTablePostgresProvider_exports = {};
|
|
524
600
|
__export(MultiTablePostgresProvider_exports, {
|
|
@@ -530,6 +606,7 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
530
606
|
"src/providers/vectordb/MultiTablePostgresProvider.ts"() {
|
|
531
607
|
"use strict";
|
|
532
608
|
init_BaseVectorProvider();
|
|
609
|
+
init_synonyms();
|
|
533
610
|
MultiTablePostgresProvider = class extends BaseVectorProvider {
|
|
534
611
|
constructor(config) {
|
|
535
612
|
var _a, _b, _c;
|
|
@@ -610,8 +687,13 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
610
687
|
}
|
|
611
688
|
tableName = tableName.replace(/[^a-z0-9_]/gi, "_").toLowerCase() || this.uploadTable;
|
|
612
689
|
const firstMeta = fileDocs[0].metadata || {};
|
|
613
|
-
|
|
614
|
-
|
|
690
|
+
let csvHeaders = [];
|
|
691
|
+
if (Array.isArray(firstMeta.csvHeaders)) {
|
|
692
|
+
csvHeaders = firstMeta.csvHeaders;
|
|
693
|
+
} else {
|
|
694
|
+
const systemKeys = ["fileName", "fileSize", "fileType", "uploadedAt", "dimension", "chunkIndex", "id", "namespace", "content", "metadata", "embedding", "docId", "docid", "chunkId", "chunkid", "csvHeaders", "csvheaders"];
|
|
695
|
+
csvHeaders = Object.keys(firstMeta).filter((k) => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
|
|
696
|
+
}
|
|
615
697
|
const columnDefs = csvHeaders.map((h) => `"${h}" TEXT`).join(",\n ");
|
|
616
698
|
const createTableSql = `
|
|
617
699
|
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
@@ -704,10 +786,26 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
704
786
|
};
|
|
705
787
|
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
706
788
|
const tableLimit = Math.max(topK, 50);
|
|
789
|
+
const metadataFilters = _filter == null ? void 0 : _filter.metadata;
|
|
707
790
|
const queryPromises = this.tables.map(async (table) => {
|
|
708
791
|
try {
|
|
709
792
|
let sqlQuery = "";
|
|
710
793
|
let params = [];
|
|
794
|
+
let whereClause = "";
|
|
795
|
+
const filterParams = [];
|
|
796
|
+
if (metadataFilters && Object.keys(metadataFilters).length > 0) {
|
|
797
|
+
const conditions = Object.entries(metadataFilters).map(([key, val]) => {
|
|
798
|
+
var _a3;
|
|
799
|
+
filterParams.push(String(val));
|
|
800
|
+
const baseOffset = queryText && dynamicKeywordQuery ? 2 : 1;
|
|
801
|
+
const paramIdx = baseOffset + filterParams.length;
|
|
802
|
+
const synonyms = (_a3 = FIELD_SYNONYMS[key]) != null ? _a3 : [];
|
|
803
|
+
const keysToCheck = [key, ...synonyms];
|
|
804
|
+
const coalesceExprs = keysToCheck.map((k) => `LOWER(metadata->>'${k}')`);
|
|
805
|
+
return `COALESCE(${coalesceExprs.join(", ")}) = LOWER($${paramIdx})`;
|
|
806
|
+
});
|
|
807
|
+
whereClause = `WHERE ${conditions.join(" AND ")}`;
|
|
808
|
+
}
|
|
711
809
|
if (queryText && dynamicKeywordQuery) {
|
|
712
810
|
const hasEntityHints = entityHints.length > 0;
|
|
713
811
|
const exactNameScoreExpr = hasEntityHints ? `+ (
|
|
@@ -724,19 +822,21 @@ var init_MultiTablePostgresProvider = __esm({
|
|
|
724
822
|
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
725
823
|
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
726
824
|
FROM "${table}" t
|
|
825
|
+
${whereClause}
|
|
727
826
|
ORDER BY hybrid_score DESC
|
|
728
827
|
LIMIT ${tableLimit}
|
|
729
828
|
`;
|
|
730
|
-
params = [vectorLiteral, dynamicKeywordQuery];
|
|
829
|
+
params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
|
|
731
830
|
} else {
|
|
732
831
|
sqlQuery = `
|
|
733
832
|
SELECT *,
|
|
734
833
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
735
834
|
FROM "${table}" t
|
|
835
|
+
${whereClause}
|
|
736
836
|
ORDER BY hybrid_score DESC
|
|
737
837
|
LIMIT ${tableLimit}
|
|
738
838
|
`;
|
|
739
|
-
params = [vectorLiteral];
|
|
839
|
+
params = [vectorLiteral, ...filterParams];
|
|
740
840
|
}
|
|
741
841
|
const result = await this.pool.query(sqlQuery, params);
|
|
742
842
|
if (result.rowCount && result.rowCount > 0) {
|
|
@@ -969,12 +1069,15 @@ var init_MongoDBProvider = __esm({
|
|
|
969
1069
|
}
|
|
970
1070
|
);
|
|
971
1071
|
const results = await this.collection.aggregate(pipeline).toArray();
|
|
972
|
-
return results.map((res) =>
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1072
|
+
return results.map((res) => {
|
|
1073
|
+
const normalizedScore = res.score * 2 - 1;
|
|
1074
|
+
return {
|
|
1075
|
+
id: res._id,
|
|
1076
|
+
content: res[this.contentKey],
|
|
1077
|
+
metadata: res[this.metadataKey] || {},
|
|
1078
|
+
score: normalizedScore
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
978
1081
|
}
|
|
979
1082
|
async delete(id, namespace) {
|
|
980
1083
|
await this.collection.deleteOne(__spreadValues({ _id: id }, namespace ? { namespace } : {}));
|
|
@@ -4361,7 +4464,8 @@ var QueryProcessor = class {
|
|
|
4361
4464
|
const fieldValuePatterns = [
|
|
4362
4465
|
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
4363
4466
|
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
4364
|
-
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
4467
|
+
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi"),
|
|
4468
|
+
new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, "gi")
|
|
4365
4469
|
];
|
|
4366
4470
|
for (const pattern of fieldValuePatterns) {
|
|
4367
4471
|
for (const match of question.matchAll(pattern)) {
|
|
@@ -4595,73 +4699,8 @@ var LLMRouter = class {
|
|
|
4595
4699
|
}
|
|
4596
4700
|
};
|
|
4597
4701
|
|
|
4598
|
-
// src/utils/synonyms.ts
|
|
4599
|
-
var FIELD_SYNONYMS = {
|
|
4600
|
-
name: ["product", "item", "title", "label", "heading", "subject", "product name", "item name"],
|
|
4601
|
-
price: ["cost", "amount", "msrp", "price", "rate", "value", "price_usd"],
|
|
4602
|
-
brand: ["manufacturer", "vendor", "make", "company", "brand_name", "supplier"],
|
|
4603
|
-
image: [
|
|
4604
|
-
"imageUrl",
|
|
4605
|
-
"thumbnail",
|
|
4606
|
-
"img",
|
|
4607
|
-
"url",
|
|
4608
|
-
"photo",
|
|
4609
|
-
"picture",
|
|
4610
|
-
"media",
|
|
4611
|
-
"image_url",
|
|
4612
|
-
"main_image",
|
|
4613
|
-
"product_image",
|
|
4614
|
-
"thumb"
|
|
4615
|
-
],
|
|
4616
|
-
stock: [
|
|
4617
|
-
"inventory",
|
|
4618
|
-
"quantity",
|
|
4619
|
-
"count",
|
|
4620
|
-
"availability",
|
|
4621
|
-
"stock_level",
|
|
4622
|
-
"inStock",
|
|
4623
|
-
"is_available",
|
|
4624
|
-
"in stock",
|
|
4625
|
-
"status"
|
|
4626
|
-
],
|
|
4627
|
-
category: [
|
|
4628
|
-
"product_category",
|
|
4629
|
-
"product category",
|
|
4630
|
-
"category_name",
|
|
4631
|
-
"category name",
|
|
4632
|
-
"department",
|
|
4633
|
-
"collection"
|
|
4634
|
-
],
|
|
4635
|
-
description: ["summary", "content", "body", "text", "info", "details"],
|
|
4636
|
-
link: ["url", "href", "product_url", "page_url", "link"]
|
|
4637
|
-
};
|
|
4638
|
-
function resolveMetadataValue(meta, uiKey) {
|
|
4639
|
-
var _a;
|
|
4640
|
-
const synonyms = (_a = FIELD_SYNONYMS[uiKey]) != null ? _a : [];
|
|
4641
|
-
const keys = Object.keys(meta);
|
|
4642
|
-
const systemKeys = ["namespace", "filename", "filesize", "filetype", "docid", "uploadedat", "dimension", "chunkindex"];
|
|
4643
|
-
let match = keys.find((k) => k.toLowerCase() === uiKey.toLowerCase());
|
|
4644
|
-
if (match !== void 0) return meta[match];
|
|
4645
|
-
match = keys.find((k) => synonyms.map((s) => s.toLowerCase()).includes(k.toLowerCase()));
|
|
4646
|
-
if (match !== void 0) return meta[match];
|
|
4647
|
-
const isBlacklisted = (kl) => {
|
|
4648
|
-
return systemKeys.includes(kl) || kl.startsWith("option1") || kl.startsWith("option2") || kl.startsWith("option3");
|
|
4649
|
-
};
|
|
4650
|
-
match = keys.find((k) => {
|
|
4651
|
-
const kl = k.toLowerCase();
|
|
4652
|
-
if (isBlacklisted(kl)) return false;
|
|
4653
|
-
return kl.includes(uiKey.toLowerCase());
|
|
4654
|
-
});
|
|
4655
|
-
if (match !== void 0) return meta[match];
|
|
4656
|
-
match = keys.find((k) => {
|
|
4657
|
-
const kl = k.toLowerCase();
|
|
4658
|
-
if (isBlacklisted(kl)) return false;
|
|
4659
|
-
return synonyms.some((sk) => kl.includes(sk.toLowerCase()) || sk.toLowerCase().includes(kl));
|
|
4660
|
-
});
|
|
4661
|
-
return match !== void 0 ? meta[match] : void 0;
|
|
4662
|
-
}
|
|
4663
|
-
|
|
4664
4702
|
// src/utils/UITransformer.ts
|
|
4703
|
+
init_synonyms();
|
|
4665
4704
|
var UITransformer = class {
|
|
4666
4705
|
// ─── Public Entry Points ─────────────────────────────────────────────────
|
|
4667
4706
|
/**
|
|
@@ -4958,7 +4997,7 @@ RULES:
|
|
|
4958
4997
|
}
|
|
4959
4998
|
// ─── Transform Helpers ────────────────────────────────────────────────────
|
|
4960
4999
|
static transformToProductCarousel(data, config, trainedSchema) {
|
|
4961
|
-
const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null);
|
|
5000
|
+
const products = data.map((item) => this.extractProductInfo(item, config, trainedSchema)).filter((p) => p !== null).slice(0, 15);
|
|
4962
5001
|
return {
|
|
4963
5002
|
type: "product_carousel",
|
|
4964
5003
|
title: "Recommended Products",
|
|
@@ -5917,14 +5956,16 @@ Given these metadata keys from a database: [${keys.join(", ")}]
|
|
|
5917
5956
|
Identify which keys best correspond to these standard UI properties:
|
|
5918
5957
|
${propertyList}
|
|
5919
5958
|
|
|
5920
|
-
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
5959
|
+
Return ONLY a valid JSON object where the keys are the UI properties and the values are the EXACT matching database keys from the list above.
|
|
5921
5960
|
If no good match is found for a property, omit it.
|
|
5922
5961
|
|
|
5923
5962
|
Example:
|
|
5924
5963
|
{
|
|
5925
|
-
"name": "
|
|
5926
|
-
"price": "
|
|
5927
|
-
"brand": "
|
|
5964
|
+
"name": "Title",
|
|
5965
|
+
"price": "Variant Price",
|
|
5966
|
+
"brand": "Vendor",
|
|
5967
|
+
"image": "Image Src",
|
|
5968
|
+
"stock": "Variant Inventory Qty"
|
|
5928
5969
|
}
|
|
5929
5970
|
`;
|
|
5930
5971
|
try {
|
|
@@ -6215,7 +6256,10 @@ var Pipeline = class {
|
|
|
6215
6256
|
upsertBatchOptions
|
|
6216
6257
|
);
|
|
6217
6258
|
if (upsertResult.errors.length > 0) {
|
|
6218
|
-
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed
|
|
6259
|
+
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
|
|
6260
|
+
upsertResult.errors.forEach((err, idx) => {
|
|
6261
|
+
console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
|
|
6262
|
+
});
|
|
6219
6263
|
}
|
|
6220
6264
|
return upsertResult.totalProcessed;
|
|
6221
6265
|
}
|
|
@@ -6330,8 +6374,9 @@ var Pipeline = class {
|
|
|
6330
6374
|
this.embeddingCache.set(cacheKey, queryVector);
|
|
6331
6375
|
}
|
|
6332
6376
|
const graphData = (strategyResult === "graph" || strategyResult === "both") && this.graphDB && ((_j = this.config.rag) == null ? void 0 : _j.useGraphRetrieval) ? yield new __await(this.graphDB.query(searchQuery)) : void 0;
|
|
6377
|
+
const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
|
|
6333
6378
|
const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
|
|
6334
|
-
const wantsExhaustiveList = hasNumericPredicates || /\b(list|all|show|provide|give|get)\b/i.test(question);
|
|
6379
|
+
const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
|
|
6335
6380
|
const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
|
|
6336
6381
|
const rawSources = (strategyResult === "vector" || strategyResult === "both") && queryVector && queryVector.length > 0 ? yield new __await(this.vectorDB.query(queryVector, retrievalLimit, ns, filter)) : [];
|
|
6337
6382
|
const retrieveEnd = performance.now();
|
|
@@ -6339,15 +6384,28 @@ var Pipeline = class {
|
|
|
6339
6384
|
const retrieveMs = retrieveEnd - embedStart;
|
|
6340
6385
|
const rerankStart = performance.now();
|
|
6341
6386
|
const structuredSources = this.applyStructuredFilters(rawSources, filter);
|
|
6342
|
-
let
|
|
6387
|
+
let fullSources = hasNumericPredicates ? structuredSources : structuredSources.filter((m) => m.score >= scoreThreshold);
|
|
6388
|
+
const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
|
|
6343
6389
|
if (!hasNumericPredicates && ((_k = this.config.rag) == null ? void 0 : _k.useReranking)) {
|
|
6344
|
-
|
|
6390
|
+
fullSources = yield new __await(this.reranker.rerank(fullSources, question, rerankLimit));
|
|
6345
6391
|
} else if (!wantsExhaustiveList) {
|
|
6346
|
-
|
|
6392
|
+
fullSources = fullSources.slice(0, topK);
|
|
6347
6393
|
}
|
|
6348
6394
|
const rerankMs = performance.now() - rerankStart;
|
|
6349
|
-
let context =
|
|
6395
|
+
let context = fullSources.length ? fullSources.map((m, i) => `[Source ${i + 1}]
|
|
6350
6396
|
${m.content}`).join("\n\n---\n\n") : "No relevant context found.";
|
|
6397
|
+
let displayCount = 15;
|
|
6398
|
+
if (hasMetadataFilter) {
|
|
6399
|
+
displayCount = fullSources.length;
|
|
6400
|
+
} else {
|
|
6401
|
+
const baseThreshold = Math.max(scoreThreshold, 0.4);
|
|
6402
|
+
const highlyRelevant = fullSources.filter((m) => m.score >= baseThreshold);
|
|
6403
|
+
displayCount = Math.max(highlyRelevant.length, topK);
|
|
6404
|
+
if (displayCount > 15) {
|
|
6405
|
+
displayCount = 15;
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
const sources = [...fullSources].sort((a, b) => b.score - a.score).slice(0, displayCount);
|
|
6351
6409
|
if (graphData && graphData.nodes.length > 0) {
|
|
6352
6410
|
const graphContext = graphData.nodes.map(
|
|
6353
6411
|
(n) => `Entity: ${n.label} (${n.id})${n.properties ? " - " + JSON.stringify(n.properties) : ""}`
|
|
@@ -6360,7 +6418,18 @@ ${context}`;
|
|
|
6360
6418
|
}
|
|
6361
6419
|
const allMetadataKeys = Array.from(new Set(sources.flatMap((s) => Object.keys(s.metadata || {}))));
|
|
6362
6420
|
const trainedSchemaPromise = allMetadataKeys.length > 0 ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => void 0) : Promise.resolve(void 0);
|
|
6363
|
-
const
|
|
6421
|
+
const uiTransformationPromise = trainedSchemaPromise.then(
|
|
6422
|
+
(trainedSchema) => this.generateUiTransformation(
|
|
6423
|
+
question,
|
|
6424
|
+
sources,
|
|
6425
|
+
trainedSchema,
|
|
6426
|
+
hasNumericPredicates
|
|
6427
|
+
)
|
|
6428
|
+
).catch((uiError) => {
|
|
6429
|
+
console.warn("[Pipeline] UI transformation failed concurrently:", uiError);
|
|
6430
|
+
return UITransformer.transform(question, sources, this.config);
|
|
6431
|
+
});
|
|
6432
|
+
const restrictionSuffix = "\n\n(IMPORTANT: Format your response beautifully using rich Markdown! Use bolding for emphasis, headings (##) for structure, bullet points for lists, and proper line breaks between paragraphs to make it highly readable. However, NEVER generate Markdown tables, HTML figures, or text charts (the UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization). If listing products, use simple markdown bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)";
|
|
6364
6433
|
const hardenedHistory = [...history];
|
|
6365
6434
|
const userQuestion = { role: "user", content: question + restrictionSuffix };
|
|
6366
6435
|
const messages = [...hardenedHistory, userQuestion];
|
|
@@ -6430,19 +6499,7 @@ ${context}`;
|
|
|
6430
6499
|
tokens,
|
|
6431
6500
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6432
6501
|
};
|
|
6433
|
-
|
|
6434
|
-
try {
|
|
6435
|
-
const trainedSchema = yield new __await(trainedSchemaPromise);
|
|
6436
|
-
uiTransformation = yield new __await(this.generateUiTransformation(
|
|
6437
|
-
question,
|
|
6438
|
-
sources,
|
|
6439
|
-
trainedSchema,
|
|
6440
|
-
hasNumericPredicates
|
|
6441
|
-
));
|
|
6442
|
-
} catch (uiError) {
|
|
6443
|
-
console.warn("[Pipeline] UI transformation failed before metadata yield:", uiError);
|
|
6444
|
-
uiTransformation = UITransformer.transform(question, sources, this.config);
|
|
6445
|
-
}
|
|
6502
|
+
const uiTransformation = yield new __await(uiTransformationPromise);
|
|
6446
6503
|
yield {
|
|
6447
6504
|
reply: "",
|
|
6448
6505
|
sources,
|
|
@@ -7176,15 +7233,24 @@ function createStreamHandler(configOrPlugin) {
|
|
|
7176
7233
|
});
|
|
7177
7234
|
}
|
|
7178
7235
|
const encoder = new TextEncoder();
|
|
7236
|
+
let isActive = true;
|
|
7179
7237
|
const stream = new ReadableStream({
|
|
7180
7238
|
async start(controller) {
|
|
7181
7239
|
var _a;
|
|
7182
|
-
const enqueue = (text) =>
|
|
7240
|
+
const enqueue = (text) => {
|
|
7241
|
+
if (!isActive) return;
|
|
7242
|
+
try {
|
|
7243
|
+
controller.enqueue(encoder.encode(text));
|
|
7244
|
+
} catch (err) {
|
|
7245
|
+
console.warn("[createStreamHandler] Failed to enqueue (stream already closed):", err);
|
|
7246
|
+
}
|
|
7247
|
+
};
|
|
7183
7248
|
try {
|
|
7184
7249
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
7185
7250
|
try {
|
|
7186
7251
|
for (var iter = __forAwait(pipelineStream), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
7187
7252
|
const chunk = temp.value;
|
|
7253
|
+
if (!isActive) break;
|
|
7188
7254
|
if (typeof chunk === "string") {
|
|
7189
7255
|
enqueue(sseTextFrame(chunk));
|
|
7190
7256
|
} else {
|
|
@@ -7222,12 +7288,27 @@ function createStreamHandler(configOrPlugin) {
|
|
|
7222
7288
|
}
|
|
7223
7289
|
}
|
|
7224
7290
|
} catch (streamError) {
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7291
|
+
if (isActive) {
|
|
7292
|
+
const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
|
|
7293
|
+
console.error("[createStreamHandler] Stream error:", streamError);
|
|
7294
|
+
try {
|
|
7295
|
+
enqueue(sseErrorFrame(errorMessage));
|
|
7296
|
+
} catch (e) {
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7228
7299
|
} finally {
|
|
7229
|
-
|
|
7300
|
+
if (isActive) {
|
|
7301
|
+
isActive = false;
|
|
7302
|
+
try {
|
|
7303
|
+
controller.close();
|
|
7304
|
+
} catch (e) {
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7230
7307
|
}
|
|
7308
|
+
},
|
|
7309
|
+
cancel(reason) {
|
|
7310
|
+
isActive = false;
|
|
7311
|
+
console.log("[createStreamHandler] Stream connection closed by client:", reason);
|
|
7231
7312
|
}
|
|
7232
7313
|
});
|
|
7233
7314
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
@@ -7288,6 +7369,7 @@ function createUploadHandler(configOrPlugin) {
|
|
|
7288
7369
|
if (parsed.data && parsed.data.length > 0) {
|
|
7289
7370
|
let i = 0;
|
|
7290
7371
|
let lastRowData = null;
|
|
7372
|
+
const csvCols = Object.keys(parsed.data[0]);
|
|
7291
7373
|
for (const row of parsed.data) {
|
|
7292
7374
|
i++;
|
|
7293
7375
|
const rowData = row;
|
|
@@ -7309,12 +7391,14 @@ function createUploadHandler(configOrPlugin) {
|
|
|
7309
7391
|
documents.push({
|
|
7310
7392
|
docId: `${file.name}-row-${i}`,
|
|
7311
7393
|
content: contentParts.join(", "),
|
|
7312
|
-
metadata: __spreadValues(__spreadValues({
|
|
7394
|
+
metadata: __spreadValues(__spreadProps(__spreadValues({
|
|
7313
7395
|
fileName: file.name,
|
|
7314
7396
|
fileSize: file.size,
|
|
7315
7397
|
fileType: file.type,
|
|
7316
7398
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7317
|
-
}, dimension ? { dimension } : {}),
|
|
7399
|
+
}, dimension ? { dimension } : {}), {
|
|
7400
|
+
csvHeaders: csvCols
|
|
7401
|
+
}), rowData)
|
|
7318
7402
|
});
|
|
7319
7403
|
}
|
|
7320
7404
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@ export function ChatWindow({
|
|
|
71
71
|
const windowRef = useRef<HTMLDivElement>(null);
|
|
72
72
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
73
73
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
74
|
-
const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
|
|
74
|
+
const { messages, send, clear, isLoading, error, stop } = useRagChat(projectId, {
|
|
75
75
|
namespace: projectId,
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -492,19 +492,29 @@ export function ChatWindow({
|
|
|
492
492
|
</button>
|
|
493
493
|
)}
|
|
494
494
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
495
|
+
{isLoading ? (
|
|
496
|
+
<button
|
|
497
|
+
onClick={stop}
|
|
498
|
+
className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 shadow-md bg-rose-500 hover:bg-rose-600 text-white cursor-pointer"
|
|
499
|
+
title="Stop generating"
|
|
500
|
+
>
|
|
501
|
+
<div className="w-3.5 h-3.5 bg-white rounded-[2px]" />
|
|
502
|
+
</button>
|
|
503
|
+
) : (
|
|
504
|
+
<button
|
|
505
|
+
onClick={sendMessage}
|
|
506
|
+
disabled={!input.trim()}
|
|
507
|
+
className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:scale-105 active:scale-95 shadow-md"
|
|
508
|
+
style={{
|
|
509
|
+
background:
|
|
510
|
+
input.trim()
|
|
511
|
+
? `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})`
|
|
512
|
+
: 'rgba(148, 163, 184, 0.2)', // slate-400/20 for light mode disabled
|
|
513
|
+
}}
|
|
514
|
+
>
|
|
515
|
+
<ArrowUp className="w-4 h-4 text-white" />
|
|
516
|
+
</button>
|
|
517
|
+
)}
|
|
508
518
|
</div>
|
|
509
519
|
<p className="text-center text-[10px] text-slate-400 dark:text-white/20 mt-2">
|
|
510
520
|
Press Enter to send · Shift+Enter for new line
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Bot, User, ChevronDown, ChevronRight, Activity } from 'lucide-react';
|
|
4
|
+
import { Bot, User, ChevronDown, ChevronRight, Activity, Copy, Check } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import { MessageBubbleProps, Product, UITransformationResponse } from '../types';
|
|
7
7
|
import { SourceCard } from './SourceCard';
|
|
@@ -61,6 +61,17 @@ export function MessageBubble({
|
|
|
61
61
|
|
|
62
62
|
const [showSources, setShowSources] = React.useState(false);
|
|
63
63
|
const [showTrace, setShowTrace] = React.useState(false);
|
|
64
|
+
const [copied, setCopied] = React.useState(false);
|
|
65
|
+
|
|
66
|
+
const handleCopy = React.useCallback(async () => {
|
|
67
|
+
try {
|
|
68
|
+
await navigator.clipboard.writeText(message.content);
|
|
69
|
+
setCopied(true);
|
|
70
|
+
setTimeout(() => setCopied(false), 2000);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('Failed to copy text:', err);
|
|
73
|
+
}
|
|
74
|
+
}, [message.content]);
|
|
64
75
|
|
|
65
76
|
const structuredContent = React.useMemo(
|
|
66
77
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
@@ -187,7 +198,7 @@ export function MessageBubble({
|
|
|
187
198
|
<div className={`flex flex-col gap-1 min-w-0 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
|
|
188
199
|
{/* Bubble */}
|
|
189
200
|
<div
|
|
190
|
-
className={`relative ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
201
|
+
className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
191
202
|
? 'text-white rounded-tr-sm'
|
|
192
203
|
: 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
|
|
193
204
|
}`}
|
|
@@ -195,6 +206,20 @@ export function MessageBubble({
|
|
|
195
206
|
isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
196
207
|
}
|
|
197
208
|
>
|
|
209
|
+
{/* Floating Copy Button for completed assistant messages */}
|
|
210
|
+
{!isUser && !isStreaming && message.content && (
|
|
211
|
+
<button
|
|
212
|
+
onClick={handleCopy}
|
|
213
|
+
className={`absolute top-2 right-2 p-1.5 rounded-lg border transition-all duration-200 cursor-pointer ${
|
|
214
|
+
copied
|
|
215
|
+
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-500 dark:text-emerald-400'
|
|
216
|
+
: 'bg-slate-500/5 hover:bg-slate-500/10 border-slate-200 dark:border-white/10 text-slate-400 dark:text-white/30 hover:text-slate-600 dark:hover:text-white/60'
|
|
217
|
+
} opacity-0 group-hover:opacity-100 backdrop-blur-sm shadow-sm scale-95 group-hover:scale-100`}
|
|
218
|
+
title={copied ? "Copied!" : "Copy response"}
|
|
219
|
+
>
|
|
220
|
+
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
198
223
|
{isStreaming && !message.content ? (
|
|
199
224
|
<span className="flex items-center gap-1 py-1">
|
|
200
225
|
<span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" />
|
|
@@ -240,11 +265,21 @@ export function MessageBubble({
|
|
|
240
265
|
if (isUser || structuredContent.payload || !message.uiTransformation) return null;
|
|
241
266
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
242
267
|
const textContent = (ui.data as { content?: string })?.content ?? '';
|
|
268
|
+
|
|
269
|
+
const isNegativeOrFallbackText =
|
|
270
|
+
textContent.toLowerCase().includes('cannot answer') ||
|
|
271
|
+
textContent.toLowerCase().includes('no relevant data') ||
|
|
272
|
+
textContent.toLowerCase().includes('no data available') ||
|
|
273
|
+
(typeof ui.title === 'string' && ui.title.toLowerCase().includes('no data')) ||
|
|
274
|
+
(typeof ui.description === 'string' && ui.description.toLowerCase().includes('no relevant data'));
|
|
275
|
+
|
|
243
276
|
const shouldShow =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
!isNegativeOrFallbackText &&
|
|
278
|
+
(ui.type === 'table' ||
|
|
279
|
+
!['text', 'table'].includes(ui.type) ||
|
|
280
|
+
(ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
|
|
281
|
+
!processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase())));
|
|
282
|
+
|
|
248
283
|
if (!shouldShow) return null;
|
|
249
284
|
return (
|
|
250
285
|
<div className="w-full mt-3">
|
|
@@ -47,7 +47,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
47
47
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
48
48
|
/>
|
|
49
49
|
) : (
|
|
50
|
-
<div className="text-slate-400 dark:text-white/20
|
|
50
|
+
<div className="text-slate-400 dark:text-white/20">
|
|
51
51
|
<ShoppingCart className="w-12 h-12" />
|
|
52
52
|
</div>
|
|
53
53
|
)}
|
|
@@ -68,8 +68,8 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
68
68
|
</h3>
|
|
69
69
|
{product.price && (
|
|
70
70
|
<span className="text-sm font-bold" style={{ color: primaryColor }}>
|
|
71
|
-
{typeof product.price === 'number'
|
|
72
|
-
? `$${product.price}`
|
|
71
|
+
{typeof product.price === 'number'
|
|
72
|
+
? `$${product.price}`
|
|
73
73
|
: (String(product.price).match(/^[\d.]/) ? `$${product.price}` : product.price)}
|
|
74
74
|
</span>
|
|
75
75
|
)}
|
|
@@ -84,7 +84,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
84
84
|
<div className="mt-2 flex gap-2">
|
|
85
85
|
<button
|
|
86
86
|
onClick={() => onAddToCart?.(product)}
|
|
87
|
-
className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5
|
|
87
|
+
className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5"
|
|
88
88
|
style={{ background: primaryColor }}
|
|
89
89
|
>
|
|
90
90
|
<ShoppingCart className="w-3 h-3" />
|