@retrivora-ai/rag-engine 1.8.1 → 1.8.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-BfRgI1Xh.d.mts → ILLMProvider-BOJFz3Na.d.mts} +47 -1
- package/dist/{ILLMProvider-BfRgI1Xh.d.ts → ILLMProvider-BOJFz3Na.d.ts} +47 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +509 -162
- package/dist/handlers/index.mjs +5849 -15
- package/dist/{index-1Z4GuYBi.d.ts → index-BwpcaziY.d.ts} +4 -2
- package/dist/{index-BV0z5mb6.d.mts → index-D3V9Et2M.d.mts} +4 -2
- package/dist/index.d.mts +30 -16
- package/dist/index.d.ts +30 -16
- package/dist/index.js +1999 -1210
- package/dist/index.mjs +2041 -1205
- package/dist/server.d.mts +21 -25
- package/dist/server.d.ts +21 -25
- package/dist/server.js +536 -163
- package/dist/server.mjs +5923 -67
- package/package.json +9 -7
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/globals.css +35 -11
- package/src/app/page.tsx +12 -321
- package/src/components/AmbientBackground.tsx +29 -0
- package/src/components/ArchitectureCard.tsx +17 -0
- package/src/components/ArchitectureCardsSection.tsx +15 -0
- package/src/components/ChatWidget.tsx +0 -1
- package/src/components/ChatWindow.tsx +32 -0
- package/src/components/CodeViewer.tsx +51 -0
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocViewer.tsx +37 -0
- package/src/components/DocumentUpload.tsx +44 -1
- package/src/components/Documentation.tsx +58 -0
- package/src/components/DynamicChart.tsx +27 -2
- package/src/components/Hero.tsx +59 -0
- package/src/components/HourglassLoader.tsx +87 -0
- package/src/components/Lifecycle.tsx +37 -0
- package/src/components/MarkdownComponents.tsx +143 -0
- package/src/components/MessageBubble.tsx +91 -1012
- package/src/components/Navbar.tsx +55 -0
- package/src/components/ObservabilityPanel.tsx +374 -0
- package/src/components/ProductCard.tsx +3 -1
- package/src/components/UIDispatcher.tsx +341 -0
- package/src/components/VisualizationRenderer.tsx +48 -26
- package/src/config/ConfigBuilder.ts +38 -1
- package/src/core/LangChainAgent.ts +1 -4
- package/src/core/Pipeline.ts +209 -86
- package/src/core/QueryProcessor.ts +65 -0
- package/src/handlers/index.ts +72 -12
- package/src/hooks/useRagChat.ts +19 -9
- package/src/index.ts +9 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
- package/src/rag/Reranker.ts +99 -6
- package/src/types/chat.ts +2 -0
- package/src/types/index.ts +52 -0
- package/src/types/props.ts +9 -1
- package/src/utils/ProductExtractor.ts +347 -0
- package/src/utils/UITransformer.ts +4 -53
- package/src/utils/synonyms.ts +78 -0
- package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
- package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
- package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +0 -8
- package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
- package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
- package/dist/RedisProvider-SR65SCKV.mjs +0 -8
- package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
- package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
- package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
- package/dist/chunk-5AJ4XHLW.mjs +0 -201
- package/dist/chunk-5YGUXK7Z.mjs +0 -80
- package/dist/chunk-BFYLQYQU.mjs +0 -3985
- package/dist/chunk-CFVEZTBJ.mjs +0 -102
- package/dist/chunk-IMP6FUCY.mjs +0 -30
- package/dist/chunk-LR3VMDVK.mjs +0 -157
- package/dist/chunk-M6JSPGAR.mjs +0 -117
- package/dist/chunk-PSFPZXHX.mjs +0 -245
- package/dist/chunk-R3RGUMHE.mjs +0 -218
- package/dist/chunk-U55XRW3U.mjs +0 -96
- package/dist/chunk-VUQJVIJT.mjs +0 -148
- package/dist/chunk-X4TOT24V.mjs +0 -89
- package/dist/chunk-YLTMFW4M.mjs +0 -49
package/src/handlers/index.ts
CHANGED
|
@@ -30,6 +30,11 @@ function sseUIFrame(uiTransformation: unknown): string {
|
|
|
30
30
|
return `data: ${JSON.stringify({ type: 'ui_transformation', data: uiTransformation })}\n\n`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Encode the observability trace as an SSE frame. */
|
|
34
|
+
function sseObservabilityFrame(trace: unknown): string {
|
|
35
|
+
return `data: ${JSON.stringify({ type: 'observability', data: trace })}\n\n`;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
/** Encode a stream error as an SSE frame. */
|
|
34
39
|
function sseErrorFrame(message: string): string {
|
|
35
40
|
return `data: ${JSON.stringify({ type: 'error', error: message })}\n\n`;
|
|
@@ -153,12 +158,14 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
153
158
|
enqueue(sseMetaFrame(chunk));
|
|
154
159
|
|
|
155
160
|
// ── LLM-driven visualization decision ─────────────────────────
|
|
156
|
-
// Sends question + RAG context to the LLM and asks it to choose
|
|
157
|
-
// the best visualization format (bar_chart, pie_chart, table…).
|
|
158
|
-
// Falls back to keyword heuristic if the LLM call fails.
|
|
159
161
|
const responseChunk = chunk as ChatResponse;
|
|
160
162
|
const sources = responseChunk?.sources || [];
|
|
161
163
|
|
|
164
|
+
// Emit observability trace if present
|
|
165
|
+
if (responseChunk?.trace) {
|
|
166
|
+
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
167
|
+
}
|
|
168
|
+
|
|
162
169
|
if (sources.length > 0) {
|
|
163
170
|
try {
|
|
164
171
|
const llmProvider = plugin.getLLMProvider?.();
|
|
@@ -172,7 +179,6 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
172
179
|
}
|
|
173
180
|
} catch (transformError) {
|
|
174
181
|
console.warn('[createStreamHandler] UI transformation warning:', transformError);
|
|
175
|
-
// Last-resort fallback: heuristic transform
|
|
176
182
|
try {
|
|
177
183
|
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
178
184
|
if (fallback) enqueue(sseUIFrame(fallback));
|
|
@@ -265,15 +271,68 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
265
271
|
const formData = await req.formData();
|
|
266
272
|
const files = formData.getAll('files') as File[];
|
|
267
273
|
const namespace = (formData.get('namespace') as string) || undefined;
|
|
274
|
+
const dimensionRaw = formData.get('dimension');
|
|
275
|
+
const dimension = dimensionRaw ? parseInt(dimensionRaw as string, 10) : undefined;
|
|
268
276
|
|
|
269
277
|
if (!files || files.length === 0) {
|
|
270
278
|
return NextResponse.json({ error: 'No files provided' }, { status: 400 });
|
|
271
279
|
}
|
|
272
280
|
|
|
273
|
-
const documents =
|
|
274
|
-
|
|
281
|
+
const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
|
|
282
|
+
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
if (file.name.toLowerCase().endsWith('.csv') || file.type === 'text/csv') {
|
|
285
|
+
const text = await file.text();
|
|
286
|
+
const Papa = await import('papaparse');
|
|
287
|
+
const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
|
|
288
|
+
|
|
289
|
+
if (parsed.data && parsed.data.length > 0) {
|
|
290
|
+
let i = 0;
|
|
291
|
+
let lastRowData: Record<string, string> | null = null;
|
|
292
|
+
|
|
293
|
+
for (const row of parsed.data) {
|
|
294
|
+
i++;
|
|
295
|
+
const rowData = row as Record<string, string>;
|
|
296
|
+
|
|
297
|
+
// Dynamically determine the grouping key (typically the first column like ID or Handle)
|
|
298
|
+
const groupingKey = Object.keys(rowData)[0];
|
|
299
|
+
|
|
300
|
+
// Forward-fill ALL empty columns for variants belonging to the same group
|
|
301
|
+
if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
|
|
302
|
+
for (const key of Object.keys(rowData)) {
|
|
303
|
+
// Forward-fill if the current row lacks the field but the previous row has it
|
|
304
|
+
if (!rowData[key] && lastRowData[key]) {
|
|
305
|
+
rowData[key] = lastRowData[key];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Save a copy of the filled rowData for the next iteration
|
|
310
|
+
lastRowData = { ...rowData };
|
|
311
|
+
|
|
312
|
+
const contentParts: string[] = [];
|
|
313
|
+
for (const [key, val] of Object.entries(rowData)) {
|
|
314
|
+
if (key && val) {
|
|
315
|
+
contentParts.push(`${key}: ${val}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
documents.push({
|
|
320
|
+
docId: `${file.name}-row-${i}`,
|
|
321
|
+
content: contentParts.join(', '),
|
|
322
|
+
metadata: {
|
|
323
|
+
fileName: file.name,
|
|
324
|
+
fileSize: file.size,
|
|
325
|
+
fileType: file.type,
|
|
326
|
+
uploadedAt: new Date().toISOString(),
|
|
327
|
+
...(dimension ? { dimension } : {}),
|
|
328
|
+
...rowData
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
275
334
|
const content = await DocumentParser.parse(file, file.name, file.type);
|
|
276
|
-
|
|
335
|
+
documents.push({
|
|
277
336
|
docId: file.name,
|
|
278
337
|
content,
|
|
279
338
|
metadata: {
|
|
@@ -281,10 +340,11 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
281
340
|
fileSize: file.size,
|
|
282
341
|
fileType: file.type,
|
|
283
342
|
uploadedAt: new Date().toISOString(),
|
|
343
|
+
...(dimension ? { dimension } : {}),
|
|
284
344
|
},
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
288
348
|
|
|
289
349
|
const results = await plugin.ingest(documents, namespace);
|
|
290
350
|
return NextResponse.json({ message: 'Upload successful', results });
|
|
@@ -325,5 +385,5 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
|
|
|
325
385
|
};
|
|
326
386
|
}
|
|
327
387
|
|
|
328
|
-
// Re-export SSE
|
|
329
|
-
export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame };
|
|
388
|
+
// Re-export SSE helpers so host apps can use them in custom handlers
|
|
389
|
+
export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame, sseObservabilityFrame };
|
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -19,16 +19,17 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
22
|
-
import { VectorMatch, RagMessage, UseRagChatOptions, UseRagChatReturn } from '../types';
|
|
22
|
+
import { VectorMatch, RagMessage, UseRagChatOptions, UseRagChatReturn, ObservabilityTrace } from '../types';
|
|
23
23
|
import { useStoredMessages } from './useStoredMessages';
|
|
24
24
|
|
|
25
25
|
// ─── SSE Frame Parser ──────────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
interface SseTextFrame
|
|
28
|
-
interface SseMetaFrame
|
|
29
|
-
interface SseErrorFrame
|
|
30
|
-
interface SseUiFrame
|
|
31
|
-
|
|
27
|
+
interface SseTextFrame { type: 'text'; text: string }
|
|
28
|
+
interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[] }
|
|
29
|
+
interface SseErrorFrame { type: 'error'; error: string }
|
|
30
|
+
interface SseUiFrame { type: 'ui_transformation'; data: unknown }
|
|
31
|
+
interface SseObservabilityFrame { type: 'observability'; data: ObservabilityTrace }
|
|
32
|
+
type SseFrame = SseTextFrame | SseMetaFrame | SseErrorFrame | SseUiFrame | SseObservabilityFrame;
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Parse all complete SSE frames from a raw chunk string.
|
|
@@ -126,6 +127,7 @@ export function useRagChat(
|
|
|
126
127
|
let assistantContent = '';
|
|
127
128
|
let sources: VectorMatch[] = [];
|
|
128
129
|
let uiTransformation: unknown = null;
|
|
130
|
+
let trace: ObservabilityTrace | undefined;
|
|
129
131
|
// Buffer for partial SSE frames that arrive split across reads
|
|
130
132
|
let buffer = '';
|
|
131
133
|
|
|
@@ -161,6 +163,8 @@ export function useRagChat(
|
|
|
161
163
|
sources = frame.sources ?? [];
|
|
162
164
|
} else if (frame.type === 'ui_transformation') {
|
|
163
165
|
uiTransformation = frame.data;
|
|
166
|
+
} else if (frame.type === 'observability') {
|
|
167
|
+
trace = (frame as SseObservabilityFrame).data;
|
|
164
168
|
} else if (frame.type === 'error') {
|
|
165
169
|
setError(frame.error || 'Stream error');
|
|
166
170
|
}
|
|
@@ -186,18 +190,24 @@ export function useRagChat(
|
|
|
186
190
|
for (const frame of parseSseChunk(buffer)) {
|
|
187
191
|
if (frame.type === 'text' && frame.text) assistantContent += frame.text;
|
|
188
192
|
else if (frame.type === 'metadata') sources = frame.sources ?? [];
|
|
193
|
+
else if (frame.type === 'observability') trace = (frame as SseObservabilityFrame).data;
|
|
189
194
|
}
|
|
190
195
|
}
|
|
191
196
|
|
|
192
|
-
|
|
197
|
+
// Final message with all accumulated data
|
|
198
|
+
const finalMsg: RagMessage = {
|
|
193
199
|
id: assistantMessageId,
|
|
194
200
|
role: 'assistant',
|
|
195
201
|
content: assistantContent,
|
|
196
202
|
sources: sources.length > 0 ? sources : undefined,
|
|
197
|
-
uiTransformation: uiTransformation
|
|
203
|
+
uiTransformation: uiTransformation ?? undefined,
|
|
204
|
+
trace,
|
|
198
205
|
createdAt: new Date().toISOString(),
|
|
199
206
|
};
|
|
200
|
-
|
|
207
|
+
setMessages((prev) =>
|
|
208
|
+
prev.map((msg) => (msg.id === assistantMessageId ? finalMsg : msg))
|
|
209
|
+
);
|
|
210
|
+
onReply?.(finalMsg);
|
|
201
211
|
} catch (err) {
|
|
202
212
|
const msg = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
203
213
|
setError(msg);
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,15 @@ export { ChatWindow } from './components/ChatWindow';
|
|
|
9
9
|
export { DocumentUpload } from './components/DocumentUpload';
|
|
10
10
|
export { MessageBubble } from './components/MessageBubble';
|
|
11
11
|
export { SourceCard } from './components/SourceCard';
|
|
12
|
+
export { ObservabilityPanel } from './components/ObservabilityPanel';
|
|
12
13
|
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
13
14
|
|
|
14
15
|
// ── Headless Hooks ────────────────────────────────────────────
|
|
15
16
|
export { useRagChat } from './hooks/useRagChat';
|
|
16
17
|
|
|
18
|
+
// ── Utils ─────────────────────────────────────────────────────
|
|
19
|
+
export { addSynonyms } from './utils/synonyms';
|
|
20
|
+
|
|
17
21
|
// ── Types (Interfaces/Types only, no Node.js deps) ─────────────
|
|
18
22
|
export type { RagConfig, VectorDBConfig, VectorDBProvider, LLMConfig, LLMProvider, EmbeddingConfig, EmbeddingProvider, UIConfig, RAGConfig } from './config/RagConfig';
|
|
19
23
|
export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
|
|
@@ -35,5 +39,9 @@ export type {
|
|
|
35
39
|
ProductCardProps,
|
|
36
40
|
ProductCarouselProps,
|
|
37
41
|
DocumentUploadProps,
|
|
38
|
-
SourceCardProps
|
|
42
|
+
SourceCardProps,
|
|
43
|
+
ObservabilityTrace,
|
|
44
|
+
LatencyBreakdown,
|
|
45
|
+
TokenUsage,
|
|
46
|
+
RetrievedChunk
|
|
39
47
|
} from './types';
|
|
@@ -21,6 +21,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
21
21
|
private readonly connectionString: string;
|
|
22
22
|
private tables: string[];
|
|
23
23
|
private searchFields: string[];
|
|
24
|
+
private readonly uploadTable: string;
|
|
24
25
|
|
|
25
26
|
constructor(config: VectorDBConfig) {
|
|
26
27
|
super(config);
|
|
@@ -31,22 +32,8 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
31
32
|
this.connectionString = opts.connectionString as string;
|
|
32
33
|
this.dimensions = (opts.dimensions as number) ?? 768;
|
|
33
34
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
(opts.tables as string | string[]) ??
|
|
37
|
-
process.env.VECTOR_DB_TABLES ??
|
|
38
|
-
'';
|
|
39
|
-
|
|
40
|
-
this.tables = typeof rawTables === 'string'
|
|
41
|
-
? rawTables.split(',').map((t) => t.trim()).filter(Boolean)
|
|
42
|
-
: rawTables;
|
|
43
|
-
|
|
44
|
-
if (this.tables.length === 0) {
|
|
45
|
-
console.warn(
|
|
46
|
-
'[MultiTablePostgresProvider] No tables configured. ' +
|
|
47
|
-
'Set VECTOR_DB_TABLES as a comma-separated list of table names or pass options.tables.'
|
|
48
|
-
);
|
|
49
|
-
}
|
|
35
|
+
// The provider will dynamically discover tables during initialize().
|
|
36
|
+
this.tables = [];
|
|
50
37
|
|
|
51
38
|
// Exact match fields can come from options.searchFields or be derived from VECTOR_DB_SEARCH_FIELDS env var
|
|
52
39
|
const rawSearchFields: string | string[] =
|
|
@@ -57,6 +44,8 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
57
44
|
this.searchFields = typeof rawSearchFields === 'string'
|
|
58
45
|
? rawSearchFields.split(',').map((f) => f.trim()).filter(Boolean)
|
|
59
46
|
: rawSearchFields;
|
|
47
|
+
|
|
48
|
+
this.uploadTable = (opts.uploadTable as string) || 'document_chunks';
|
|
60
49
|
}
|
|
61
50
|
|
|
62
51
|
async initialize(): Promise<void> {
|
|
@@ -65,6 +54,24 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
65
54
|
// Dynamically discover all tables that have an 'embedding' column
|
|
66
55
|
const client: PoolClient = await this.pool.connect();
|
|
67
56
|
try {
|
|
57
|
+
await client.query('CREATE EXTENSION IF NOT EXISTS vector');
|
|
58
|
+
|
|
59
|
+
// Ensure the upload table exists
|
|
60
|
+
await client.query(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS ${this.uploadTable} (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
metadata JSONB,
|
|
66
|
+
embedding VECTOR(${this.dimensions})
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
await client.query(`
|
|
70
|
+
CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
|
|
71
|
+
ON ${this.uploadTable}
|
|
72
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
73
|
+
`);
|
|
74
|
+
|
|
68
75
|
const res = await client.query(`
|
|
69
76
|
SELECT table_name
|
|
70
77
|
FROM information_schema.columns
|
|
@@ -72,21 +79,8 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
72
79
|
AND table_schema = 'public'
|
|
73
80
|
`);
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (this.tables.length === 0) {
|
|
78
|
-
// No static tables configured, use everything discovered
|
|
79
|
-
this.tables = discoveredTables;
|
|
80
|
-
} else {
|
|
81
|
-
// Filter configured tables to ensure they actually exist with embeddings
|
|
82
|
-
const staticTables = [...this.tables];
|
|
83
|
-
this.tables = staticTables.filter(t => discoveredTables.includes(t));
|
|
84
|
-
|
|
85
|
-
if (this.tables.length < staticTables.length) {
|
|
86
|
-
const missing = staticTables.filter(t => !discoveredTables.includes(t));
|
|
87
|
-
console.warn(`[MultiTablePostgresProvider] Some configured tables were skipped (missing 'embedding' column): ${missing.join(', ')}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
82
|
+
// Dynamically use all tables that have an embedding column
|
|
83
|
+
this.tables = res.rows.map(r => r.table_name);
|
|
90
84
|
|
|
91
85
|
if (this.tables.length === 0) {
|
|
92
86
|
console.warn('[MultiTablePostgresProvider] No tables with "embedding" columns found in the database.');
|
|
@@ -101,23 +95,125 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
101
95
|
}
|
|
102
96
|
|
|
103
97
|
/**
|
|
104
|
-
* Upsert
|
|
105
|
-
* searching across pre-existing tables with varying schemas.
|
|
98
|
+
* Upsert a document by dynamically provisioning a table and columns.
|
|
106
99
|
*/
|
|
107
|
-
async upsert(
|
|
108
|
-
|
|
109
|
-
'[MultiTablePostgresProvider] upsert() is not supported in multi-table mode. ' +
|
|
110
|
-
'Please use the standard PostgreSQLProvider for single-table managed indices.'
|
|
111
|
-
);
|
|
100
|
+
async upsert(doc: UpsertDocument, namespace = ''): Promise<void> {
|
|
101
|
+
await this.batchUpsert([doc], namespace);
|
|
112
102
|
}
|
|
113
103
|
|
|
114
104
|
/**
|
|
115
|
-
* Batch upsert
|
|
105
|
+
* Batch upsert documents by dynamically provisioning tables and columns based on CSV headers.
|
|
116
106
|
*/
|
|
117
|
-
async batchUpsert(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
async batchUpsert(docs: UpsertDocument[], namespace = ''): Promise<void> {
|
|
108
|
+
if (docs.length === 0) return;
|
|
109
|
+
|
|
110
|
+
// Group docs by fileName to support dynamic table creation per CSV
|
|
111
|
+
const docsByFile: Record<string, UpsertDocument[]> = {};
|
|
112
|
+
for (const doc of docs) {
|
|
113
|
+
const fileName = (doc.metadata?.fileName as string) || this.uploadTable;
|
|
114
|
+
if (!docsByFile[fileName]) docsByFile[fileName] = [];
|
|
115
|
+
docsByFile[fileName].push(doc);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const client = await this.pool.connect();
|
|
119
|
+
try {
|
|
120
|
+
await client.query('BEGIN');
|
|
121
|
+
|
|
122
|
+
for (const [fileName, fileDocs] of Object.entries(docsByFile)) {
|
|
123
|
+
// Sanitize table name (e.g., "jewelery.csv" -> "jewelery")
|
|
124
|
+
let tableName = fileName;
|
|
125
|
+
if (tableName.toLowerCase().endsWith('.csv')) {
|
|
126
|
+
tableName = tableName.slice(0, -4);
|
|
127
|
+
}
|
|
128
|
+
tableName = tableName.replace(/[^a-z0-9_]/gi, '_').toLowerCase() || this.uploadTable;
|
|
129
|
+
|
|
130
|
+
// Extract headers from the first doc's metadata
|
|
131
|
+
const firstMeta = fileDocs[0].metadata || {};
|
|
132
|
+
const systemKeys = ['fileName', 'fileSize', 'fileType', 'uploadedAt', 'dimension', 'chunkIndex', 'id', 'namespace', 'content', 'metadata', 'embedding'];
|
|
133
|
+
const csvHeaders = Object.keys(firstMeta).filter(k => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
|
|
134
|
+
|
|
135
|
+
// 1. Create Table Dynamically
|
|
136
|
+
const columnDefs = csvHeaders.map(h => `"${h}" TEXT`).join(',\n ');
|
|
137
|
+
const createTableSql = `
|
|
138
|
+
CREATE TABLE IF NOT EXISTS "${tableName}" (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
${columnDefs ? columnDefs + ',' : ''}
|
|
141
|
+
namespace TEXT NOT NULL DEFAULT '',
|
|
142
|
+
content TEXT NOT NULL,
|
|
143
|
+
metadata JSONB,
|
|
144
|
+
embedding VECTOR(${this.dimensions})
|
|
145
|
+
)
|
|
146
|
+
`;
|
|
147
|
+
await client.query(createTableSql);
|
|
148
|
+
|
|
149
|
+
// Create Index
|
|
150
|
+
await client.query(`
|
|
151
|
+
CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
|
|
152
|
+
ON "${tableName}"
|
|
153
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
// Ensure this new table is added to our search scope
|
|
157
|
+
if (!this.tables.includes(tableName)) {
|
|
158
|
+
this.tables.push(tableName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 2. Insert Data
|
|
162
|
+
const BATCH_SIZE = 50;
|
|
163
|
+
for (let i = 0; i < fileDocs.length; i += BATCH_SIZE) {
|
|
164
|
+
const batch = fileDocs.slice(i, i + BATCH_SIZE);
|
|
165
|
+
const values: unknown[] = [];
|
|
166
|
+
|
|
167
|
+
const headerColumns = csvHeaders.map(h => `"${h}"`).join(', ');
|
|
168
|
+
const allColumns = `id, ${headerColumns ? headerColumns + ', ' : ''}namespace, content, metadata, embedding`;
|
|
169
|
+
|
|
170
|
+
const valuePlaceholders = batch.map((doc, idx) => {
|
|
171
|
+
const offset = idx * (5 + csvHeaders.length);
|
|
172
|
+
|
|
173
|
+
values.push(doc.id);
|
|
174
|
+
// Push header values
|
|
175
|
+
for (const h of csvHeaders) {
|
|
176
|
+
values.push(String(doc.metadata?.[h] || ''));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
values.push(namespace, doc.content, JSON.stringify(doc.metadata ?? {}), `[${doc.vector.join(',')}]`);
|
|
180
|
+
|
|
181
|
+
const docPlaceholders = [];
|
|
182
|
+
for (let j = 1; j <= 5 + csvHeaders.length; j++) {
|
|
183
|
+
if (j === 5 + csvHeaders.length) {
|
|
184
|
+
docPlaceholders.push(`$${offset + j}::vector`);
|
|
185
|
+
} else {
|
|
186
|
+
docPlaceholders.push(`$${offset + j}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `(${docPlaceholders.join(', ')})`;
|
|
191
|
+
}).join(', ');
|
|
192
|
+
|
|
193
|
+
const conflictUpdates = csvHeaders.map(h => `"${h}" = EXCLUDED."${h}"`).join(',\n ');
|
|
194
|
+
|
|
195
|
+
const query = `
|
|
196
|
+
INSERT INTO "${tableName}" (${allColumns})
|
|
197
|
+
VALUES ${valuePlaceholders}
|
|
198
|
+
ON CONFLICT (id) DO UPDATE
|
|
199
|
+
SET ${conflictUpdates ? conflictUpdates + ',' : ''}
|
|
200
|
+
namespace = EXCLUDED.namespace,
|
|
201
|
+
content = EXCLUDED.content,
|
|
202
|
+
metadata = EXCLUDED.metadata,
|
|
203
|
+
embedding = EXCLUDED.embedding
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
await client.query(query, values);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await client.query('COMMIT');
|
|
211
|
+
} catch (error) {
|
|
212
|
+
await client.query('ROLLBACK');
|
|
213
|
+
throw error;
|
|
214
|
+
} finally {
|
|
215
|
+
client.release();
|
|
216
|
+
}
|
|
121
217
|
}
|
|
122
218
|
|
|
123
219
|
/**
|
|
@@ -258,30 +354,20 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
258
354
|
allResults.push(...tableResults);
|
|
259
355
|
}
|
|
260
356
|
|
|
261
|
-
// 1.
|
|
262
|
-
|
|
263
|
-
for (const res of allResults) {
|
|
264
|
-
const table = res.metadata?.source_table as string;
|
|
265
|
-
if (!resultsByTable[table]) resultsByTable[table] = [];
|
|
266
|
-
resultsByTable[table].push(res);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// 2. Take top 3 from EACH table guaranteed, then fill the rest with overall best
|
|
270
|
-
const balancedResults: VectorMatch[] = [];
|
|
271
|
-
const tables = Object.keys(resultsByTable);
|
|
357
|
+
// 1. Sort all results globally by semantic similarity score
|
|
358
|
+
allResults.sort((a, b) => b.score - a.score);
|
|
272
359
|
|
|
273
|
-
|
|
274
|
-
for (const table of tables) {
|
|
275
|
-
balancedResults.push(...resultsByTable[table].slice(0, 3));
|
|
276
|
-
}
|
|
360
|
+
if (allResults.length === 0) return [];
|
|
277
361
|
|
|
278
|
-
//
|
|
279
|
-
const
|
|
362
|
+
// 2. Identify the "Active Table Context" from the absolute highest scoring match
|
|
363
|
+
const bestMatchTable = allResults[0].metadata?.source_table as string;
|
|
364
|
+
|
|
365
|
+
// 3. Filter out all results that don't belong to the active table to prevent cross-contamination
|
|
366
|
+
const finalSorted = allResults.filter(res => res.metadata?.source_table === bestMatchTable);
|
|
280
367
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
368
|
+
console.log(`[MultiTablePostgresProvider] --- Search Complete ---`);
|
|
369
|
+
console.log(`[MultiTablePostgresProvider] Final top match from "${bestMatchTable}" with score ${finalSorted[0].score.toFixed(4)}`);
|
|
370
|
+
console.log(`[MultiTablePostgresProvider] Restricting recommendations strictly to table: "${bestMatchTable}"`);
|
|
285
371
|
|
|
286
372
|
return finalSorted.slice(0, Math.max(topK, 15));
|
|
287
373
|
}
|
package/src/rag/Reranker.ts
CHANGED
|
@@ -1,18 +1,111 @@
|
|
|
1
1
|
import { VectorMatch } from '../types';
|
|
2
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
2
3
|
|
|
3
4
|
export class Reranker {
|
|
4
5
|
/**
|
|
5
|
-
* Re-ranks matches based on a secondary relevance score.
|
|
6
|
-
* In a production environment, this would call a Cross-Encoder model.
|
|
7
|
-
* Here we implement a placeholder that filters by score and limits count.
|
|
6
|
+
* Re-ranks matches based on a secondary relevance score using an LLM.
|
|
8
7
|
*/
|
|
9
8
|
async rerank(
|
|
10
9
|
matches: VectorMatch[],
|
|
11
10
|
query: string,
|
|
12
|
-
limit: number = 5
|
|
11
|
+
limit: number = 5,
|
|
12
|
+
llm?: ILLMProvider
|
|
13
13
|
): Promise<VectorMatch[]> {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if (!llm || matches.length <= 1) {
|
|
15
|
+
// Fallback to deterministic keyword-based re-ranking for production speed
|
|
16
|
+
const keywords = query.toLowerCase()
|
|
17
|
+
.replace(/[^\w\s]/g, '')
|
|
18
|
+
.split(/\s+/)
|
|
19
|
+
.filter(w => w.length > 2); // Filter out short words/stop words
|
|
20
|
+
|
|
21
|
+
if (keywords.length === 0) {
|
|
22
|
+
return matches
|
|
23
|
+
.sort((a, b) => b.score - a.score)
|
|
24
|
+
.slice(0, limit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const scoredMatches = matches.map(match => {
|
|
28
|
+
const contentLower = match.content.toLowerCase();
|
|
29
|
+
let keywordScore = 0;
|
|
30
|
+
|
|
31
|
+
keywords.forEach(keyword => {
|
|
32
|
+
if (contentLower.includes(keyword)) {
|
|
33
|
+
keywordScore += 1;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Normalize keyword score (fraction of keywords found)
|
|
38
|
+
const normalizedKeywordScore = keywordScore / keywords.length;
|
|
39
|
+
|
|
40
|
+
// Combine scores: Vector score (usually 0-1) + Keyword score weighted
|
|
41
|
+
// We give a significant weight to keyword matches to surface specific results
|
|
42
|
+
const combinedScore = match.score + (normalizedKeywordScore * 0.5);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...match,
|
|
46
|
+
score: combinedScore
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return scoredMatches
|
|
51
|
+
.sort((a, b) => b.score - a.score)
|
|
52
|
+
.slice(0, limit);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Take top 10 matches to avoid large context and high latency
|
|
57
|
+
const topN = matches.slice(0, 10);
|
|
58
|
+
|
|
59
|
+
const prompt = `You are a relevance ranking expert.
|
|
60
|
+
Given the following user query and a list of retrieved document chunks, rank the chunks by relevance to the query.
|
|
61
|
+
Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant.
|
|
62
|
+
Use the indices provided in brackets like [0], [1], etc.
|
|
63
|
+
Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.
|
|
64
|
+
|
|
65
|
+
Query: "${query}"
|
|
66
|
+
|
|
67
|
+
Documents:
|
|
68
|
+
${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}`;
|
|
69
|
+
|
|
70
|
+
const response = await llm.chat(
|
|
71
|
+
[{ role: 'user', content: prompt }],
|
|
72
|
+
'',
|
|
73
|
+
{ temperature: 0, maxTokens: 50 }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const cleanedResponse = response.trim().replace(/[\[\]\s]/g, '');
|
|
77
|
+
const rankedIndices = cleanedResponse.split(',').map(Number).filter(n => !isNaN(n));
|
|
78
|
+
|
|
79
|
+
if (rankedIndices.length > 0) {
|
|
80
|
+
const rankedMatches: VectorMatch[] = [];
|
|
81
|
+
const usedIndices = new Set<number>();
|
|
82
|
+
|
|
83
|
+
// Add matches in the order specified by the LLM
|
|
84
|
+
for (const index of rankedIndices) {
|
|
85
|
+
if (index >= 0 && index < topN.length && !usedIndices.has(index)) {
|
|
86
|
+
rankedMatches.push(topN[index]);
|
|
87
|
+
usedIndices.add(index);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add any remaining matches that the LLM might have missed
|
|
92
|
+
for (let i = 0; i < topN.length; i++) {
|
|
93
|
+
if (!usedIndices.has(i)) {
|
|
94
|
+
rankedMatches.push(topN[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add back the rest of the matches that weren't in the top N
|
|
99
|
+
const rest = matches.slice(10);
|
|
100
|
+
rankedMatches.push(...rest);
|
|
101
|
+
|
|
102
|
+
return rankedMatches.slice(0, limit);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn('[Reranker] LLM re-ranking failed, falling back to score-based sorting:', error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback if LLM fails or returns invalid response
|
|
16
109
|
return matches
|
|
17
110
|
.sort((a, b) => b.score - a.score)
|
|
18
111
|
.slice(0, limit);
|
package/src/types/chat.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RagMessage extends ChatMessage {
|
|
|
11
11
|
id: string;
|
|
12
12
|
sources?: VectorMatch[];
|
|
13
13
|
uiTransformation?: unknown;
|
|
14
|
+
/** Full observability trace emitted by the backend. Only present on assistant messages. */
|
|
15
|
+
trace?: import('./index').ObservabilityTrace;
|
|
14
16
|
createdAt: string;
|
|
15
17
|
}
|
|
16
18
|
|