@retrivora-ai/rag-engine 1.9.3 → 1.9.7
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 +59 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +737 -237
- package/dist/handlers/index.mjs +736 -237
- package/dist/index-B9J_XEh0.d.ts +187 -0
- package/dist/index-BJ4cd-t5.d.mts +187 -0
- package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
- package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
- package/dist/index.css +237 -10
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +365 -164
- package/dist/index.mjs +350 -158
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +850 -239
- package/dist/server.mjs +839 -238
- package/package.json +2 -4
- 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 +168 -148
- package/src/app/layout.tsx +5 -18
- package/src/app/types.ts +17 -17
- package/src/components/ChatWidget.tsx +3 -1
- package/src/components/ChatWindow.tsx +5 -1
- package/src/components/DocViewer.tsx +71 -5
- package/src/components/Documentation.tsx +74 -11
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/components/constants.tsx +275 -0
- package/src/config/RagConfig.ts +47 -0
- package/src/config/constants.ts +1 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +73 -22
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +16 -7
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +91 -0
- package/src/core/VectorPlugin.ts +62 -8
- package/src/exceptions/index.ts +111 -0
- package/src/handlers/index.ts +73 -0
- package/src/hooks/useRagChat.ts +30 -5
- package/src/index.ts +27 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +8 -4
- 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 +30 -1
- package/src/types/chat.ts +9 -0
- package/src/types/props.ts +38 -1
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
package/src/core/Pipeline.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
|
|
|
2
2
|
import { BaseGraphProvider } from '../providers/graphdb/BaseGraphProvider';
|
|
3
3
|
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
4
4
|
import { ChatMessage } from '../types';
|
|
5
|
+
import { EmbeddingFailedException } from '../exceptions';
|
|
5
6
|
import { DocumentChunker, Chunk } from '../rag/DocumentChunker';
|
|
6
7
|
import { EntityExtractor } from '../rag/EntityExtractor';
|
|
7
8
|
import { Reranker } from '../rag/Reranker';
|
|
@@ -115,24 +116,15 @@ async function scoreHallucination(
|
|
|
115
116
|
? context.slice(0, maxContextChars) + '\n...[truncated]'
|
|
116
117
|
: context;
|
|
117
118
|
|
|
118
|
-
const prompt = `You are an AI quality checker. Given the CONTEXT retrieved from a knowledge base and the ANSWER generated from it, rate how well-grounded the answer is.
|
|
119
|
-
|
|
120
|
-
CONTEXT:
|
|
121
|
-
${truncatedContext}
|
|
122
|
-
|
|
123
|
-
ANSWER:
|
|
124
|
-
${answer}
|
|
125
|
-
|
|
126
|
-
Return ONLY a valid JSON object with no markdown fences:
|
|
127
|
-
{"score": <float 0-1>, "reason": "<one sentence>"}
|
|
128
|
-
|
|
129
|
-
Where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.`;
|
|
130
|
-
|
|
131
119
|
try {
|
|
132
120
|
const raw = await llm.chat(
|
|
133
|
-
[{ role: 'user', content:
|
|
121
|
+
[{ role: 'user', content: `CONTEXT:\n${truncatedContext}\n\nANSWER:\n${answer}` }],
|
|
134
122
|
'',
|
|
135
|
-
{
|
|
123
|
+
{
|
|
124
|
+
systemPrompt: 'You are an AI quality checker. Given the CONTEXT retrieved from a knowledge base and the ANSWER generated from it, rate how well-grounded the answer is. Return ONLY a valid JSON object with no markdown fences: {"score": <float 0-1>, "reason": "<one sentence>"} where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.',
|
|
125
|
+
temperature: 0,
|
|
126
|
+
maxTokens: 120
|
|
127
|
+
},
|
|
136
128
|
);
|
|
137
129
|
|
|
138
130
|
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
@@ -223,7 +215,13 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
223
215
|
- Do NOT try to format product lists as tables.
|
|
224
216
|
`;
|
|
225
217
|
|
|
226
|
-
|
|
218
|
+
// Merge chartInstruction with any user-supplied systemPrompt.
|
|
219
|
+
// We append rather than overwrite so plugin consumers who pass a custom
|
|
220
|
+
// systemPrompt do not have it silently replaced.
|
|
221
|
+
const userPromptPrefix = this.config.llm.systemPrompt
|
|
222
|
+
? `${this.config.llm.systemPrompt}\n\n`
|
|
223
|
+
: '';
|
|
224
|
+
this.config.llm.systemPrompt = `${userPromptPrefix}${chartInstruction}`;
|
|
227
225
|
|
|
228
226
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
229
227
|
|
|
@@ -330,7 +328,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
330
328
|
);
|
|
331
329
|
|
|
332
330
|
if (vectors.length !== chunks.length) {
|
|
333
|
-
throw new
|
|
331
|
+
throw new EmbeddingFailedException(
|
|
334
332
|
`[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. ` +
|
|
335
333
|
`Check embedding provider logs for individual chunk failures.`
|
|
336
334
|
);
|
|
@@ -431,7 +429,9 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
431
429
|
await this.initialize();
|
|
432
430
|
const ns = namespace ?? this.config.projectId;
|
|
433
431
|
const topK = this.config.rag?.topK ?? 5;
|
|
434
|
-
|
|
432
|
+
// Default score threshold of 0.3 filters random matches while keeping
|
|
433
|
+
// genuinely relevant chunks. Users can override via rag.scoreThreshold.
|
|
434
|
+
const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.3;
|
|
435
435
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
436
436
|
const requestStart = performance.now();
|
|
437
437
|
|
|
@@ -462,10 +462,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
462
462
|
const cacheKey = `${ns}::${searchQuery}`;
|
|
463
463
|
const cachedVector = this.embeddingCache.get(cacheKey);
|
|
464
464
|
|
|
465
|
+
const useGraph = this.graphDB && this.config.rag?.useGraphRetrieval;
|
|
465
466
|
const [strategyResult, embeddedVector] = await Promise.all([
|
|
466
467
|
QueryProcessor.determineRetrievalStrategy(
|
|
467
468
|
searchQuery,
|
|
468
|
-
this.llmRouter.get('fast'),
|
|
469
|
+
useGraph ? this.llmRouter.get('fast') : undefined,
|
|
469
470
|
this.config.rag?.graphKeywords,
|
|
470
471
|
this.config.rag?.vectorKeywords
|
|
471
472
|
),
|
|
@@ -546,48 +547,186 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
546
547
|
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
547
548
|
}
|
|
548
549
|
|
|
549
|
-
// 4.5 Schema Training — fire-and-forget, never
|
|
550
|
+
// 4.5 Schema Training — true fire-and-forget, never gates anything.
|
|
550
551
|
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
|
|
551
|
-
const
|
|
552
|
-
? SchemaMapper.
|
|
553
|
-
:
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
552
|
+
const cachedSchema = allMetadataKeys.length > 0
|
|
553
|
+
? SchemaMapper.getCached(ns, allMetadataKeys)
|
|
554
|
+
: undefined;
|
|
555
|
+
|
|
556
|
+
// Kick off schema training in the background — don't await it before starting UI transform.
|
|
557
|
+
// The next request will benefit from the trained schema via the cache.
|
|
558
|
+
if (allMetadataKeys.length > 0 && !cachedSchema) {
|
|
559
|
+
SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => { /* non-critical */ });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// 4.6 Start UI Transformation concurrently with generation.
|
|
563
|
+
// For product queries: use the fast heuristic immediately — no LLM call needed.
|
|
564
|
+
// For analytics queries: fire analyzeAndDecide() in parallel with text streaming.
|
|
565
|
+
const isProductQ = UITransformer.isProductQueryPublic(question);
|
|
566
|
+
const uiTransformationPromise = isProductQ
|
|
567
|
+
// Product carousel: pure heuristic, zero LLM latency
|
|
568
|
+
? Promise.resolve(
|
|
569
|
+
UITransformer.transform(question, sources, this.config, cachedSchema,
|
|
570
|
+
{ visualizationHint: 'product_browse', recommendedChart: 'text', filterInStockOnly: false, wantsExplicitTable: false, isTemporal: false, isComparison: false, language: 'en' }
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
: this.generateUiTransformation(
|
|
574
|
+
question,
|
|
575
|
+
sources,
|
|
576
|
+
cachedSchema,
|
|
577
|
+
hasNumericPredicates,
|
|
578
|
+
).catch(uiError => {
|
|
579
|
+
console.warn('[Pipeline] UI transformation failed concurrently:', uiError);
|
|
580
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
581
|
+
});
|
|
567
582
|
|
|
568
583
|
// 5. Generation (Streaming)
|
|
584
|
+
// Start hallucination scoring as a background promise BEFORE streaming
|
|
585
|
+
// so that the score runs concurrently with text delivery to the client.
|
|
586
|
+
// We await it together with uiTransformationPromise (see step 7) so the
|
|
587
|
+
// metadata frame is never yielded until the score is resolved — no
|
|
588
|
+
// post-yield mutation required.
|
|
589
|
+
let hallucinationScoringPromise: Promise<{ score: number; reason: string } | undefined> =
|
|
590
|
+
Promise.resolve(undefined);
|
|
591
|
+
|
|
569
592
|
const restrictionSuffix = '\n\n(IMPORTANT: Format your response beautifully using rich Markdown! Use bolding for emphasis, headings (##) for structure, bullet points for lists, and proper line breaks between paragraphs to make it highly readable. However, NEVER generate Markdown tables, HTML figures, or text charts (the UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization). If listing products, use simple markdown bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)';
|
|
593
|
+
|
|
594
|
+
const isClaude37 = this.config.llm.model.includes('claude-3-7-sonnet');
|
|
595
|
+
const isNativeThinking = isClaude37 && (this.config.llm.options?.thinking === true || this.config.llm.options?.thinking === 'enabled' || this.config.llm.options?.thinking !== false);
|
|
596
|
+
|
|
597
|
+
const modelNameLower = this.config.llm.model.toLowerCase();
|
|
598
|
+
const isReasoningModel = modelNameLower.includes('-r1') || modelNameLower.includes('deepseek-r1') || modelNameLower.includes('reasoning') || modelNameLower.includes('thinking');
|
|
599
|
+
const isSimulatedThinking = !isNativeThinking && (
|
|
600
|
+
this.config.llm.options?.thinking === true ||
|
|
601
|
+
this.config.llm.options?.thinking === 'enabled' ||
|
|
602
|
+
(isReasoningModel && this.config.llm.options?.thinking !== false)
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
let finalRestrictionSuffix = restrictionSuffix;
|
|
606
|
+
if (isSimulatedThinking) {
|
|
607
|
+
finalRestrictionSuffix += '\n\n(IMPORTANT: You must think step-by-step before answering. Write your thinking process inside a `<think>...</think>` block. Write the `<think>` block first, then follow it with your final response. Keep the thinking process thorough and detailed. Example: `<think>Thinking details here...</think>Final answer text here.`)';
|
|
608
|
+
}
|
|
609
|
+
|
|
570
610
|
const hardenedHistory = [...history];
|
|
571
|
-
const userQuestion = { role: 'user', content: question +
|
|
611
|
+
const userQuestion = { role: 'user', content: question + finalRestrictionSuffix } as ChatMessage;
|
|
572
612
|
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
573
613
|
|
|
574
614
|
const systemPrompt = this.config.llm.systemPrompt ?? '';
|
|
575
615
|
const userPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
576
616
|
|
|
577
617
|
let fullReply = '';
|
|
618
|
+
let thinkingText = '';
|
|
619
|
+
let thinkingStartMs = performance.now();
|
|
620
|
+
let thinkingDurationMs = 0;
|
|
578
621
|
const generateStart = performance.now();
|
|
579
622
|
|
|
580
623
|
if (this.llmProvider.chatStream) {
|
|
581
624
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
582
625
|
if (!stream) throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
626
|
+
|
|
627
|
+
let inThinking = false;
|
|
628
|
+
let textBuffer = '';
|
|
629
|
+
|
|
583
630
|
for await (const chunk of stream) {
|
|
584
|
-
|
|
585
|
-
|
|
631
|
+
// 1. Native Anthropic thinking
|
|
632
|
+
if (chunk.startsWith('\x00THINKING\x00')) {
|
|
633
|
+
const thinkingDelta = chunk.slice('\x00THINKING\x00'.length);
|
|
634
|
+
thinkingText += thinkingDelta;
|
|
635
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
636
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 2. Simulated thinking tag parsing
|
|
641
|
+
if (isSimulatedThinking) {
|
|
642
|
+
textBuffer += chunk;
|
|
643
|
+
while (textBuffer.length > 0) {
|
|
644
|
+
if (!inThinking) {
|
|
645
|
+
const thinkIndex = textBuffer.indexOf('<think>');
|
|
646
|
+
if (thinkIndex !== -1) {
|
|
647
|
+
const before = textBuffer.slice(0, thinkIndex);
|
|
648
|
+
if (before) {
|
|
649
|
+
fullReply += before;
|
|
650
|
+
yield before;
|
|
651
|
+
}
|
|
652
|
+
inThinking = true;
|
|
653
|
+
thinkingStartMs = performance.now();
|
|
654
|
+
textBuffer = textBuffer.slice(thinkIndex + '<think>'.length);
|
|
655
|
+
} else {
|
|
656
|
+
const maxSafeLength = textBuffer.length - '<think>'.length;
|
|
657
|
+
if (maxSafeLength > 0) {
|
|
658
|
+
const safeText = textBuffer.slice(0, maxSafeLength);
|
|
659
|
+
fullReply += safeText;
|
|
660
|
+
yield safeText;
|
|
661
|
+
textBuffer = textBuffer.slice(maxSafeLength);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
const endThinkIndex = textBuffer.indexOf('</think>');
|
|
667
|
+
if (endThinkIndex !== -1) {
|
|
668
|
+
const thinkingDelta = textBuffer.slice(0, endThinkIndex);
|
|
669
|
+
if (thinkingDelta) {
|
|
670
|
+
thinkingText += thinkingDelta;
|
|
671
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
672
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
673
|
+
}
|
|
674
|
+
inThinking = false;
|
|
675
|
+
textBuffer = textBuffer.slice(endThinkIndex + '</think>'.length);
|
|
676
|
+
} else {
|
|
677
|
+
const maxSafeLength = textBuffer.length - '</think>'.length;
|
|
678
|
+
if (maxSafeLength > 0) {
|
|
679
|
+
const thinkingDelta = textBuffer.slice(0, maxSafeLength);
|
|
680
|
+
thinkingText += thinkingDelta;
|
|
681
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
682
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
683
|
+
textBuffer = textBuffer.slice(maxSafeLength);
|
|
684
|
+
}
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
fullReply += chunk;
|
|
691
|
+
yield chunk;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Flush any remaining buffer
|
|
696
|
+
if (isSimulatedThinking && textBuffer.length > 0) {
|
|
697
|
+
if (inThinking) {
|
|
698
|
+
thinkingText += textBuffer;
|
|
699
|
+
yield { type: 'thinking', text: textBuffer } as any;
|
|
700
|
+
} else {
|
|
701
|
+
fullReply += textBuffer;
|
|
702
|
+
yield textBuffer;
|
|
703
|
+
}
|
|
586
704
|
}
|
|
587
705
|
} else {
|
|
588
706
|
const reply = await this.llmProvider.chat(messages, context);
|
|
589
|
-
|
|
590
|
-
|
|
707
|
+
if (isSimulatedThinking) {
|
|
708
|
+
const thinkStart = reply.indexOf('<think>');
|
|
709
|
+
const thinkEnd = reply.indexOf('</think>');
|
|
710
|
+
if (thinkStart !== -1 && thinkEnd !== -1 && thinkEnd > thinkStart) {
|
|
711
|
+
thinkingText = reply.slice(thinkStart + '<think>'.length, thinkEnd);
|
|
712
|
+
fullReply = reply.slice(0, thinkStart) + reply.slice(thinkEnd + '</think>'.length);
|
|
713
|
+
thinkingDurationMs = 100;
|
|
714
|
+
} else {
|
|
715
|
+
fullReply = reply;
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
fullReply = reply;
|
|
719
|
+
}
|
|
720
|
+
yield fullReply;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Kick off hallucination scoring now that we have the full reply.
|
|
724
|
+
// It runs in parallel with the remaining post-processing below.
|
|
725
|
+
const runHallucination = this.config.llm.options?.hallucinationScoring === true ||
|
|
726
|
+
(this.config.llm.provider !== 'ollama' && this.config.llm.options?.hallucinationScoring !== false);
|
|
727
|
+
if (runHallucination) {
|
|
728
|
+
hallucinationScoringPromise = scoreHallucination(this.llmProvider, fullReply, context)
|
|
729
|
+
.catch(() => undefined);
|
|
591
730
|
}
|
|
592
731
|
|
|
593
732
|
const generateMs = performance.now() - generateStart;
|
|
@@ -612,14 +751,20 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
612
751
|
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
|
|
613
752
|
};
|
|
614
753
|
|
|
615
|
-
// 7.
|
|
616
|
-
//
|
|
754
|
+
// 7. Await both UI transformation and hallucination score concurrently
|
|
755
|
+
// before yielding the metadata frame. This guarantees the trace is always
|
|
756
|
+
// complete when the consumer receives it — no post-yield mutation needed.
|
|
757
|
+
const [uiTransformation, hallucinationResult] = await Promise.all([
|
|
758
|
+
uiTransformationPromise,
|
|
759
|
+
hallucinationScoringPromise,
|
|
760
|
+
]);
|
|
761
|
+
|
|
617
762
|
const trace: ObservabilityTrace = {
|
|
618
763
|
requestId,
|
|
619
764
|
query: question,
|
|
620
765
|
rewrittenQuery,
|
|
621
766
|
systemPrompt,
|
|
622
|
-
userPrompt: question +
|
|
767
|
+
userPrompt: question + finalRestrictionSuffix,
|
|
623
768
|
chunks: sources.map((s) => ({
|
|
624
769
|
id: s.id,
|
|
625
770
|
score: s.score,
|
|
@@ -630,28 +775,22 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
630
775
|
latency,
|
|
631
776
|
tokens,
|
|
632
777
|
timestamp: new Date().toISOString(),
|
|
778
|
+
// Hallucination score is now always present when available
|
|
779
|
+
...(hallucinationResult ? {
|
|
780
|
+
hallucinationScore: hallucinationResult.score,
|
|
781
|
+
hallucinationReason: hallucinationResult.reason,
|
|
782
|
+
} : {}),
|
|
633
783
|
};
|
|
634
784
|
|
|
635
|
-
const uiTransformation = await uiTransformationPromise;
|
|
636
|
-
|
|
637
785
|
yield {
|
|
638
786
|
reply: '',
|
|
639
787
|
sources,
|
|
640
788
|
graphData,
|
|
641
789
|
ui_transformation: uiTransformation,
|
|
642
790
|
trace,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// The client receives this as a supplementary observability frame after the metadata.
|
|
647
|
-
scoreHallucination(this.llmProvider, fullReply, context)
|
|
648
|
-
.then((hallucinationResult) => {
|
|
649
|
-
if (hallucinationResult) {
|
|
650
|
-
trace.hallucinationScore = hallucinationResult.score;
|
|
651
|
-
trace.hallucinationReason = hallucinationResult.reason;
|
|
652
|
-
}
|
|
653
|
-
})
|
|
654
|
-
.catch(() => { /* non-critical */ });
|
|
791
|
+
thinking: thinkingText || undefined,
|
|
792
|
+
thinkingMs: thinkingDurationMs > 0 ? Math.round(thinkingDurationMs) : undefined,
|
|
793
|
+
} as ChatResponse & { trace: ObservabilityTrace; thinking?: string; thinkingMs?: number };
|
|
655
794
|
|
|
656
795
|
} catch (error) {
|
|
657
796
|
throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -665,22 +804,31 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
665
804
|
private async generateUiTransformation(
|
|
666
805
|
question: string,
|
|
667
806
|
sources: VectorMatch[],
|
|
668
|
-
|
|
807
|
+
cachedSchema?: SchemaMap,
|
|
669
808
|
forceDeterministic: boolean = false,
|
|
670
809
|
): Promise<unknown> {
|
|
671
810
|
if (!sources || sources.length === 0) {
|
|
672
|
-
return UITransformer.transform(question, sources, this.config,
|
|
811
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Fast heuristic for product queries — skip both LLM calls entirely.
|
|
815
|
+
if (UITransformer.isProductQueryPublic(question)) {
|
|
816
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema,
|
|
817
|
+
{ visualizationHint: 'product_browse', recommendedChart: 'text', filterInStockOnly: false, wantsExplicitTable: false, isTemporal: false, isComparison: false, language: 'en' }
|
|
818
|
+
);
|
|
673
819
|
}
|
|
674
820
|
|
|
675
|
-
|
|
676
|
-
|
|
821
|
+
const isLocalProvider = this.config.llm.provider === 'ollama';
|
|
822
|
+
const disableLlmUiTransform = this.config.llm.options?.disableLlmUiTransform === true;
|
|
823
|
+
if (forceDeterministic || isLocalProvider || disableLlmUiTransform) {
|
|
824
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
677
825
|
}
|
|
678
826
|
|
|
679
827
|
try {
|
|
680
828
|
return await UITransformer.analyzeAndDecide(question, sources, this.llmRouter.get('fast'));
|
|
681
829
|
} catch (err) {
|
|
682
830
|
console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
|
|
683
|
-
return UITransformer.transform(question, sources, this.config,
|
|
831
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
684
832
|
}
|
|
685
833
|
}
|
|
686
834
|
|
|
@@ -803,7 +951,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
803
951
|
let queryVector = this.embeddingCache.get(cacheKey);
|
|
804
952
|
|
|
805
953
|
// Determine strategy
|
|
806
|
-
const
|
|
954
|
+
const useGraph = this.graphDB && this.config.rag?.useGraphRetrieval;
|
|
955
|
+
const strategy = await QueryProcessor.determineRetrievalStrategy(
|
|
956
|
+
query,
|
|
957
|
+
useGraph ? this.llmRouter.get('fast') : undefined
|
|
958
|
+
);
|
|
807
959
|
console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
|
|
808
960
|
|
|
809
961
|
const [retrievedVector, graphData] = await Promise.all([
|
|
@@ -868,10 +1020,13 @@ Optimized Search Query:`;
|
|
|
868
1020
|
|
|
869
1021
|
const rewrite = await this.llmProvider.chat(
|
|
870
1022
|
[
|
|
871
|
-
{ role: 'system', content: 'You are an assistant that optimizes search queries for RAG systems.' },
|
|
872
1023
|
{ role: 'user', content: prompt }
|
|
873
1024
|
],
|
|
874
|
-
''
|
|
1025
|
+
'',
|
|
1026
|
+
{
|
|
1027
|
+
systemPrompt: 'You are an assistant that optimizes search queries for RAG systems. Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.',
|
|
1028
|
+
temperature: 0
|
|
1029
|
+
}
|
|
875
1030
|
);
|
|
876
1031
|
|
|
877
1032
|
return rewrite.trim() || question;
|
|
@@ -901,10 +1056,13 @@ Suggestions:`;
|
|
|
901
1056
|
|
|
902
1057
|
const response = await this.llmProvider.chat(
|
|
903
1058
|
[
|
|
904
|
-
{ role: 'system', content: 'You are a helpful assistant that generates search suggestions.' },
|
|
905
1059
|
{ role: 'user', content: prompt }
|
|
906
1060
|
],
|
|
907
|
-
''
|
|
1061
|
+
'',
|
|
1062
|
+
{
|
|
1063
|
+
systemPrompt: 'You are a helpful assistant that generates search suggestions based on the provided snippets. Focus on questions that can be answered by the context, keep each under 10 words, and return ONLY a JSON array of strings.',
|
|
1064
|
+
temperature: 0
|
|
1065
|
+
}
|
|
908
1066
|
);
|
|
909
1067
|
|
|
910
1068
|
const match = response.match(/\[[\s\S]*\]/);
|
|
@@ -4,12 +4,13 @@ import { VectorDBConfig, GraphDBConfig, LLMConfig, EmbeddingConfig } from '../co
|
|
|
4
4
|
import { LLMFactory } from '../llm/LLMFactory';
|
|
5
5
|
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
6
6
|
import { IProviderValidator, IProviderHealthChecker } from './ProviderInterfaces';
|
|
7
|
+
import { ProviderNotFoundException } from '../exceptions';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* ProviderRegistry — dynamic provider loader for Vector DBs and LLMs.
|
|
10
11
|
*/
|
|
11
12
|
type VectorProviderClass = {
|
|
12
|
-
new
|
|
13
|
+
new(config: VectorDBConfig): BaseVectorProvider;
|
|
13
14
|
getValidator?: () => IProviderValidator;
|
|
14
15
|
getHealthChecker?: () => IProviderHealthChecker;
|
|
15
16
|
};
|
|
@@ -19,10 +20,10 @@ type GraphProviderClass = new (config: GraphDBConfig) => BaseGraphProvider;
|
|
|
19
20
|
export class ProviderRegistry {
|
|
20
21
|
private static vectorProviders: Record<string, VectorProviderClass> = {};
|
|
21
22
|
private static graphProviders: Record<string, GraphProviderClass> = {};
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
private static vectorValidators: Record<string, IProviderValidator> = {};
|
|
24
25
|
private static vectorHealthCheckers: Record<string, IProviderHealthChecker> = {};
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
private static llmValidators: Record<string, IProviderValidator> = {};
|
|
27
28
|
private static llmHealthCheckers: Record<string, IProviderHealthChecker> = {};
|
|
28
29
|
|
|
@@ -38,7 +39,7 @@ export class ProviderRegistry {
|
|
|
38
39
|
|
|
39
40
|
static async getVectorValidator(provider: string): Promise<IProviderValidator | null> {
|
|
40
41
|
if (this.vectorValidators[provider]) return this.vectorValidators[provider];
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
// Try to load built-in validator
|
|
43
44
|
try {
|
|
44
45
|
const providerClass = await this.loadVectorProviderClass(provider);
|
|
@@ -54,7 +55,7 @@ export class ProviderRegistry {
|
|
|
54
55
|
|
|
55
56
|
static async getVectorHealthChecker(provider: string): Promise<IProviderHealthChecker | null> {
|
|
56
57
|
if (this.vectorHealthCheckers[provider]) return this.vectorHealthCheckers[provider];
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
try {
|
|
59
60
|
const providerClass = await this.loadVectorProviderClass(provider);
|
|
60
61
|
if (providerClass.getHealthChecker) {
|
|
@@ -116,7 +117,7 @@ export class ProviderRegistry {
|
|
|
116
117
|
return UniversalVectorProvider;
|
|
117
118
|
}
|
|
118
119
|
default:
|
|
119
|
-
throw new
|
|
120
|
+
throw new ProviderNotFoundException('vector', provider);
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -137,11 +138,19 @@ export class ProviderRegistry {
|
|
|
137
138
|
return new SimpleGraphProvider(config);
|
|
138
139
|
}
|
|
139
140
|
default:
|
|
140
|
-
throw new
|
|
141
|
+
throw new ProviderNotFoundException('graph', provider);
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
static createLLMProvider(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig): ILLMProvider {
|
|
145
146
|
return LLMFactory.create(llmConfig, embeddingConfig);
|
|
146
147
|
}
|
|
148
|
+
|
|
149
|
+
static registerLLMProvider(name: string, factory: (config: LLMConfig) => ILLMProvider): void {
|
|
150
|
+
LLMFactory.register(name, factory);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static createEmbeddingProvider(embeddingConfig: EmbeddingConfig): ILLMProvider {
|
|
154
|
+
return LLMFactory.createEmbeddingProvider(embeddingConfig);
|
|
155
|
+
}
|
|
147
156
|
}
|
|
@@ -72,6 +72,30 @@ export class QueryProcessor {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
const activeValidFields = validFields.length > 0
|
|
76
|
+
? validFields
|
|
77
|
+
: ['brand', 'category', 'price', 'rating', 'stock', 'stock_quantity', 'status', 'name', 'title'];
|
|
78
|
+
|
|
79
|
+
const resolveValidField = (fieldStr: string): string | null => {
|
|
80
|
+
const cleaned = this.normalizeHintValue(fieldStr)
|
|
81
|
+
.replace(/\b(?:provide|show|get|give|list|all|the|a|an|of|organizations?|companies?|records?|items?|whose|with|having|where|that|which|have|has|is|are|was|were)\b/gi, ' ')
|
|
82
|
+
.replace(/\s+/g, ' ')
|
|
83
|
+
.trim();
|
|
84
|
+
|
|
85
|
+
const comparable = (v: string) => v.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
86
|
+
const cleanedComparable = comparable(cleaned);
|
|
87
|
+
if (!cleanedComparable) return null;
|
|
88
|
+
|
|
89
|
+
const matchedField = activeValidFields.find(fieldName => {
|
|
90
|
+
const candidate = comparable(fieldName);
|
|
91
|
+
return candidate === cleanedComparable ||
|
|
92
|
+
candidate.includes(cleanedComparable) ||
|
|
93
|
+
cleanedComparable.includes(candidate);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return matchedField ?? null;
|
|
97
|
+
};
|
|
98
|
+
|
|
75
99
|
// 1. Quoted phrases
|
|
76
100
|
for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
|
|
77
101
|
addHint(match[1]);
|
|
@@ -105,7 +129,13 @@ export class QueryProcessor {
|
|
|
105
129
|
const field = match[1];
|
|
106
130
|
const value = match[2];
|
|
107
131
|
if (field && !this.isLikelyPromptPhrase(field)) {
|
|
108
|
-
|
|
132
|
+
const resolvedField = resolveValidField(field);
|
|
133
|
+
if (resolvedField) {
|
|
134
|
+
addHint(value, resolvedField);
|
|
135
|
+
} else {
|
|
136
|
+
addHint(value);
|
|
137
|
+
addHint(field);
|
|
138
|
+
}
|
|
109
139
|
}
|
|
110
140
|
}
|
|
111
141
|
|
|
@@ -161,13 +191,17 @@ export class QueryProcessor {
|
|
|
161
191
|
|
|
162
192
|
for (const pattern of fieldValuePatterns) {
|
|
163
193
|
for (const match of question.matchAll(pattern)) {
|
|
164
|
-
const field =
|
|
194
|
+
const field = match[1] ?? '';
|
|
165
195
|
const value = match[2] ?? '';
|
|
166
196
|
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
const resolvedField = resolveValidField(field);
|
|
198
|
+
if (resolvedField) {
|
|
199
|
+
addHint(value, resolvedField);
|
|
169
200
|
} else {
|
|
170
201
|
addHint(value);
|
|
202
|
+
if (field && !this.isLikelyPromptPhrase(field)) {
|
|
203
|
+
addHint(field);
|
|
204
|
+
}
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
207
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RagConfig, RetrievalConfig, UniversalRagConfig } from '../config/RagConfig';
|
|
2
|
+
import { ChatMessage, ChatResponse, IngestDocument } from '../types';
|
|
3
|
+
import { ConfigResolver } from './ConfigResolver';
|
|
4
|
+
import { ConfigValidator } from './ConfigValidator';
|
|
5
|
+
import { Pipeline } from './Pipeline';
|
|
6
|
+
import { wrapError, RetrivoraErrorCode } from '../exceptions';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public SDK facade matching the prompt-level Retrivora API.
|
|
10
|
+
*
|
|
11
|
+
* It keeps provider details behind configuration and delegates execution to the
|
|
12
|
+
* existing Pipeline implementation.
|
|
13
|
+
*/
|
|
14
|
+
export class Retrivora {
|
|
15
|
+
private readonly pipeline: Pipeline;
|
|
16
|
+
readonly config: RagConfig;
|
|
17
|
+
|
|
18
|
+
constructor(config?: UniversalRagConfig) {
|
|
19
|
+
this.config = ConfigResolver.resolveUniversal(config);
|
|
20
|
+
this.pipeline = new Pipeline(this.config);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async initialize(): Promise<void> {
|
|
24
|
+
try {
|
|
25
|
+
await ConfigValidator.validateAndThrow(this.config);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw wrapError(err, 'CONFIGURATION_ERROR');
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await this.pipeline.initialize();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
throw wrapError(err, 'AUTHENTICATION_ERROR');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async ingest(
|
|
37
|
+
documents: IngestDocument[],
|
|
38
|
+
namespace?: string,
|
|
39
|
+
): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
|
|
40
|
+
try {
|
|
41
|
+
return await this.pipeline.ingest(documents, namespace);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const msg = String(err);
|
|
44
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
45
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
46
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
47
|
+
}
|
|
48
|
+
throw wrapError(err, defaultCode);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
|
|
53
|
+
try {
|
|
54
|
+
return await this.pipeline.ask(question, history, namespace);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const msg = String(err);
|
|
57
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
58
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
59
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
60
|
+
}
|
|
61
|
+
throw wrapError(err, defaultCode);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async *askStream(
|
|
66
|
+
question: string,
|
|
67
|
+
history: ChatMessage[] = [],
|
|
68
|
+
namespace?: string,
|
|
69
|
+
): AsyncIterable<string | ChatResponse> {
|
|
70
|
+
try {
|
|
71
|
+
const stream = this.pipeline.askStream(question, history, namespace);
|
|
72
|
+
for await (const chunk of stream) {
|
|
73
|
+
yield chunk;
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const msg = String(err);
|
|
77
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
78
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
79
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
80
|
+
}
|
|
81
|
+
throw wrapError(err, defaultCode);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getPipeline(): Pipeline {
|
|
86
|
+
return this.pipeline;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type { RetrievalConfig, UniversalRagConfig };
|
|
91
|
+
|