@retrivora-ai/rag-engine 1.9.0 → 1.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ILLMProvider-BOJFz3Na.d.mts → ILLMProvider-Bw2A28nU.d.mts} +12 -0
- package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-Bw2A28nU.d.ts} +12 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1874 -542
- package/dist/handlers/index.mjs +1873 -541
- package/dist/{index-D3V9Et2M.d.mts → index-B70ZLkfG.d.mts} +1 -1
- package/dist/{index-BwpcaziY.d.ts → index-DVu-mkAM.d.ts} +1 -1
- package/dist/index.css +83 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +330 -106
- package/dist/index.mjs +333 -107
- package/dist/server.d.mts +32 -5
- package/dist/server.d.ts +32 -5
- package/dist/server.js +1871 -736
- package/dist/server.mjs +1870 -735
- package/package.json +1 -1
- package/src/components/ChatWindow.tsx +24 -14
- package/src/components/MarkdownComponents.tsx +3 -3
- package/src/components/MessageBubble.tsx +89 -7
- package/src/components/ProductCard.tsx +29 -2
- package/src/components/UIDispatcher.tsx +1 -0
- package/src/components/VisualizationRenderer.tsx +143 -11
- package/src/config/EmbeddingStrategy.ts +5 -4
- package/src/config/RagConfig.ts +10 -0
- package/src/config/serverConfig.ts +16 -1
- package/src/core/LLMRouter.ts +79 -0
- package/src/core/Pipeline.ts +295 -51
- package/src/core/ProviderRegistry.ts +6 -0
- package/src/core/QueryProcessor.ts +108 -9
- package/src/handlers/index.ts +37 -11
- package/src/hooks/useRagChat.ts +77 -17
- package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
- package/src/providers/vectordb/ChromaDBProvider.ts +13 -2
- package/src/providers/vectordb/MilvusProvider.ts +18 -2
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +48 -16
- package/src/providers/vectordb/PostgreSQLProvider.ts +1 -1
- package/src/providers/vectordb/QdrantProvider.ts +1 -1
- package/src/providers/vectordb/RedisProvider.ts +3 -4
- package/src/providers/vectordb/WeaviateProvider.ts +41 -3
- package/src/types/chat.ts +2 -0
- package/src/types/index.ts +26 -0
- package/src/utils/ProductExtractor.ts +5 -3
- package/src/utils/SchemaMapper.ts +6 -4
- package/src/utils/UITransformer.ts +1350 -490
- package/src/utils/synonyms.ts +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.2",
|
|
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
|
|
@@ -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) {
|
|
@@ -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';
|
|
@@ -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({
|
|
@@ -39,6 +61,17 @@ export function MessageBubble({
|
|
|
39
61
|
|
|
40
62
|
const [showSources, setShowSources] = React.useState(false);
|
|
41
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]);
|
|
42
75
|
|
|
43
76
|
const structuredContent = React.useMemo(
|
|
44
77
|
() => isUser ? { payload: null, text: message.content } : extractStructuredPayload(message.content),
|
|
@@ -56,6 +89,11 @@ export function MessageBubble({
|
|
|
56
89
|
return false;
|
|
57
90
|
}, [message.uiTransformation, structuredContent.payload]);
|
|
58
91
|
|
|
92
|
+
const uiTransformationType = (message.uiTransformation as UITransformationResponse | undefined)?.type;
|
|
93
|
+
const shouldSuppressProductCarousel = Boolean(
|
|
94
|
+
uiTransformationType && !['text', 'product_carousel', 'carousel'].includes(uiTransformationType),
|
|
95
|
+
);
|
|
96
|
+
|
|
59
97
|
const shouldRenderStructuredOnly = !isUser && hasRichUI && isLikelyUiOnlyMessage(structuredContent.text);
|
|
60
98
|
|
|
61
99
|
// ── Extraction ─────────────────────────────────────────────────────────────
|
|
@@ -89,6 +127,26 @@ export function MessageBubble({
|
|
|
89
127
|
|
|
90
128
|
const processedMarkdown = React.useMemo<string>(() => {
|
|
91
129
|
let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
|
|
130
|
+
|
|
131
|
+
// 0. Pre-sanitize: strip $ from non-monetary whole integers.
|
|
132
|
+
// Real prices always have a decimal (e.g. $257.00). A bare $168 or $9680811891595 is NOT a price.
|
|
133
|
+
raw = raw.replace(/\$\s*(\d+)(?!\.\d)/g, '$1');
|
|
134
|
+
raw = normalizePlusSeparatedLists(raw);
|
|
135
|
+
raw = stripLeakedAnswerWrappers(raw);
|
|
136
|
+
|
|
137
|
+
// 0a. Strip bold (**...**) from inline field labels.
|
|
138
|
+
// The LLM wraps field names like **Stock** — 168 or **EAN** — 9680811891595 in bold.
|
|
139
|
+
// We want to strip the ** so it renders as plain text, not bold.
|
|
140
|
+
raw = raw.replace(/\*\*([^*\n]+)\*\*(\s*[—\-–:]\s*)/g, '$1$2');
|
|
141
|
+
|
|
142
|
+
// 0b. Remove duplicate lines (LLM sometimes repeats fields like Stock, EAN twice)
|
|
143
|
+
raw = raw
|
|
144
|
+
.split('\n')
|
|
145
|
+
.filter((line, idx, arr) => {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
return !trimmed || arr.findIndex(l => l.trim() === trimmed) === idx;
|
|
148
|
+
})
|
|
149
|
+
.join('\n');
|
|
92
150
|
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
93
151
|
|
|
94
152
|
// 1. Structural fixes
|
|
@@ -140,7 +198,7 @@ export function MessageBubble({
|
|
|
140
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'}`}>
|
|
141
199
|
{/* Bubble */}
|
|
142
200
|
<div
|
|
143
|
-
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
|
|
144
202
|
? 'text-white rounded-tr-sm'
|
|
145
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'
|
|
146
204
|
}`}
|
|
@@ -148,6 +206,20 @@ export function MessageBubble({
|
|
|
148
206
|
isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
149
207
|
}
|
|
150
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
|
+
)}
|
|
151
223
|
{isStreaming && !message.content ? (
|
|
152
224
|
<span className="flex items-center gap-1 py-1">
|
|
153
225
|
<span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" />
|
|
@@ -193,11 +265,21 @@ export function MessageBubble({
|
|
|
193
265
|
if (isUser || structuredContent.payload || !message.uiTransformation) return null;
|
|
194
266
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
195
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
|
+
|
|
196
276
|
const shouldShow =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
+
|
|
201
283
|
if (!shouldShow) return null;
|
|
202
284
|
return (
|
|
203
285
|
<div className="w-full mt-3">
|
|
@@ -213,7 +295,7 @@ export function MessageBubble({
|
|
|
213
295
|
})()}
|
|
214
296
|
|
|
215
297
|
{/* Product Carousel */}
|
|
216
|
-
{!isUser && !hasRichUI && allProducts.length > 0 && (
|
|
298
|
+
{!isUser && !hasRichUI && !shouldSuppressProductCarousel && allProducts.length > 0 && (
|
|
217
299
|
<div className="w-full mt-1">
|
|
218
300
|
<ProductCarousel
|
|
219
301
|
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 */}
|
|
@@ -48,9 +75,9 @@ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }:
|
|
|
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
|
|
|
@@ -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
|
@@ -187,6 +187,16 @@ export interface RAGConfig {
|
|
|
187
187
|
* e.g., { "name": "ProductName", "price": "SalesPrice", "image": "ThumbnailUrl" }
|
|
188
188
|
*/
|
|
189
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[];
|
|
190
200
|
}
|
|
191
201
|
|
|
192
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'),
|