@retrivora-ai/rag-engine 1.9.3 → 1.9.6
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/README.md +27 -0
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-DNhyOYoK.d.mts} +41 -1
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-DNhyOYoK.d.ts} +41 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +552 -198
- package/dist/handlers/index.mjs +552 -198
- package/dist/index-C9v7-tWd.d.mts +183 -0
- package/dist/{index-B70ZLkfG.d.mts → index-CHL1jdYm.d.mts} +1 -1
- package/dist/index-CjQdL0cX.d.ts +183 -0
- package/dist/{index-DVu-mkAM.d.ts → index-Hgbwl9X4.d.ts} +1 -1
- package/dist/index.css +40 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +326 -158
- package/dist/index.mjs +312 -151
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +612 -198
- package/dist/server.mjs +604 -198
- package/package.json +1 -1
- package/src/app/api/chat/route.ts +2 -2
- package/src/app/api/health/route.ts +3 -2
- package/src/app/api/ingest/route.ts +3 -2
- package/src/app/api/suggestions/route.ts +3 -2
- package/src/app/api/upload/route.ts +3 -2
- package/src/app/constants.tsx +85 -30
- package/src/app/layout.tsx +18 -7
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/config/RagConfig.ts +47 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +56 -4
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +224 -67
- package/src/core/ProviderRegistry.ts +11 -2
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +51 -0
- package/src/exceptions/index.ts +59 -0
- package/src/handlers/index.ts +2 -0
- package/src/hooks/useRagChat.ts +26 -4
- package/src/index.ts +25 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +4 -1
- package/src/llm/providers/AnthropicProvider.ts +70 -20
- package/src/llm/providers/GeminiProvider.ts +14 -15
- package/src/llm/providers/OllamaProvider.ts +13 -16
- package/src/llm/providers/OpenAIProvider.ts +9 -14
- package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
- package/src/llm/utils.ts +46 -0
- package/src/providers/vectordb/MongoDBProvider.ts +9 -4
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
- package/src/rag/EntityExtractor.ts +2 -2
- package/src/rag/Reranker.ts +9 -16
- package/src/server.ts +27 -1
- package/src/types/chat.ts +7 -0
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
|
@@ -72,6 +72,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
72
72
|
ON ${this.uploadTable}
|
|
73
73
|
USING hnsw (embedding vector_cosine_ops)
|
|
74
74
|
`);
|
|
75
|
+
await client.query(`
|
|
76
|
+
CREATE INDEX IF NOT EXISTS "${this.uploadTable}_fts_idx"
|
|
77
|
+
ON "${this.uploadTable}"
|
|
78
|
+
USING gin (to_tsvector('english', content))
|
|
79
|
+
`);
|
|
75
80
|
|
|
76
81
|
const res = await client.query(`
|
|
77
82
|
SELECT table_name
|
|
@@ -158,6 +163,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
158
163
|
ON "${tableName}"
|
|
159
164
|
USING hnsw (embedding vector_cosine_ops)
|
|
160
165
|
`);
|
|
166
|
+
await client.query(`
|
|
167
|
+
CREATE INDEX IF NOT EXISTS "${tableName}_fts_idx"
|
|
168
|
+
ON "${tableName}"
|
|
169
|
+
USING gin (to_tsvector('english', content))
|
|
170
|
+
`);
|
|
161
171
|
|
|
162
172
|
// Ensure this new table is added to our search scope
|
|
163
173
|
if (!this.tables.includes(tableName)) {
|
|
@@ -297,9 +307,12 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
297
307
|
const synonyms = FIELD_SYNONYMS[key] ?? [];
|
|
298
308
|
const keysToCheck = [key, ...synonyms];
|
|
299
309
|
|
|
300
|
-
// Map each candidate key to LOWER(metadata->>'
|
|
301
|
-
const coalesceExprs = keysToCheck.
|
|
302
|
-
|
|
310
|
+
// Map each candidate key to LOWER(coalesce(metadata->>'key', to_jsonb(t)->>'key'))
|
|
311
|
+
const coalesceExprs = keysToCheck.flatMap(k => [
|
|
312
|
+
`metadata->>'${k}'`,
|
|
313
|
+
`to_jsonb(t)->>'${k}'`
|
|
314
|
+
]);
|
|
315
|
+
return `COALESCE(${coalesceExprs.map(expr => `LOWER(${expr})`).join(', ')}) = LOWER($${paramIdx})`;
|
|
303
316
|
});
|
|
304
317
|
whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
305
318
|
}
|
|
@@ -312,31 +325,50 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
312
325
|
CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(', ')})
|
|
313
326
|
THEN 1.0 ELSE 0.0 END
|
|
314
327
|
), 0)
|
|
315
|
-
FROM jsonb_each_text(
|
|
328
|
+
FROM jsonb_each_text(COALESCE(v.metadata, k.metadata)) AS kv(key, val)
|
|
316
329
|
WHERE key IN (${this.searchFields.map(f => `'${f}'`).join(', ')})
|
|
317
330
|
) * 3.0`
|
|
318
331
|
: '';
|
|
319
332
|
|
|
320
|
-
//
|
|
333
|
+
// Optimized Subquery CTE Hybrid Search (Uses HNSW vector index & GIN fulltext index)
|
|
321
334
|
sqlQuery = `
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
335
|
+
WITH vector_search AS (
|
|
336
|
+
SELECT *, (1 - (embedding <=> $1::vector)) AS vector_score
|
|
337
|
+
FROM "${table}" t
|
|
338
|
+
${whereClause}
|
|
339
|
+
ORDER BY embedding <=> $1::vector ASC
|
|
340
|
+
LIMIT ${tableLimit}
|
|
341
|
+
),
|
|
342
|
+
keyword_search AS (
|
|
343
|
+
SELECT *, COALESCE(ts_rank(to_tsvector('english', content), to_tsquery('english', $2)), 0) AS keyword_score
|
|
344
|
+
FROM "${table}" t
|
|
345
|
+
WHERE ${whereClause ? whereClause.replace('WHERE', '') : 'TRUE'}
|
|
346
|
+
AND to_tsvector('english', content) @@ to_tsquery('english', $2)
|
|
347
|
+
ORDER BY keyword_score DESC
|
|
348
|
+
LIMIT ${tableLimit}
|
|
349
|
+
)
|
|
350
|
+
SELECT
|
|
351
|
+
COALESCE(v.id, k.id) AS id,
|
|
352
|
+
COALESCE(v.namespace, k.namespace) AS namespace,
|
|
353
|
+
COALESCE(v.content, k.content) AS content,
|
|
354
|
+
COALESCE(v.metadata, k.metadata) AS metadata,
|
|
355
|
+
COALESCE(v.vector_score, 0) AS vector_score,
|
|
356
|
+
COALESCE(k.keyword_score, 0) AS keyword_score,
|
|
357
|
+
(COALESCE(v.vector_score, 0) + COALESCE(k.keyword_score, 0) * 2.0 ${exactNameScoreExpr}) AS hybrid_score
|
|
358
|
+
FROM vector_search v
|
|
359
|
+
FULL OUTER JOIN keyword_search k ON v.id = k.id
|
|
328
360
|
ORDER BY hybrid_score DESC
|
|
329
361
|
LIMIT ${tableLimit}
|
|
330
362
|
`;
|
|
331
363
|
params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
|
|
332
364
|
} else {
|
|
333
|
-
// Fallback to Vector-only Search
|
|
365
|
+
// Fallback to Vector-only Search (ordered ASC to use HNSW index)
|
|
334
366
|
sqlQuery = `
|
|
335
367
|
SELECT *,
|
|
336
368
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
337
369
|
FROM "${table}" t
|
|
338
370
|
${whereClause}
|
|
339
|
-
ORDER BY
|
|
371
|
+
ORDER BY embedding <=> $1::vector ASC
|
|
340
372
|
LIMIT ${tableLimit}
|
|
341
373
|
`;
|
|
342
374
|
params = [vectorLiteral, ...filterParams];
|
|
@@ -41,7 +41,7 @@ Text to extract from:
|
|
|
41
41
|
// 1. Try to find the JSON block using a regex
|
|
42
42
|
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
43
43
|
const cleanJson = jsonMatch ? jsonMatch[0] : response.trim();
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
return JSON.parse(cleanJson);
|
|
46
46
|
} catch {
|
|
47
47
|
// 2. If parsing fails, try to repair truncated JSON
|
|
@@ -62,7 +62,7 @@ Text to extract from:
|
|
|
62
62
|
*/
|
|
63
63
|
private repairTruncatedJson(json: string): string | null {
|
|
64
64
|
let text = json.trim();
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
// Find the first { and work from there
|
|
67
67
|
const startIdx = text.indexOf('{');
|
|
68
68
|
if (startIdx === -1) return null;
|
package/src/rag/Reranker.ts
CHANGED
|
@@ -6,8 +6,8 @@ export class Reranker {
|
|
|
6
6
|
* Re-ranks matches based on a secondary relevance score using an LLM.
|
|
7
7
|
*/
|
|
8
8
|
async rerank(
|
|
9
|
-
matches: VectorMatch[],
|
|
10
|
-
query: string,
|
|
9
|
+
matches: VectorMatch[],
|
|
10
|
+
query: string,
|
|
11
11
|
limit: number = 5,
|
|
12
12
|
llm?: ILLMProvider
|
|
13
13
|
): Promise<VectorMatch[]> {
|
|
@@ -27,7 +27,7 @@ export class Reranker {
|
|
|
27
27
|
const scoredMatches = matches.map(match => {
|
|
28
28
|
const contentLower = match.content.toLowerCase();
|
|
29
29
|
let keywordScore = 0;
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
keywords.forEach(keyword => {
|
|
32
32
|
if (contentLower.includes(keyword)) {
|
|
33
33
|
keywordScore += 1;
|
|
@@ -55,22 +55,15 @@ export class Reranker {
|
|
|
55
55
|
try {
|
|
56
56
|
// Take top 10 matches to avoid large context and high latency
|
|
57
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
58
|
|
|
70
59
|
const response = await llm.chat(
|
|
71
|
-
[{ role: 'user', content:
|
|
60
|
+
[{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}` }],
|
|
72
61
|
'',
|
|
73
|
-
{
|
|
62
|
+
{
|
|
63
|
+
systemPrompt: 'You are a relevance ranking expert. Given the user query and a list of retrieved document chunks, rank the chunks by relevance to the query. Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant. Use the indices provided in brackets like [0], [1], etc. Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.',
|
|
64
|
+
temperature: 0,
|
|
65
|
+
maxTokens: 50
|
|
66
|
+
}
|
|
74
67
|
);
|
|
75
68
|
|
|
76
69
|
const cleanedResponse = response.trim().replace(/[\[\]\s]/g, '');
|
package/src/server.ts
CHANGED
|
@@ -6,7 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
// ── Shared Types ──────────────────────────────────────────────
|
|
9
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
RagConfig,
|
|
11
|
+
UniversalRagConfig,
|
|
12
|
+
RetrievalConfig,
|
|
13
|
+
WorkflowConfig,
|
|
14
|
+
VectorDBConfig,
|
|
15
|
+
LLMConfig,
|
|
16
|
+
EmbeddingConfig,
|
|
17
|
+
UIConfig,
|
|
18
|
+
RAGConfig,
|
|
19
|
+
VectorDBProvider,
|
|
20
|
+
LLMProvider,
|
|
21
|
+
EmbeddingProvider,
|
|
22
|
+
} from './config/RagConfig';
|
|
10
23
|
export type { VectorMatch, UpsertDocument, IngestDocument, ChatResponse, ChatMessage, ChatOptions } from './types';
|
|
11
24
|
export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
|
|
12
25
|
export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
|
|
@@ -16,6 +29,7 @@ export type { BatchOptions, BatchResult } from './core/BatchProcessor';
|
|
|
16
29
|
|
|
17
30
|
// ── Core Orchestration ─────────────────────────────────────────
|
|
18
31
|
export { VectorPlugin } from './core/VectorPlugin';
|
|
32
|
+
export { Retrivora } from './core/Retrivora';
|
|
19
33
|
export { Pipeline } from './core/Pipeline';
|
|
20
34
|
export { ConfigResolver } from './core/ConfigResolver';
|
|
21
35
|
export { ProviderRegistry } from './core/ProviderRegistry';
|
|
@@ -23,6 +37,18 @@ export { ConfigValidator } from './core/ConfigValidator';
|
|
|
23
37
|
export { ProviderHealthCheck } from './core/ProviderHealthCheck';
|
|
24
38
|
export { BatchProcessor } from './core/BatchProcessor';
|
|
25
39
|
|
|
40
|
+
// ── Exceptions ────────────────────────────────────────────────
|
|
41
|
+
export {
|
|
42
|
+
RetrivoraError,
|
|
43
|
+
ProviderNotFoundException,
|
|
44
|
+
EmbeddingFailedException,
|
|
45
|
+
RetrievalException,
|
|
46
|
+
RateLimitException,
|
|
47
|
+
ConfigurationException,
|
|
48
|
+
AuthenticationException,
|
|
49
|
+
} from './exceptions';
|
|
50
|
+
export type { RetrivoraErrorCode } from './exceptions';
|
|
51
|
+
|
|
26
52
|
// ── Configuration Helpers ──────────────────────────────────────
|
|
27
53
|
export { getRagConfig } from './config/serverConfig';
|
|
28
54
|
export { ConfigBuilder, PRESETS, createFromPreset } from './config/ConfigBuilder';
|
package/src/types/chat.ts
CHANGED
|
@@ -14,6 +14,13 @@ export interface RagMessage extends ChatMessage {
|
|
|
14
14
|
/** Full observability trace emitted by the backend. Only present on assistant messages. */
|
|
15
15
|
trace?: import('./index').ObservabilityTrace;
|
|
16
16
|
createdAt: string;
|
|
17
|
+
/**
|
|
18
|
+
* Chain-of-thought reasoning from the LLM.
|
|
19
|
+
* Populated by native Anthropic extended thinking or simulated <think> tag stripping.
|
|
20
|
+
*/
|
|
21
|
+
thinking?: string;
|
|
22
|
+
/** Wall-clock ms the model spent in the thinking phase. */
|
|
23
|
+
thinkingMs?: number;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
export interface ChatOptions {
|
|
@@ -217,7 +217,7 @@ export class UITransformer {
|
|
|
217
217
|
// ── Step 1: Detect intent ───────────────────────────────────────────────
|
|
218
218
|
let intent: QueryIntent;
|
|
219
219
|
try {
|
|
220
|
-
intent = await this.detectIntent(query, llm);
|
|
220
|
+
intent = await this.detectIntent(query, llm, sources);
|
|
221
221
|
console.debug('[UITransformer] Detected intent:', intent);
|
|
222
222
|
} catch (err) {
|
|
223
223
|
console.warn('[UITransformer] Intent detection failed; using heuristic.', err);
|
|
@@ -239,11 +239,24 @@ export class UITransformer {
|
|
|
239
239
|
const context = this.buildContextSummary(sources);
|
|
240
240
|
const systemPrompt = this.buildVisualizationSystemPrompt();
|
|
241
241
|
|
|
242
|
+
const profile = this.profileData(sources);
|
|
243
|
+
const schemaContext = `
|
|
244
|
+
RETRIEVED DATA SCHEMA:
|
|
245
|
+
- Numeric fields (measures): ${profile.numericFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
246
|
+
- Category fields (dimensions): ${profile.categoricalFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
247
|
+
- Date fields (temporal): ${profile.dateFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
248
|
+
- Boolean fields: ${profile.booleanFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
249
|
+
- Text/Other fields: ${profile.fields.filter(f => f.kind === 'text').map(f => f.key).join(', ') || 'None'}
|
|
250
|
+
`;
|
|
251
|
+
|
|
242
252
|
const userPrompt = [
|
|
243
253
|
`USER QUESTION: ${query}`,
|
|
244
254
|
'',
|
|
245
255
|
`DETECTED INTENT (JSON): ${JSON.stringify(intent)}`,
|
|
246
256
|
'',
|
|
257
|
+
`RETRIEVED DATA SCHEMA:`,
|
|
258
|
+
schemaContext,
|
|
259
|
+
'',
|
|
247
260
|
'RETRIEVED DATA (JSON):',
|
|
248
261
|
context,
|
|
249
262
|
].join('\n');
|
|
@@ -256,6 +269,26 @@ export class UITransformer {
|
|
|
256
269
|
|
|
257
270
|
const parsed = this.parseTransformationResponse(rawResponse);
|
|
258
271
|
if (parsed) {
|
|
272
|
+
// Validate suitability of selected chart type based on schema
|
|
273
|
+
let isCompatible = true;
|
|
274
|
+
if (parsed.type === 'line_chart' && (profile.dateFields.length === 0 || profile.numericFields.length === 0)) {
|
|
275
|
+
isCompatible = false;
|
|
276
|
+
} else if (
|
|
277
|
+
['bar_chart', 'horizontal_bar', 'pie_chart', 'donut_chart'].includes(parsed.type) &&
|
|
278
|
+
(profile.categoricalFields.length === 0 && profile.numericFields.length === 0)
|
|
279
|
+
) {
|
|
280
|
+
isCompatible = false;
|
|
281
|
+
} else if (parsed.type === 'scatter_plot' && profile.numericFields.length < 2) {
|
|
282
|
+
isCompatible = false;
|
|
283
|
+
} else if (parsed.type === 'metric_card' && profile.numericFields.length === 0) {
|
|
284
|
+
isCompatible = false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!isCompatible) {
|
|
288
|
+
console.warn(`[UITransformer] LLM chose incompatible visualization type "${parsed.type}" for retrieved schema. Falling back to heuristic.`);
|
|
289
|
+
return this.transform(query, sources, undefined, undefined, intent);
|
|
290
|
+
}
|
|
291
|
+
|
|
259
292
|
const intentAllowsTable =
|
|
260
293
|
intent.wantsExplicitTable ||
|
|
261
294
|
['tabular', 'table', 'geographic'].includes(intent.visualizationHint);
|
|
@@ -295,7 +328,27 @@ export class UITransformer {
|
|
|
295
328
|
* - The intent object can be reused across both the heuristic and LLM paths.
|
|
296
329
|
* - It is easy to unit-test intent detection in isolation.
|
|
297
330
|
*/
|
|
298
|
-
static async detectIntent(query: string, llm: ILLMProvider): Promise<QueryIntent> {
|
|
331
|
+
static async detectIntent(query: string, llm: ILLMProvider, sources?: VectorMatch[]): Promise<QueryIntent> {
|
|
332
|
+
let schemaProfileText = '';
|
|
333
|
+
if (sources && sources.length > 0) {
|
|
334
|
+
const profile = this.profileData(sources);
|
|
335
|
+
const hasProducts = sources.some(item => this.isProductData(item));
|
|
336
|
+
schemaProfileText = `
|
|
337
|
+
RETRIEVED DATA SCHEMA:
|
|
338
|
+
- Numeric fields (measures): ${profile.numericFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
339
|
+
- Category fields (dimensions): ${profile.categoricalFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
340
|
+
- Date fields (temporal): ${profile.dateFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
341
|
+
- Boolean fields: ${profile.booleanFields.map(f => `${f.key} (unique values: ${f.uniqueCount})`).join(', ') || 'None'}
|
|
342
|
+
- Text fields: ${profile.fields.filter(f => f.kind === 'text').map(f => f.key).join(', ') || 'None'}
|
|
343
|
+
|
|
344
|
+
Please choose visualizationHint and recommendedChart dynamically based on the available schema.
|
|
345
|
+
- Do NOT recommend "trend" or "line_chart" if there are no Date fields.
|
|
346
|
+
- Do NOT recommend numeric-dependent visualizations (like bar_chart, line_chart, pie_chart, scatter_plot, histogram, metric_card) if there are no Numeric fields. Fall back to tabular/table or text.
|
|
347
|
+
- If there are multiple metadata fields but no clear categories or numbers, "tabular"/"table" is a highly accurate default.
|
|
348
|
+
${hasProducts ? `- Note: The retrieved data matches product/catalog structure. If the user query is looking for, exploring, searching, browsing, or listing products/items, you MUST recommend visualizationHint: "product_browse" and recommendedChart: "text".` : ''}
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
|
|
299
352
|
const systemPrompt = `You are an intent classifier for a product-search RAG system.
|
|
300
353
|
Given a user query, return ONLY a valid JSON object with this exact shape — no prose, no markdown:
|
|
301
354
|
|
|
@@ -332,8 +385,10 @@ RULES:
|
|
|
332
385
|
- isComparison: true if user compares, ranks, or contrasts entities or categories.
|
|
333
386
|
- language: detect from the query text itself; default "en" if uncertain.`;
|
|
334
387
|
|
|
388
|
+
const userPrompt = `QUERY: ${query}${schemaProfileText ? `\n\n${schemaProfileText}` : ''}`;
|
|
389
|
+
|
|
335
390
|
const rawResponse = await llm.chat(
|
|
336
|
-
[{ role: 'user', content:
|
|
391
|
+
[{ role: 'user', content: userPrompt }],
|
|
337
392
|
'',
|
|
338
393
|
{ systemPrompt, temperature: 0 },
|
|
339
394
|
);
|
|
@@ -894,14 +949,28 @@ RULES:
|
|
|
894
949
|
return asksForRecords && (hasNumericPredicate || hasEntityTerms);
|
|
895
950
|
}
|
|
896
951
|
|
|
952
|
+
/** @internal kept private for transform() internal logic */
|
|
897
953
|
private static isProductQuery(query: string): boolean {
|
|
954
|
+
return UITransformer._productQueryTest(query);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Public entry-point for external callers (e.g. Pipeline)
|
|
959
|
+
* to check whether a query maps to product_browse without triggering an LLM call.
|
|
960
|
+
*/
|
|
961
|
+
static isProductQueryPublic(query: string): boolean {
|
|
962
|
+
return UITransformer._productQueryTest(query);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private static _productQueryTest(query: string): boolean {
|
|
898
966
|
const q = query.toLowerCase();
|
|
899
967
|
const productTerms = /\b(product|products|item|items|sku|catalog|catalogue|price|prices|brand|model|stock|inventory|in stock|out of stock|buy|shop)\b/.test(q);
|
|
900
|
-
const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list)\b/.test(q);
|
|
968
|
+
const productAction = /\b(recommend|suggest|describe|description|detail|details|about|show|find|search|browse|list|explore|exploring)\b/.test(q);
|
|
901
969
|
const nonProductEntityTerms = /\b(organization|organisations?|organizations?|company|companies|employee|employees|staff|headcount|workforce)\b/.test(q);
|
|
902
970
|
return productTerms && productAction && !nonProductEntityTerms;
|
|
903
971
|
}
|
|
904
972
|
|
|
973
|
+
|
|
905
974
|
private static isProductData(item: VectorMatch): boolean {
|
|
906
975
|
const content = (item.content || '').toLowerCase();
|
|
907
976
|
const productKeywords = [
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DocumentChunker — splits long text into overlapping chunks
|
|
3
|
-
* suitable for vector embedding and retrieval.
|
|
4
|
-
*
|
|
5
|
-
* Strategy: sentence-aware sliding window
|
|
6
|
-
* 1. Split on sentence boundaries
|
|
7
|
-
* 2. Accumulate sentences into chunks up to `chunkSize` characters
|
|
8
|
-
* 3. Carry over `chunkOverlap` characters from the previous chunk
|
|
9
|
-
*/
|
|
10
|
-
interface Chunk {
|
|
11
|
-
id: string;
|
|
12
|
-
content: string;
|
|
13
|
-
metadata?: Record<string, unknown>;
|
|
14
|
-
}
|
|
15
|
-
interface ChunkOptions {
|
|
16
|
-
/** Target chunk size in characters (default 1000) */
|
|
17
|
-
chunkSize?: number;
|
|
18
|
-
/** Overlap between consecutive chunks in characters (default 200) */
|
|
19
|
-
chunkOverlap?: number;
|
|
20
|
-
/** Source document identifier used as ID prefix */
|
|
21
|
-
docId?: string | number;
|
|
22
|
-
/** Extra metadata to attach to every chunk */
|
|
23
|
-
metadata?: Record<string, unknown>;
|
|
24
|
-
/** Characters used to split text, in order of priority */
|
|
25
|
-
separators?: string[];
|
|
26
|
-
}
|
|
27
|
-
declare class DocumentChunker {
|
|
28
|
-
private readonly chunkSize;
|
|
29
|
-
private readonly chunkOverlap;
|
|
30
|
-
private readonly separators;
|
|
31
|
-
constructor(chunkSize?: number, chunkOverlap?: number, separators?: string[]);
|
|
32
|
-
/**
|
|
33
|
-
* Split a single text string into overlapping chunks using a recursive strategy.
|
|
34
|
-
* Preserves structural boundaries (Markdown headers) where possible.
|
|
35
|
-
*/
|
|
36
|
-
chunk(text: string, options?: ChunkOptions): Chunk[];
|
|
37
|
-
private recursiveSplit;
|
|
38
|
-
chunkMany(documents: Array<{
|
|
39
|
-
content: string;
|
|
40
|
-
docId?: string | number;
|
|
41
|
-
metadata?: Record<string, unknown>;
|
|
42
|
-
}>): Chunk[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { type Chunk as C, DocumentChunker as D, type ChunkOptions as a };
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DocumentChunker — splits long text into overlapping chunks
|
|
3
|
-
* suitable for vector embedding and retrieval.
|
|
4
|
-
*
|
|
5
|
-
* Strategy: sentence-aware sliding window
|
|
6
|
-
* 1. Split on sentence boundaries
|
|
7
|
-
* 2. Accumulate sentences into chunks up to `chunkSize` characters
|
|
8
|
-
* 3. Carry over `chunkOverlap` characters from the previous chunk
|
|
9
|
-
*/
|
|
10
|
-
interface Chunk {
|
|
11
|
-
id: string;
|
|
12
|
-
content: string;
|
|
13
|
-
metadata?: Record<string, unknown>;
|
|
14
|
-
}
|
|
15
|
-
interface ChunkOptions {
|
|
16
|
-
/** Target chunk size in characters (default 1000) */
|
|
17
|
-
chunkSize?: number;
|
|
18
|
-
/** Overlap between consecutive chunks in characters (default 200) */
|
|
19
|
-
chunkOverlap?: number;
|
|
20
|
-
/** Source document identifier used as ID prefix */
|
|
21
|
-
docId?: string | number;
|
|
22
|
-
/** Extra metadata to attach to every chunk */
|
|
23
|
-
metadata?: Record<string, unknown>;
|
|
24
|
-
/** Characters used to split text, in order of priority */
|
|
25
|
-
separators?: string[];
|
|
26
|
-
}
|
|
27
|
-
declare class DocumentChunker {
|
|
28
|
-
private readonly chunkSize;
|
|
29
|
-
private readonly chunkOverlap;
|
|
30
|
-
private readonly separators;
|
|
31
|
-
constructor(chunkSize?: number, chunkOverlap?: number, separators?: string[]);
|
|
32
|
-
/**
|
|
33
|
-
* Split a single text string into overlapping chunks using a recursive strategy.
|
|
34
|
-
* Preserves structural boundaries (Markdown headers) where possible.
|
|
35
|
-
*/
|
|
36
|
-
chunk(text: string, options?: ChunkOptions): Chunk[];
|
|
37
|
-
private recursiveSplit;
|
|
38
|
-
chunkMany(documents: Array<{
|
|
39
|
-
content: string;
|
|
40
|
-
docId?: string | number;
|
|
41
|
-
metadata?: Record<string, unknown>;
|
|
42
|
-
}>): Chunk[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { type Chunk as C, DocumentChunker as D, type ChunkOptions as a };
|