@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
package/src/core/Pipeline.ts
CHANGED
|
@@ -115,24 +115,15 @@ async function scoreHallucination(
|
|
|
115
115
|
? context.slice(0, maxContextChars) + '\n...[truncated]'
|
|
116
116
|
: context;
|
|
117
117
|
|
|
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
118
|
try {
|
|
132
119
|
const raw = await llm.chat(
|
|
133
|
-
[{ role: 'user', content:
|
|
120
|
+
[{ role: 'user', content: `CONTEXT:\n${truncatedContext}\n\nANSWER:\n${answer}` }],
|
|
134
121
|
'',
|
|
135
|
-
{
|
|
122
|
+
{
|
|
123
|
+
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.',
|
|
124
|
+
temperature: 0,
|
|
125
|
+
maxTokens: 120
|
|
126
|
+
},
|
|
136
127
|
);
|
|
137
128
|
|
|
138
129
|
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
@@ -223,7 +214,13 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
223
214
|
- Do NOT try to format product lists as tables.
|
|
224
215
|
`;
|
|
225
216
|
|
|
226
|
-
|
|
217
|
+
// Merge chartInstruction with any user-supplied systemPrompt.
|
|
218
|
+
// We append rather than overwrite so plugin consumers who pass a custom
|
|
219
|
+
// systemPrompt do not have it silently replaced.
|
|
220
|
+
const userPromptPrefix = this.config.llm.systemPrompt
|
|
221
|
+
? `${this.config.llm.systemPrompt}\n\n`
|
|
222
|
+
: '';
|
|
223
|
+
this.config.llm.systemPrompt = `${userPromptPrefix}${chartInstruction}`;
|
|
227
224
|
|
|
228
225
|
this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
|
|
229
226
|
|
|
@@ -431,7 +428,9 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
431
428
|
await this.initialize();
|
|
432
429
|
const ns = namespace ?? this.config.projectId;
|
|
433
430
|
const topK = this.config.rag?.topK ?? 5;
|
|
434
|
-
|
|
431
|
+
// Default score threshold of 0.3 filters random matches while keeping
|
|
432
|
+
// genuinely relevant chunks. Users can override via rag.scoreThreshold.
|
|
433
|
+
const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.3;
|
|
435
434
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
436
435
|
const requestStart = performance.now();
|
|
437
436
|
|
|
@@ -462,10 +461,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
462
461
|
const cacheKey = `${ns}::${searchQuery}`;
|
|
463
462
|
const cachedVector = this.embeddingCache.get(cacheKey);
|
|
464
463
|
|
|
464
|
+
const useGraph = this.graphDB && this.config.rag?.useGraphRetrieval;
|
|
465
465
|
const [strategyResult, embeddedVector] = await Promise.all([
|
|
466
466
|
QueryProcessor.determineRetrievalStrategy(
|
|
467
467
|
searchQuery,
|
|
468
|
-
this.llmRouter.get('fast'),
|
|
468
|
+
useGraph ? this.llmRouter.get('fast') : undefined,
|
|
469
469
|
this.config.rag?.graphKeywords,
|
|
470
470
|
this.config.rag?.vectorKeywords
|
|
471
471
|
),
|
|
@@ -546,48 +546,186 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
546
546
|
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
// 4.5 Schema Training — fire-and-forget, never
|
|
549
|
+
// 4.5 Schema Training — true fire-and-forget, never gates anything.
|
|
550
550
|
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
|
-
|
|
551
|
+
const cachedSchema = allMetadataKeys.length > 0
|
|
552
|
+
? SchemaMapper.getCached(ns, allMetadataKeys)
|
|
553
|
+
: undefined;
|
|
554
|
+
|
|
555
|
+
// Kick off schema training in the background — don't await it before starting UI transform.
|
|
556
|
+
// The next request will benefit from the trained schema via the cache.
|
|
557
|
+
if (allMetadataKeys.length > 0 && !cachedSchema) {
|
|
558
|
+
SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => { /* non-critical */ });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 4.6 Start UI Transformation concurrently with generation.
|
|
562
|
+
// For product queries: use the fast heuristic immediately — no LLM call needed.
|
|
563
|
+
// For analytics queries: fire analyzeAndDecide() in parallel with text streaming.
|
|
564
|
+
const isProductQ = UITransformer.isProductQueryPublic(question);
|
|
565
|
+
const uiTransformationPromise = isProductQ
|
|
566
|
+
// Product carousel: pure heuristic, zero LLM latency
|
|
567
|
+
? Promise.resolve(
|
|
568
|
+
UITransformer.transform(question, sources, this.config, cachedSchema,
|
|
569
|
+
{ visualizationHint: 'product_browse', recommendedChart: 'text', filterInStockOnly: false, wantsExplicitTable: false, isTemporal: false, isComparison: false, language: 'en' }
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
: this.generateUiTransformation(
|
|
573
|
+
question,
|
|
574
|
+
sources,
|
|
575
|
+
cachedSchema,
|
|
576
|
+
hasNumericPredicates,
|
|
577
|
+
).catch(uiError => {
|
|
578
|
+
console.warn('[Pipeline] UI transformation failed concurrently:', uiError);
|
|
579
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
580
|
+
});
|
|
567
581
|
|
|
568
582
|
// 5. Generation (Streaming)
|
|
583
|
+
// Start hallucination scoring as a background promise BEFORE streaming
|
|
584
|
+
// so that the score runs concurrently with text delivery to the client.
|
|
585
|
+
// We await it together with uiTransformationPromise (see step 7) so the
|
|
586
|
+
// metadata frame is never yielded until the score is resolved — no
|
|
587
|
+
// post-yield mutation required.
|
|
588
|
+
let hallucinationScoringPromise: Promise<{ score: number; reason: string } | undefined> =
|
|
589
|
+
Promise.resolve(undefined);
|
|
590
|
+
|
|
569
591
|
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.)';
|
|
592
|
+
|
|
593
|
+
const isClaude37 = this.config.llm.model.includes('claude-3-7-sonnet');
|
|
594
|
+
const isNativeThinking = isClaude37 && (this.config.llm.options?.thinking === true || this.config.llm.options?.thinking === 'enabled' || this.config.llm.options?.thinking !== false);
|
|
595
|
+
|
|
596
|
+
const modelNameLower = this.config.llm.model.toLowerCase();
|
|
597
|
+
const isReasoningModel = modelNameLower.includes('-r1') || modelNameLower.includes('deepseek-r1') || modelNameLower.includes('reasoning') || modelNameLower.includes('thinking');
|
|
598
|
+
const isSimulatedThinking = !isNativeThinking && (
|
|
599
|
+
this.config.llm.options?.thinking === true ||
|
|
600
|
+
this.config.llm.options?.thinking === 'enabled' ||
|
|
601
|
+
(isReasoningModel && this.config.llm.options?.thinking !== false)
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
let finalRestrictionSuffix = restrictionSuffix;
|
|
605
|
+
if (isSimulatedThinking) {
|
|
606
|
+
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.`)';
|
|
607
|
+
}
|
|
608
|
+
|
|
570
609
|
const hardenedHistory = [...history];
|
|
571
|
-
const userQuestion = { role: 'user', content: question +
|
|
610
|
+
const userQuestion = { role: 'user', content: question + finalRestrictionSuffix } as ChatMessage;
|
|
572
611
|
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
573
612
|
|
|
574
613
|
const systemPrompt = this.config.llm.systemPrompt ?? '';
|
|
575
614
|
const userPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
576
615
|
|
|
577
616
|
let fullReply = '';
|
|
617
|
+
let thinkingText = '';
|
|
618
|
+
let thinkingStartMs = performance.now();
|
|
619
|
+
let thinkingDurationMs = 0;
|
|
578
620
|
const generateStart = performance.now();
|
|
579
621
|
|
|
580
622
|
if (this.llmProvider.chatStream) {
|
|
581
623
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
582
624
|
if (!stream) throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
625
|
+
|
|
626
|
+
let inThinking = false;
|
|
627
|
+
let textBuffer = '';
|
|
628
|
+
|
|
583
629
|
for await (const chunk of stream) {
|
|
584
|
-
|
|
585
|
-
|
|
630
|
+
// 1. Native Anthropic thinking
|
|
631
|
+
if (chunk.startsWith('\x00THINKING\x00')) {
|
|
632
|
+
const thinkingDelta = chunk.slice('\x00THINKING\x00'.length);
|
|
633
|
+
thinkingText += thinkingDelta;
|
|
634
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
635
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// 2. Simulated thinking tag parsing
|
|
640
|
+
if (isSimulatedThinking) {
|
|
641
|
+
textBuffer += chunk;
|
|
642
|
+
while (textBuffer.length > 0) {
|
|
643
|
+
if (!inThinking) {
|
|
644
|
+
const thinkIndex = textBuffer.indexOf('<think>');
|
|
645
|
+
if (thinkIndex !== -1) {
|
|
646
|
+
const before = textBuffer.slice(0, thinkIndex);
|
|
647
|
+
if (before) {
|
|
648
|
+
fullReply += before;
|
|
649
|
+
yield before;
|
|
650
|
+
}
|
|
651
|
+
inThinking = true;
|
|
652
|
+
thinkingStartMs = performance.now();
|
|
653
|
+
textBuffer = textBuffer.slice(thinkIndex + '<think>'.length);
|
|
654
|
+
} else {
|
|
655
|
+
const maxSafeLength = textBuffer.length - '<think>'.length;
|
|
656
|
+
if (maxSafeLength > 0) {
|
|
657
|
+
const safeText = textBuffer.slice(0, maxSafeLength);
|
|
658
|
+
fullReply += safeText;
|
|
659
|
+
yield safeText;
|
|
660
|
+
textBuffer = textBuffer.slice(maxSafeLength);
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
const endThinkIndex = textBuffer.indexOf('</think>');
|
|
666
|
+
if (endThinkIndex !== -1) {
|
|
667
|
+
const thinkingDelta = textBuffer.slice(0, endThinkIndex);
|
|
668
|
+
if (thinkingDelta) {
|
|
669
|
+
thinkingText += thinkingDelta;
|
|
670
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
671
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
672
|
+
}
|
|
673
|
+
inThinking = false;
|
|
674
|
+
textBuffer = textBuffer.slice(endThinkIndex + '</think>'.length);
|
|
675
|
+
} else {
|
|
676
|
+
const maxSafeLength = textBuffer.length - '</think>'.length;
|
|
677
|
+
if (maxSafeLength > 0) {
|
|
678
|
+
const thinkingDelta = textBuffer.slice(0, maxSafeLength);
|
|
679
|
+
thinkingText += thinkingDelta;
|
|
680
|
+
thinkingDurationMs = performance.now() - thinkingStartMs;
|
|
681
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
682
|
+
textBuffer = textBuffer.slice(maxSafeLength);
|
|
683
|
+
}
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
fullReply += chunk;
|
|
690
|
+
yield chunk;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Flush any remaining buffer
|
|
695
|
+
if (isSimulatedThinking && textBuffer.length > 0) {
|
|
696
|
+
if (inThinking) {
|
|
697
|
+
thinkingText += textBuffer;
|
|
698
|
+
yield { type: 'thinking', text: textBuffer } as any;
|
|
699
|
+
} else {
|
|
700
|
+
fullReply += textBuffer;
|
|
701
|
+
yield textBuffer;
|
|
702
|
+
}
|
|
586
703
|
}
|
|
587
704
|
} else {
|
|
588
705
|
const reply = await this.llmProvider.chat(messages, context);
|
|
589
|
-
|
|
590
|
-
|
|
706
|
+
if (isSimulatedThinking) {
|
|
707
|
+
const thinkStart = reply.indexOf('<think>');
|
|
708
|
+
const thinkEnd = reply.indexOf('</think>');
|
|
709
|
+
if (thinkStart !== -1 && thinkEnd !== -1 && thinkEnd > thinkStart) {
|
|
710
|
+
thinkingText = reply.slice(thinkStart + '<think>'.length, thinkEnd);
|
|
711
|
+
fullReply = reply.slice(0, thinkStart) + reply.slice(thinkEnd + '</think>'.length);
|
|
712
|
+
thinkingDurationMs = 100;
|
|
713
|
+
} else {
|
|
714
|
+
fullReply = reply;
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
fullReply = reply;
|
|
718
|
+
}
|
|
719
|
+
yield fullReply;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Kick off hallucination scoring now that we have the full reply.
|
|
723
|
+
// It runs in parallel with the remaining post-processing below.
|
|
724
|
+
const runHallucination = this.config.llm.options?.hallucinationScoring === true ||
|
|
725
|
+
(this.config.llm.provider !== 'ollama' && this.config.llm.options?.hallucinationScoring !== false);
|
|
726
|
+
if (runHallucination) {
|
|
727
|
+
hallucinationScoringPromise = scoreHallucination(this.llmProvider, fullReply, context)
|
|
728
|
+
.catch(() => undefined);
|
|
591
729
|
}
|
|
592
730
|
|
|
593
731
|
const generateMs = performance.now() - generateStart;
|
|
@@ -612,14 +750,20 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
612
750
|
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
|
|
613
751
|
};
|
|
614
752
|
|
|
615
|
-
// 7.
|
|
616
|
-
//
|
|
753
|
+
// 7. Await both UI transformation and hallucination score concurrently
|
|
754
|
+
// before yielding the metadata frame. This guarantees the trace is always
|
|
755
|
+
// complete when the consumer receives it — no post-yield mutation needed.
|
|
756
|
+
const [uiTransformation, hallucinationResult] = await Promise.all([
|
|
757
|
+
uiTransformationPromise,
|
|
758
|
+
hallucinationScoringPromise,
|
|
759
|
+
]);
|
|
760
|
+
|
|
617
761
|
const trace: ObservabilityTrace = {
|
|
618
762
|
requestId,
|
|
619
763
|
query: question,
|
|
620
764
|
rewrittenQuery,
|
|
621
765
|
systemPrompt,
|
|
622
|
-
userPrompt: question +
|
|
766
|
+
userPrompt: question + finalRestrictionSuffix,
|
|
623
767
|
chunks: sources.map((s) => ({
|
|
624
768
|
id: s.id,
|
|
625
769
|
score: s.score,
|
|
@@ -630,28 +774,22 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
630
774
|
latency,
|
|
631
775
|
tokens,
|
|
632
776
|
timestamp: new Date().toISOString(),
|
|
777
|
+
// Hallucination score is now always present when available
|
|
778
|
+
...(hallucinationResult ? {
|
|
779
|
+
hallucinationScore: hallucinationResult.score,
|
|
780
|
+
hallucinationReason: hallucinationResult.reason,
|
|
781
|
+
} : {}),
|
|
633
782
|
};
|
|
634
783
|
|
|
635
|
-
const uiTransformation = await uiTransformationPromise;
|
|
636
|
-
|
|
637
784
|
yield {
|
|
638
785
|
reply: '',
|
|
639
786
|
sources,
|
|
640
787
|
graphData,
|
|
641
788
|
ui_transformation: uiTransformation,
|
|
642
789
|
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 */ });
|
|
790
|
+
thinking: thinkingText || undefined,
|
|
791
|
+
thinkingMs: thinkingDurationMs > 0 ? Math.round(thinkingDurationMs) : undefined,
|
|
792
|
+
} as ChatResponse & { trace: ObservabilityTrace; thinking?: string; thinkingMs?: number };
|
|
655
793
|
|
|
656
794
|
} catch (error) {
|
|
657
795
|
throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -665,22 +803,31 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
665
803
|
private async generateUiTransformation(
|
|
666
804
|
question: string,
|
|
667
805
|
sources: VectorMatch[],
|
|
668
|
-
|
|
806
|
+
cachedSchema?: SchemaMap,
|
|
669
807
|
forceDeterministic: boolean = false,
|
|
670
808
|
): Promise<unknown> {
|
|
671
809
|
if (!sources || sources.length === 0) {
|
|
672
|
-
return UITransformer.transform(question, sources, this.config,
|
|
810
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Fast heuristic for product queries — skip both LLM calls entirely.
|
|
814
|
+
if (UITransformer.isProductQueryPublic(question)) {
|
|
815
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema,
|
|
816
|
+
{ visualizationHint: 'product_browse', recommendedChart: 'text', filterInStockOnly: false, wantsExplicitTable: false, isTemporal: false, isComparison: false, language: 'en' }
|
|
817
|
+
);
|
|
673
818
|
}
|
|
674
819
|
|
|
675
|
-
|
|
676
|
-
|
|
820
|
+
const isLocalProvider = this.config.llm.provider === 'ollama';
|
|
821
|
+
const disableLlmUiTransform = this.config.llm.options?.disableLlmUiTransform === true;
|
|
822
|
+
if (forceDeterministic || isLocalProvider || disableLlmUiTransform) {
|
|
823
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
677
824
|
}
|
|
678
825
|
|
|
679
826
|
try {
|
|
680
827
|
return await UITransformer.analyzeAndDecide(question, sources, this.llmRouter.get('fast'));
|
|
681
828
|
} catch (err) {
|
|
682
829
|
console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
|
|
683
|
-
return UITransformer.transform(question, sources, this.config,
|
|
830
|
+
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
684
831
|
}
|
|
685
832
|
}
|
|
686
833
|
|
|
@@ -803,7 +950,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
803
950
|
let queryVector = this.embeddingCache.get(cacheKey);
|
|
804
951
|
|
|
805
952
|
// Determine strategy
|
|
806
|
-
const
|
|
953
|
+
const useGraph = this.graphDB && this.config.rag?.useGraphRetrieval;
|
|
954
|
+
const strategy = await QueryProcessor.determineRetrievalStrategy(
|
|
955
|
+
query,
|
|
956
|
+
useGraph ? this.llmRouter.get('fast') : undefined
|
|
957
|
+
);
|
|
807
958
|
console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
|
|
808
959
|
|
|
809
960
|
const [retrievedVector, graphData] = await Promise.all([
|
|
@@ -868,10 +1019,13 @@ Optimized Search Query:`;
|
|
|
868
1019
|
|
|
869
1020
|
const rewrite = await this.llmProvider.chat(
|
|
870
1021
|
[
|
|
871
|
-
{ role: 'system', content: 'You are an assistant that optimizes search queries for RAG systems.' },
|
|
872
1022
|
{ role: 'user', content: prompt }
|
|
873
1023
|
],
|
|
874
|
-
''
|
|
1024
|
+
'',
|
|
1025
|
+
{
|
|
1026
|
+
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.',
|
|
1027
|
+
temperature: 0
|
|
1028
|
+
}
|
|
875
1029
|
);
|
|
876
1030
|
|
|
877
1031
|
return rewrite.trim() || question;
|
|
@@ -901,10 +1055,13 @@ Suggestions:`;
|
|
|
901
1055
|
|
|
902
1056
|
const response = await this.llmProvider.chat(
|
|
903
1057
|
[
|
|
904
|
-
{ role: 'system', content: 'You are a helpful assistant that generates search suggestions.' },
|
|
905
1058
|
{ role: 'user', content: prompt }
|
|
906
1059
|
],
|
|
907
|
-
''
|
|
1060
|
+
'',
|
|
1061
|
+
{
|
|
1062
|
+
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.',
|
|
1063
|
+
temperature: 0
|
|
1064
|
+
}
|
|
908
1065
|
);
|
|
909
1066
|
|
|
910
1067
|
const match = response.match(/\[[\s\S]*\]/);
|
|
@@ -4,6 +4,7 @@ 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.
|
|
@@ -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,51 @@
|
|
|
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
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public SDK facade matching the prompt-level Retrivora API.
|
|
9
|
+
*
|
|
10
|
+
* It keeps provider details behind configuration and delegates execution to the
|
|
11
|
+
* existing Pipeline implementation.
|
|
12
|
+
*/
|
|
13
|
+
export class Retrivora {
|
|
14
|
+
private readonly pipeline: Pipeline;
|
|
15
|
+
readonly config: RagConfig;
|
|
16
|
+
|
|
17
|
+
constructor(config?: UniversalRagConfig) {
|
|
18
|
+
this.config = ConfigResolver.resolveUniversal(config);
|
|
19
|
+
this.pipeline = new Pipeline(this.config);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async initialize(): Promise<void> {
|
|
23
|
+
await ConfigValidator.validateAndThrow(this.config);
|
|
24
|
+
await this.pipeline.initialize();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async ingest(
|
|
28
|
+
documents: IngestDocument[],
|
|
29
|
+
namespace?: string,
|
|
30
|
+
): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
|
|
31
|
+
return this.pipeline.ingest(documents, namespace);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
|
|
35
|
+
return this.pipeline.ask(question, history, namespace);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
askStream(
|
|
39
|
+
question: string,
|
|
40
|
+
history: ChatMessage[] = [],
|
|
41
|
+
namespace?: string,
|
|
42
|
+
): AsyncIterable<string | ChatResponse> {
|
|
43
|
+
return this.pipeline.askStream(question, history, namespace);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getPipeline(): Pipeline {
|
|
47
|
+
return this.pipeline;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type { RetrievalConfig, UniversalRagConfig };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named SDK exceptions make failures machine-readable for host applications.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type RetrivoraErrorCode =
|
|
6
|
+
| 'PROVIDER_NOT_FOUND'
|
|
7
|
+
| 'EMBEDDING_FAILED'
|
|
8
|
+
| 'RETRIEVAL_FAILED'
|
|
9
|
+
| 'RATE_LIMITED'
|
|
10
|
+
| 'CONFIGURATION_ERROR'
|
|
11
|
+
| 'AUTHENTICATION_ERROR';
|
|
12
|
+
|
|
13
|
+
export class RetrivoraError extends Error {
|
|
14
|
+
readonly code: RetrivoraErrorCode;
|
|
15
|
+
readonly details?: unknown;
|
|
16
|
+
|
|
17
|
+
constructor(message: string, code: RetrivoraErrorCode, details?: unknown) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = new.target.name;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ProviderNotFoundException extends RetrivoraError {
|
|
26
|
+
constructor(providerType: string, provider: string, details?: unknown) {
|
|
27
|
+
super(`Unsupported ${providerType} provider: ${provider}`, 'PROVIDER_NOT_FOUND', details);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class EmbeddingFailedException extends RetrivoraError {
|
|
32
|
+
constructor(message = 'Embedding generation failed', details?: unknown) {
|
|
33
|
+
super(message, 'EMBEDDING_FAILED', details);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class RetrievalException extends RetrivoraError {
|
|
38
|
+
constructor(message = 'Retrieval failed', details?: unknown) {
|
|
39
|
+
super(message, 'RETRIEVAL_FAILED', details);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class RateLimitException extends RetrivoraError {
|
|
44
|
+
constructor(message = 'Provider rate limit exceeded', details?: unknown) {
|
|
45
|
+
super(message, 'RATE_LIMITED', details);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ConfigurationException extends RetrivoraError {
|
|
50
|
+
constructor(message: string, details?: unknown) {
|
|
51
|
+
super(message, 'CONFIGURATION_ERROR', details);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class AuthenticationException extends RetrivoraError {
|
|
56
|
+
constructor(message = 'Provider authentication failed', details?: unknown) {
|
|
57
|
+
super(message, 'AUTHENTICATION_ERROR', details);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/handlers/index.ts
CHANGED
|
@@ -162,6 +162,8 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
162
162
|
if (!isActive) break;
|
|
163
163
|
if (typeof chunk === 'string') {
|
|
164
164
|
enqueue(sseTextFrame(chunk));
|
|
165
|
+
} else if (chunk && typeof chunk === 'object' && 'type' in chunk && (chunk as any).type === 'thinking') {
|
|
166
|
+
enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
165
167
|
} else {
|
|
166
168
|
// Retrieval metadata object — always the final frame
|
|
167
169
|
enqueue(sseMetaFrame(chunk));
|