@retrivora-ai/rag-engine 1.8.9 → 1.9.1
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-CbUtvWAW.d.mts → ILLMProvider-BQyKZLnd.d.mts} +10 -2
- package/dist/{ILLMProvider-CbUtvWAW.d.ts → ILLMProvider-BQyKZLnd.d.ts} +10 -2
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1726 -522
- package/dist/handlers/index.mjs +1725 -521
- package/dist/{index-DgzcnqZs.d.ts → index-A0GqPsaG.d.ts} +1 -1
- package/dist/{index-c_y_5qdw.d.mts → index-CDftK3qs.d.mts} +1 -1
- package/dist/index.css +26 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +246 -76
- package/dist/index.mjs +248 -76
- package/dist/server.d.mts +32 -5
- package/dist/server.d.ts +32 -5
- package/dist/server.js +1735 -733
- package/dist/server.mjs +1733 -731
- package/package.json +1 -1
- package/src/components/MarkdownComponents.tsx +3 -3
- package/src/components/MessageBubble.tsx +49 -2
- package/src/components/ProductCard.tsx +33 -6
- package/src/components/UIDispatcher.tsx +1 -0
- package/src/components/VisualizationRenderer.tsx +143 -11
- package/src/config/EmbeddingStrategy.ts +5 -4
- package/src/config/RagConfig.ts +10 -2
- package/src/config/serverConfig.ts +16 -1
- package/src/core/LLMRouter.ts +79 -0
- package/src/core/Pipeline.ts +262 -45
- package/src/core/ProviderRegistry.ts +6 -0
- package/src/core/QueryProcessor.ts +98 -0
- package/src/handlers/index.ts +8 -5
- package/src/hooks/useRagChat.ts +53 -17
- package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
- package/src/providers/vectordb/ChromaDBProvider.ts +13 -6
- package/src/providers/vectordb/MilvusProvider.ts +18 -2
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +16 -29
- package/src/providers/vectordb/PostgreSQLProvider.ts +4 -15
- package/src/providers/vectordb/QdrantProvider.ts +2 -5
- package/src/providers/vectordb/RedisProvider.ts +3 -4
- package/src/providers/vectordb/WeaviateProvider.ts +41 -3
- package/src/types/index.ts +26 -0
- package/src/utils/ProductExtractor.ts +5 -3
- package/src/utils/UITransformer.ts +1345 -534
- package/src/utils/synonyms.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
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",
|
|
@@ -20,9 +20,9 @@ export function createMarkdownComponents({
|
|
|
20
20
|
viewportSize,
|
|
21
21
|
}: MarkdownComponentsProps): Components {
|
|
22
22
|
return {
|
|
23
|
-
p: ({ ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
|
24
|
-
|
|
25
|
-
),
|
|
23
|
+
// p: ({ ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
|
24
|
+
// <p className="whitespace-pre-line" {...props} />
|
|
25
|
+
// ),
|
|
26
26
|
table: ({ ...props }: React.HTMLAttributes<HTMLTableElement>) => {
|
|
27
27
|
// Fix: intercept table rendering during streaming instead of using sentinel
|
|
28
28
|
if (isStreaming) {
|
|
@@ -22,6 +22,28 @@ import {
|
|
|
22
22
|
import { createMarkdownComponents } from './MarkdownComponents';
|
|
23
23
|
import { UIDispatcher } from './UIDispatcher';
|
|
24
24
|
|
|
25
|
+
function normalizePlusSeparatedLists(raw: string): string {
|
|
26
|
+
return raw
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => {
|
|
29
|
+
const productListLine =
|
|
30
|
+
/[—:]\s*\d+\s+\+\s+[A-Za-z]/.test(line) ||
|
|
31
|
+
/\b(products?|categories?|in stock|out of stock)\b/i.test(line);
|
|
32
|
+
if (!productListLine) return line;
|
|
33
|
+
|
|
34
|
+
return line.replace(/\s+\+\s+(?=[A-Za-z0-9])/g, ', ');
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stripLeakedAnswerWrappers(raw: string): string {
|
|
40
|
+
return raw
|
|
41
|
+
.replace(/<prose\s+answer(?:\s+in\s+[^>]*)?>\s*([\s\S]*?)\s*<\/prose>/gi, '$1')
|
|
42
|
+
.replace(/<\/?prose(?:\s+answer)?[^>]*>/gi, '')
|
|
43
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
26
48
|
|
|
27
49
|
export function MessageBubble({
|
|
@@ -56,6 +78,11 @@ export function MessageBubble({
|
|
|
56
78
|
return false;
|
|
57
79
|
}, [message.uiTransformation, structuredContent.payload]);
|
|
58
80
|
|
|
81
|
+
const uiTransformationType = (message.uiTransformation as UITransformationResponse | undefined)?.type;
|
|
82
|
+
const shouldSuppressProductCarousel = Boolean(
|
|
83
|
+
uiTransformationType && !['text', 'product_carousel', 'carousel'].includes(uiTransformationType),
|
|
84
|
+
);
|
|
85
|
+
|
|
59
86
|
const shouldRenderStructuredOnly = !isUser && hasRichUI && isLikelyUiOnlyMessage(structuredContent.text);
|
|
60
87
|
|
|
61
88
|
// ── Extraction ─────────────────────────────────────────────────────────────
|
|
@@ -89,6 +116,26 @@ export function MessageBubble({
|
|
|
89
116
|
|
|
90
117
|
const processedMarkdown = React.useMemo<string>(() => {
|
|
91
118
|
let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
|
|
119
|
+
|
|
120
|
+
// 0. Pre-sanitize: strip $ from non-monetary whole integers.
|
|
121
|
+
// Real prices always have a decimal (e.g. $257.00). A bare $168 or $9680811891595 is NOT a price.
|
|
122
|
+
raw = raw.replace(/\$\s*(\d+)(?!\.\d)/g, '$1');
|
|
123
|
+
raw = normalizePlusSeparatedLists(raw);
|
|
124
|
+
raw = stripLeakedAnswerWrappers(raw);
|
|
125
|
+
|
|
126
|
+
// 0a. Strip bold (**...**) from inline field labels.
|
|
127
|
+
// The LLM wraps field names like **Stock** — 168 or **EAN** — 9680811891595 in bold.
|
|
128
|
+
// We want to strip the ** so it renders as plain text, not bold.
|
|
129
|
+
raw = raw.replace(/\*\*([^*\n]+)\*\*(\s*[—\-–:]\s*)/g, '$1$2');
|
|
130
|
+
|
|
131
|
+
// 0b. Remove duplicate lines (LLM sometimes repeats fields like Stock, EAN twice)
|
|
132
|
+
raw = raw
|
|
133
|
+
.split('\n')
|
|
134
|
+
.filter((line, idx, arr) => {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
return !trimmed || arr.findIndex(l => l.trim() === trimmed) === idx;
|
|
137
|
+
})
|
|
138
|
+
.join('\n');
|
|
92
139
|
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
93
140
|
|
|
94
141
|
// 1. Structural fixes
|
|
@@ -194,7 +241,7 @@ export function MessageBubble({
|
|
|
194
241
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
195
242
|
const textContent = (ui.data as { content?: string })?.content ?? '';
|
|
196
243
|
const shouldShow =
|
|
197
|
-
|
|
244
|
+
ui.type === 'table' ||
|
|
198
245
|
!['text', 'table'].includes(ui.type) ||
|
|
199
246
|
(ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
|
|
200
247
|
!processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase()));
|
|
@@ -213,7 +260,7 @@ export function MessageBubble({
|
|
|
213
260
|
})()}
|
|
214
261
|
|
|
215
262
|
{/* Product Carousel */}
|
|
216
|
-
{!isUser && !hasRichUI && allProducts.length > 0 && (
|
|
263
|
+
{!isUser && !hasRichUI && !shouldSuppressProductCarousel && allProducts.length > 0 && (
|
|
217
264
|
<div className="w-full mt-1">
|
|
218
265
|
<ProductCarousel
|
|
219
266
|
products={allProducts}
|
|
@@ -6,7 +6,34 @@ import { ShoppingCart, ExternalLink } from 'lucide-react';
|
|
|
6
6
|
|
|
7
7
|
import { ProductCardProps } from '../types';
|
|
8
8
|
|
|
9
|
+
function cleanProductDescription(raw: unknown): string {
|
|
10
|
+
if (raw === null || raw === undefined) return '';
|
|
11
|
+
|
|
12
|
+
const source = String(raw);
|
|
13
|
+
const bodyMatch = source.match(
|
|
14
|
+
/\bBody\s*\(HTML\)\s*:\s*([\s\S]*?)(?=\s*[,.]?\s+\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\b\s*:|$)/i,
|
|
15
|
+
);
|
|
16
|
+
const descriptionMatch = bodyMatch ?? source.match(
|
|
17
|
+
/\b(?:Description|Summary|Details|Body)\s*:\s*([\s\S]*?)(?=\s*[,.]?\s+\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\b\s*:|$)/i,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (descriptionMatch?.[1] ?? source)
|
|
21
|
+
.replace(/<[^>]*>/g, ' ')
|
|
22
|
+
.replace(/ /gi, ' ')
|
|
23
|
+
.replace(/&/gi, '&')
|
|
24
|
+
.replace(/"/gi, '"')
|
|
25
|
+
.replace(/'/gi, "'")
|
|
26
|
+
.replace(/\[[^\]]*TYPE[^\]]*\]/gi, ' ')
|
|
27
|
+
.replace(/\b(?:Handle|Title|Vendor|Type|Tags|Published|Option\d*\s+Name|Option\d*\s+Value|Option|Variant|Image|SKU|Price|Status|Category)\s*:\s*[^,.\n:]+[,.]?/gi, ' ')
|
|
28
|
+
.replace(/\bBody\s*\(HTML\)\s*:\s*/gi, ' ')
|
|
29
|
+
.replace(/\b(?:Description|Summary|Details|Body|Content)\s*:\s*/gi, ' ')
|
|
30
|
+
.replace(/\s+/g, ' ')
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }: ProductCardProps) {
|
|
35
|
+
const description = cleanProductDescription(product.description);
|
|
36
|
+
|
|
10
37
|
return (
|
|
11
38
|
<div className="flex-shrink-0 w-full bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 group">
|
|
12
39
|
{/* Product Image */}
|
|
@@ -20,7 +47,7 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
20
47
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
21
48
|
/>
|
|
22
49
|
) : (
|
|
23
|
-
<div className="text-slate-400 dark:text-white/20">
|
|
50
|
+
<div className="text-slate-400 dark:text-white/20 cursor-pointer" onClick={() => onAddToCart?.(product)}>
|
|
24
51
|
<ShoppingCart className="w-12 h-12" />
|
|
25
52
|
</div>
|
|
26
53
|
)}
|
|
@@ -41,23 +68,23 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
41
68
|
</h3>
|
|
42
69
|
{product.price && (
|
|
43
70
|
<span className="text-sm font-bold" style={{ color: primaryColor }}>
|
|
44
|
-
{typeof product.price === 'number'
|
|
45
|
-
? `$${product.price}`
|
|
71
|
+
{typeof product.price === 'number'
|
|
72
|
+
? `$${product.price}`
|
|
46
73
|
: (String(product.price).match(/^[\d.]/) ? `$${product.price}` : product.price)}
|
|
47
74
|
</span>
|
|
48
75
|
)}
|
|
49
76
|
</div>
|
|
50
77
|
|
|
51
|
-
{
|
|
78
|
+
{description && (
|
|
52
79
|
<p className="text-[11px] text-slate-500 dark:text-white/50 line-clamp-2 leading-relaxed">
|
|
53
|
-
{
|
|
80
|
+
{description}
|
|
54
81
|
</p>
|
|
55
82
|
)}
|
|
56
83
|
|
|
57
84
|
<div className="mt-2 flex gap-2">
|
|
58
85
|
<button
|
|
59
86
|
onClick={() => onAddToCart?.(product)}
|
|
60
|
-
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 cursor-pointer"
|
|
61
88
|
style={{ background: primaryColor }}
|
|
62
89
|
>
|
|
63
90
|
<ShoppingCart className="w-3 h-3" />
|
|
@@ -166,6 +166,7 @@ export function UIDispatcher({ rawContent, primaryColor, accentColor, isStreamin
|
|
|
166
166
|
onAddToCart?: (product: Product) => void;
|
|
167
167
|
viewportSize?: ChatViewportSize;
|
|
168
168
|
}) {
|
|
169
|
+
console.log('rawContent', rawContent);
|
|
169
170
|
const result = React.useMemo<{ config: UIConfig } | { error: string } | { loading: true }>(() => {
|
|
170
171
|
try {
|
|
171
172
|
const clean = rawContent
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
Bar,
|
|
10
10
|
LineChart,
|
|
11
11
|
Line,
|
|
12
|
+
ScatterChart,
|
|
13
|
+
Scatter,
|
|
12
14
|
XAxis,
|
|
13
15
|
YAxis,
|
|
14
16
|
CartesianGrid,
|
|
@@ -22,7 +24,18 @@ import {
|
|
|
22
24
|
PolarRadiusAxis,
|
|
23
25
|
} from 'recharts';
|
|
24
26
|
import { ProductCarousel } from './ProductCarousel';
|
|
25
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
Product,
|
|
29
|
+
UITransformationResponse,
|
|
30
|
+
VisualizationType,
|
|
31
|
+
PieChartData,
|
|
32
|
+
BarChartData,
|
|
33
|
+
LineChartDataPoint,
|
|
34
|
+
ScatterPlotDataPoint,
|
|
35
|
+
MetricCardData,
|
|
36
|
+
TableData,
|
|
37
|
+
CarouselProduct,
|
|
38
|
+
} from '../types';
|
|
26
39
|
|
|
27
40
|
import { CHART_COLORS } from './DynamicChart';
|
|
28
41
|
|
|
@@ -30,6 +43,13 @@ function getColor(index: number): string {
|
|
|
30
43
|
return CHART_COLORS[index % CHART_COLORS.length];
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
function stripLeakedAnswerWrappers(raw: string): string {
|
|
47
|
+
return raw
|
|
48
|
+
.replace(/<prose\s+answer(?:\s+in\s+[^>]*)?>\s*([\s\S]*?)\s*<\/prose>/gi, '$1')
|
|
49
|
+
.replace(/<\/?prose(?:\s+answer)?[^>]*>/gi, '')
|
|
50
|
+
.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
// ─── Custom Tooltip ───────────────────────────────────────────────────────────
|
|
34
54
|
|
|
35
55
|
interface TooltipPayloadItem {
|
|
@@ -145,10 +165,20 @@ function renderVisualization(
|
|
|
145
165
|
return <PieChartView data={data as PieChartData[]} isStreaming={isStreaming} />;
|
|
146
166
|
case 'bar_chart':
|
|
147
167
|
return <BarChartView data={data as BarChartData[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
|
|
168
|
+
case 'histogram':
|
|
169
|
+
return <BarChartView data={data as BarChartData[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
|
|
170
|
+
case 'horizontal_bar':
|
|
171
|
+
return <BarChartView data={data as BarChartData[]} primaryColor={primaryColor} isStreaming={isStreaming} layout="vertical" />;
|
|
148
172
|
case 'line_chart':
|
|
149
173
|
return <LineChartView data={data as LineChartDataPoint[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
|
|
174
|
+
case 'scatter_plot':
|
|
175
|
+
return <ScatterPlotView data={data as ScatterPlotDataPoint[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
|
|
150
176
|
case 'radar_chart':
|
|
151
177
|
return <RadarChartView data={data as Record<string, unknown>[]} isStreaming={isStreaming} />;
|
|
178
|
+
case 'metric_card':
|
|
179
|
+
return <MetricCardView data={data as MetricCardData} isStreaming={isStreaming} />;
|
|
180
|
+
case 'geo_map':
|
|
181
|
+
return <BarChartView data={data as BarChartData[]} primaryColor={primaryColor} isStreaming={isStreaming} />;
|
|
152
182
|
case 'table':
|
|
153
183
|
return <TableView data={data as TableData} isStreaming={isStreaming} />;
|
|
154
184
|
case 'product_carousel':
|
|
@@ -204,7 +234,12 @@ function PieChartView({ data, isStreaming }: { data: PieChartData[]; isStreaming
|
|
|
204
234
|
const chartData = data.map((item) => ({
|
|
205
235
|
name: item.label,
|
|
206
236
|
value: item.value,
|
|
237
|
+
inStockCount: item.inStockCount,
|
|
238
|
+
outOfStockCount: item.outOfStockCount,
|
|
207
239
|
}));
|
|
240
|
+
const hasStockBreakdown = chartData.some(
|
|
241
|
+
(item) => item.inStockCount !== undefined || item.outOfStockCount !== undefined,
|
|
242
|
+
);
|
|
208
243
|
|
|
209
244
|
return (
|
|
210
245
|
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm">
|
|
@@ -228,13 +263,52 @@ function PieChartView({ data, isStreaming }: { data: PieChartData[]; isStreaming
|
|
|
228
263
|
<Legend wrapperStyle={{ fontSize: 12 }} verticalAlign="bottom" />
|
|
229
264
|
</PieChart>
|
|
230
265
|
</ResponsiveContainer>
|
|
266
|
+
{hasStockBreakdown && (
|
|
267
|
+
<div className="mt-3 grid gap-2 sm:grid-cols-2">
|
|
268
|
+
{chartData.map((item, index) => {
|
|
269
|
+
const inStock = Number(item.inStockCount ?? 0);
|
|
270
|
+
const outOfStock = Number(item.outOfStockCount ?? 0);
|
|
271
|
+
const status = inStock > 0 ? `${inStock} in stock` : 'Out of stock';
|
|
272
|
+
const mutedStatus = outOfStock > 0 ? `${outOfStock} out` : null;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<div
|
|
276
|
+
key={item.name}
|
|
277
|
+
className="flex min-w-0 items-center justify-between gap-3 rounded-md border border-slate-100 px-2.5 py-2 text-xs dark:border-white/10"
|
|
278
|
+
>
|
|
279
|
+
<span className="flex min-w-0 items-center gap-2 text-slate-700 dark:text-white/75">
|
|
280
|
+
<span
|
|
281
|
+
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
|
282
|
+
style={{ background: getColor(index) }}
|
|
283
|
+
/>
|
|
284
|
+
<span className="truncate">{item.name}</span>
|
|
285
|
+
</span>
|
|
286
|
+
<span className={inStock > 0 ? 'whitespace-nowrap font-medium text-emerald-600' : 'whitespace-nowrap font-medium text-rose-600'}>
|
|
287
|
+
{status}
|
|
288
|
+
{mutedStatus && <span className="ml-1 text-slate-400">({mutedStatus})</span>}
|
|
289
|
+
</span>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
})}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
231
295
|
</div>
|
|
232
296
|
);
|
|
233
297
|
}
|
|
234
298
|
|
|
235
299
|
// ─── Bar Chart ────────────────────────────────────────────────────────────────
|
|
236
300
|
|
|
237
|
-
function BarChartView({
|
|
301
|
+
function BarChartView({
|
|
302
|
+
data,
|
|
303
|
+
primaryColor,
|
|
304
|
+
isStreaming,
|
|
305
|
+
layout = 'horizontal',
|
|
306
|
+
}: {
|
|
307
|
+
data: BarChartData[];
|
|
308
|
+
primaryColor?: string;
|
|
309
|
+
isStreaming?: boolean;
|
|
310
|
+
layout?: 'horizontal' | 'vertical';
|
|
311
|
+
}) {
|
|
238
312
|
if (isStreaming) return <ChartSkeleton type="bar" />;
|
|
239
313
|
if (!Array.isArray(data) || data.length === 0) {
|
|
240
314
|
return <div className="text-xs text-red-500">Invalid bar chart data</div>;
|
|
@@ -253,15 +327,28 @@ function BarChartView({ data, primaryColor, isStreaming }: { data: BarChartData[
|
|
|
253
327
|
return (
|
|
254
328
|
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm overflow-x-auto">
|
|
255
329
|
<ResponsiveContainer width="100%" height={260}>
|
|
256
|
-
<BarChart
|
|
330
|
+
<BarChart
|
|
331
|
+
data={chartData}
|
|
332
|
+
layout={layout}
|
|
333
|
+
margin={{ top: 10, right: 10, left: layout === 'vertical' ? 20 : -20, bottom: 0 }}
|
|
334
|
+
>
|
|
257
335
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
336
|
+
{layout === 'vertical' ? (
|
|
337
|
+
<>
|
|
338
|
+
<XAxis type="number" tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={{ stroke: '#cbd5e1' }} />
|
|
339
|
+
<YAxis type="category" dataKey="category" width={110} tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={false} />
|
|
340
|
+
</>
|
|
341
|
+
) : (
|
|
342
|
+
<>
|
|
343
|
+
<XAxis
|
|
344
|
+
dataKey="category"
|
|
345
|
+
tick={{ fontSize: 11, fill: '#64748b' }}
|
|
346
|
+
tickLine={false}
|
|
347
|
+
axisLine={{ stroke: '#cbd5e1' }}
|
|
348
|
+
/>
|
|
349
|
+
<YAxis tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={false} />
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
265
352
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#f1f5f9' }} />
|
|
266
353
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
|
267
354
|
{hasStock ? (
|
|
@@ -278,6 +365,51 @@ function BarChartView({ data, primaryColor, isStreaming }: { data: BarChartData[
|
|
|
278
365
|
);
|
|
279
366
|
}
|
|
280
367
|
|
|
368
|
+
// ─── Scatter Plot ─────────────────────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
function ScatterPlotView({ data, primaryColor, isStreaming }: { data: ScatterPlotDataPoint[]; primaryColor?: string; isStreaming?: boolean }) {
|
|
371
|
+
if (isStreaming) return <ChartSkeleton type="bar" />;
|
|
372
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
373
|
+
return <div className="text-xs text-red-500">Invalid scatter plot data</div>;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm overflow-x-auto">
|
|
378
|
+
<ResponsiveContainer width="100%" height={260}>
|
|
379
|
+
<ScatterChart margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
380
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
|
381
|
+
<XAxis type="number" dataKey="x" tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={{ stroke: '#cbd5e1' }} />
|
|
382
|
+
<YAxis type="number" dataKey="y" tick={{ fontSize: 11, fill: '#64748b' }} tickLine={false} axisLine={false} />
|
|
383
|
+
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3' }} />
|
|
384
|
+
<Scatter name="Value" data={data} fill={primaryColor ?? getColor(0)} />
|
|
385
|
+
</ScatterChart>
|
|
386
|
+
</ResponsiveContainer>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ─── Metric Card ──────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
function MetricCardView({ data, isStreaming }: { data: MetricCardData; isStreaming?: boolean }) {
|
|
394
|
+
if (isStreaming) return <ChartSkeleton type="bar" />;
|
|
395
|
+
if (!data || typeof data.value !== 'number') {
|
|
396
|
+
return <div className="text-xs text-red-500">Invalid metric data</div>;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-slate-900">
|
|
401
|
+
<div className="text-xs font-medium uppercase text-slate-500 dark:text-slate-400">{data.label}</div>
|
|
402
|
+
<div className="mt-2 text-3xl font-semibold text-slate-900 dark:text-white">
|
|
403
|
+
{data.value.toLocaleString()}
|
|
404
|
+
{data.unit && <span className="ml-1 text-base text-slate-500">{data.unit}</span>}
|
|
405
|
+
</div>
|
|
406
|
+
{data.operation && (
|
|
407
|
+
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">{data.operation}</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
281
413
|
// ─── Line Chart ───────────────────────────────────────────────────────────────
|
|
282
414
|
|
|
283
415
|
function LineChartView({ data, primaryColor, isStreaming }: { data: LineChartDataPoint[]; primaryColor?: string; isStreaming?: boolean }) {
|
|
@@ -451,7 +583,7 @@ function CarouselView({
|
|
|
451
583
|
// ─── Text / Prose ─────────────────────────────────────────────────────────────
|
|
452
584
|
|
|
453
585
|
function TextView({ data }: { data: unknown }) {
|
|
454
|
-
const content = (data as { content?: string })?.content || String(data ?? '');
|
|
586
|
+
const content = stripLeakedAnswerWrappers((data as { content?: string })?.content || String(data ?? ''));
|
|
455
587
|
|
|
456
588
|
return (
|
|
457
589
|
<div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-white/10 p-4 shadow-sm">
|
|
@@ -51,24 +51,25 @@ export class EmbeddingStrategyResolver {
|
|
|
51
51
|
llmConfig: LLMConfig,
|
|
52
52
|
embeddingConfig?: EmbeddingConfig
|
|
53
53
|
): EmbeddingStrategy {
|
|
54
|
-
//
|
|
54
|
+
// No explicit embedding config — LLM provider must handle embeddings itself.
|
|
55
55
|
if (!embeddingConfig) {
|
|
56
56
|
return this.supportsEmbedding(llmConfig.provider)
|
|
57
57
|
? EmbeddingStrategy.INTEGRATED
|
|
58
58
|
: EmbeddingStrategy.SEPARATE;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Different provider for embeddings (e.g. Anthropic LLM + OpenAI embeddings).
|
|
62
62
|
if (embeddingConfig.provider !== (llmConfig.provider as EmbeddingProvider)) {
|
|
63
63
|
return EmbeddingStrategy.SEPARATE;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// Same provider, different model (e.g. OpenAI gpt-4o + text-embedding-3-small).
|
|
67
|
+
// Use EXTERNAL so the factory creates a dedicated embedding-model client.
|
|
67
68
|
if (embeddingConfig.model !== llmConfig.model) {
|
|
68
69
|
return EmbeddingStrategy.EXTERNAL;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
//
|
|
72
|
+
// Same provider, same model — fully integrated.
|
|
72
73
|
return EmbeddingStrategy.INTEGRATED;
|
|
73
74
|
}
|
|
74
75
|
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -44,8 +44,6 @@ export interface VectorDBConfig {
|
|
|
44
44
|
* - searchFields?: string[] | string // optional override for which columns receive exact-match boosts
|
|
45
45
|
*/
|
|
46
46
|
options: Record<string, unknown>;
|
|
47
|
-
/** Optional distance metric (defaults to cosine if not specified and supported by provider) */
|
|
48
|
-
distanceMetric?: 'cosine' | 'euclidean' | 'dotproduct';
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// ---------------------------------------------------------------------------
|
|
@@ -189,6 +187,16 @@ export interface RAGConfig {
|
|
|
189
187
|
* e.g., { "name": "ProductName", "price": "SalesPrice", "image": "ThumbnailUrl" }
|
|
190
188
|
*/
|
|
191
189
|
uiMapping?: Record<string, string>;
|
|
190
|
+
/**
|
|
191
|
+
* Custom keywords that signal graph-based retrieval should be used.
|
|
192
|
+
* Defaults to built-in list (relationship, connect, hierarchy, etc.).
|
|
193
|
+
*/
|
|
194
|
+
graphKeywords?: string[];
|
|
195
|
+
/**
|
|
196
|
+
* Custom keywords that signal vector-based retrieval should be used.
|
|
197
|
+
* Defaults to built-in list (find, search, tell me about, etc.).
|
|
198
|
+
*/
|
|
199
|
+
vectorKeywords?: string[];
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
// ---------------------------------------------------------------------------
|
|
@@ -126,12 +126,27 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
126
126
|
|
|
127
127
|
const embeddingApiKeyByProvider: Record<string, string | undefined> = {
|
|
128
128
|
openai: readString(env, 'OPENAI_API_KEY'),
|
|
129
|
+
anthropic: readString(env, 'ANTHROPIC_API_KEY'), // Anthropic needs a separate embedding provider; key kept for completeness
|
|
129
130
|
gemini: readString(env, 'GEMINI_API_KEY'),
|
|
130
131
|
ollama: undefined,
|
|
131
132
|
universal_rest: readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'OPENAI_API_KEY'),
|
|
132
133
|
custom: readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'OPENAI_API_KEY'),
|
|
133
134
|
};
|
|
134
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Provider-aware default models.
|
|
138
|
+
* Applied when LLM_MODEL env var is not explicitly set.
|
|
139
|
+
*/
|
|
140
|
+
const DEFAULT_MODEL_BY_PROVIDER: Record<string, string> = {
|
|
141
|
+
openai: 'gpt-4o',
|
|
142
|
+
anthropic: 'claude-3-5-sonnet-20241022',
|
|
143
|
+
gemini: 'gemini-2.0-flash',
|
|
144
|
+
ollama: 'llama3',
|
|
145
|
+
universal_rest: 'default',
|
|
146
|
+
rest: 'default',
|
|
147
|
+
custom: 'default',
|
|
148
|
+
};
|
|
149
|
+
|
|
135
150
|
return {
|
|
136
151
|
projectId,
|
|
137
152
|
vectorDb: {
|
|
@@ -144,7 +159,7 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
144
159
|
},
|
|
145
160
|
llm: {
|
|
146
161
|
provider: llmProvider,
|
|
147
|
-
model: readString(env, 'LLM_MODEL') ?? 'gpt-4o',
|
|
162
|
+
model: readString(env, 'LLM_MODEL') ?? DEFAULT_MODEL_BY_PROVIDER[llmProvider] ?? 'gpt-4o',
|
|
148
163
|
apiKey: llmApiKeyByProvider[llmProvider] ?? '',
|
|
149
164
|
baseUrl: readString(env, 'LLM_BASE_URL'),
|
|
150
165
|
systemPrompt: readString(env, 'LLM_SYSTEM_PROMPT'),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
2
|
+
import { LLMFactory } from '../llm/LLMFactory';
|
|
3
|
+
import { RagConfig, LLMConfig } from '../config/RagConfig';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provider-aware fast model defaults.
|
|
7
|
+
* Maps an LLM provider name to a sensible lightweight model for routing tasks.
|
|
8
|
+
*/
|
|
9
|
+
const FAST_MODEL_DEFAULTS: Record<string, string> = {
|
|
10
|
+
openai: 'gpt-4o-mini',
|
|
11
|
+
gemini: 'gemini-2.0-flash',
|
|
12
|
+
anthropic: 'claude-3-haiku-20240307',
|
|
13
|
+
ollama: '', // Ollama has no universal lightweight default — reuse main model
|
|
14
|
+
rest: '',
|
|
15
|
+
universal_rest: '',
|
|
16
|
+
custom: '',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* LLMRouter — manages multiple LLM instances for different task roles.
|
|
21
|
+
*
|
|
22
|
+
* Roles:
|
|
23
|
+
* - 'default' : the main configured LLM (used for chat generation)
|
|
24
|
+
* - 'fast' : a lightweight model for classification/routing tasks (UI decisions, strategy)
|
|
25
|
+
* - 'powerful': reserved for future use (defaults to main model)
|
|
26
|
+
*
|
|
27
|
+
* Provider-agnostic: will never mix provider SDKs (e.g., won't set a Gemini model name on an
|
|
28
|
+
* OpenAI client). If FAST_LLM_MODEL is set in the environment it is applied to the *same* provider.
|
|
29
|
+
*/
|
|
30
|
+
export class LLMRouter {
|
|
31
|
+
private models = new Map<string, ILLMProvider>();
|
|
32
|
+
|
|
33
|
+
constructor(private config: RagConfig) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize all LLM roles.
|
|
37
|
+
*
|
|
38
|
+
* @param prebuiltDefault - optional pre-built provider (from EmbeddingStrategyResolver).
|
|
39
|
+
* When provided it is used directly as the 'default' role without re-constructing.
|
|
40
|
+
*/
|
|
41
|
+
async initialize(prebuiltDefault?: ILLMProvider): Promise<void> {
|
|
42
|
+
// 1. Seed 'default' — prefer the pre-built provider so we don't double-construct it.
|
|
43
|
+
const defaultModel = prebuiltDefault ?? LLMFactory.create(this.config.llm, this.config.embedding);
|
|
44
|
+
this.models.set('default', defaultModel);
|
|
45
|
+
|
|
46
|
+
// 2. Resolve 'fast' model — always stays on the SAME provider to avoid SDK mismatches.
|
|
47
|
+
const envFastModel = process.env.FAST_LLM_MODEL;
|
|
48
|
+
const providerFastDefault = FAST_MODEL_DEFAULTS[this.config.llm.provider] ?? '';
|
|
49
|
+
|
|
50
|
+
const fastModelName = envFastModel || providerFastDefault;
|
|
51
|
+
|
|
52
|
+
if (fastModelName && fastModelName !== this.config.llm.model) {
|
|
53
|
+
console.log(`[LLMRouter] Fast role → provider="${this.config.llm.provider}" model="${fastModelName}"`);
|
|
54
|
+
const fastConfig: LLMConfig = {
|
|
55
|
+
...this.config.llm,
|
|
56
|
+
model: fastModelName,
|
|
57
|
+
};
|
|
58
|
+
this.models.set('fast', LLMFactory.create(fastConfig, this.config.embedding));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`[LLMRouter] Fast role → reusing default model (no lightweight alternative configured).`);
|
|
61
|
+
this.models.set('fast', defaultModel);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. 'powerful' defaults to main model (reserved for future use)
|
|
65
|
+
this.models.set('powerful', defaultModel);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve a model provider by its task role.
|
|
70
|
+
* Falls back to 'default' if the requested role is not registered.
|
|
71
|
+
*/
|
|
72
|
+
get(role: 'fast' | 'powerful' | 'default'): ILLMProvider {
|
|
73
|
+
const provider = this.models.get(role) ?? this.models.get('default');
|
|
74
|
+
if (!provider) {
|
|
75
|
+
throw new Error(`[LLMRouter] No provider registered for role: "${role}". Did you call initialize()?`);
|
|
76
|
+
}
|
|
77
|
+
return provider;
|
|
78
|
+
}
|
|
79
|
+
}
|