@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.
Files changed (60) hide show
  1. package/README.md +27 -0
  2. package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-DNhyOYoK.d.mts} +41 -1
  3. package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-DNhyOYoK.d.ts} +41 -1
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +552 -198
  7. package/dist/handlers/index.mjs +552 -198
  8. package/dist/index-C9v7-tWd.d.mts +183 -0
  9. package/dist/{index-B70ZLkfG.d.mts → index-CHL1jdYm.d.mts} +1 -1
  10. package/dist/index-CjQdL0cX.d.ts +183 -0
  11. package/dist/{index-DVu-mkAM.d.ts → index-Hgbwl9X4.d.ts} +1 -1
  12. package/dist/index.css +40 -0
  13. package/dist/index.d.mts +3 -3
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +326 -158
  16. package/dist/index.mjs +312 -151
  17. package/dist/server.d.mts +15 -94
  18. package/dist/server.d.ts +15 -94
  19. package/dist/server.js +612 -198
  20. package/dist/server.mjs +604 -198
  21. package/package.json +1 -1
  22. package/src/app/api/chat/route.ts +2 -2
  23. package/src/app/api/health/route.ts +3 -2
  24. package/src/app/api/ingest/route.ts +3 -2
  25. package/src/app/api/suggestions/route.ts +3 -2
  26. package/src/app/api/upload/route.ts +3 -2
  27. package/src/app/constants.tsx +85 -30
  28. package/src/app/layout.tsx +18 -7
  29. package/src/components/MessageBubble.tsx +39 -2
  30. package/src/components/ThinkingBlock.tsx +75 -0
  31. package/src/components/VisualizationRenderer.tsx +27 -1
  32. package/src/config/RagConfig.ts +47 -0
  33. package/src/config/serverConfig.ts +25 -0
  34. package/src/core/ConfigResolver.ts +56 -4
  35. package/src/core/ConfigValidator.ts +2 -1
  36. package/src/core/Pipeline.ts +224 -67
  37. package/src/core/ProviderRegistry.ts +11 -2
  38. package/src/core/QueryProcessor.ts +38 -4
  39. package/src/core/Retrivora.ts +51 -0
  40. package/src/exceptions/index.ts +59 -0
  41. package/src/handlers/index.ts +2 -0
  42. package/src/hooks/useRagChat.ts +26 -4
  43. package/src/index.ts +25 -1
  44. package/src/lib/plugin.ts +24 -0
  45. package/src/llm/LLMFactory.ts +4 -1
  46. package/src/llm/providers/AnthropicProvider.ts +70 -20
  47. package/src/llm/providers/GeminiProvider.ts +14 -15
  48. package/src/llm/providers/OllamaProvider.ts +13 -16
  49. package/src/llm/providers/OpenAIProvider.ts +9 -14
  50. package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
  51. package/src/llm/utils.ts +46 -0
  52. package/src/providers/vectordb/MongoDBProvider.ts +9 -4
  53. package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
  54. package/src/rag/EntityExtractor.ts +2 -2
  55. package/src/rag/Reranker.ts +9 -16
  56. package/src/server.ts +27 -1
  57. package/src/types/chat.ts +7 -0
  58. package/src/utils/UITransformer.ts +73 -4
  59. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  60. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
@@ -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: prompt }],
120
+ [{ role: 'user', content: `CONTEXT:\n${truncatedContext}\n\nANSWER:\n${answer}` }],
134
121
  '',
135
- { temperature: 0, maxTokens: 120 },
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
- this.config.llm.systemPrompt = chartInstruction;
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
- const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.0;
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 blocks the stream.
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 trainedSchemaPromise = allMetadataKeys.length > 0
552
- ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => undefined)
553
- : Promise.resolve(undefined);
554
-
555
- // 4.6 Start UI Transformation concurrently with generation to drastically reduce latency
556
- const uiTransformationPromise = trainedSchemaPromise.then(trainedSchema =>
557
- this.generateUiTransformation(
558
- question,
559
- sources,
560
- trainedSchema,
561
- hasNumericPredicates,
562
- )
563
- ).catch(uiError => {
564
- console.warn('[Pipeline] UI transformation failed concurrently:', uiError);
565
- return UITransformer.transform(question, sources, this.config);
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 + restrictionSuffix } as ChatMessage;
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
- fullReply += chunk;
585
- yield chunk;
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
- fullReply = reply;
590
- yield reply;
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. Yield metadata frame IMMEDIATELY after generation don't wait for scoring/UI.
616
- // Hallucination scoring and UI transformation are kicked off as background tasks.
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 + restrictionSuffix,
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
- } as ChatResponse & { trace: ObservabilityTrace };
644
-
645
- // 8. Hallucination scoring fire-and-forget, yields an updated trace frame.
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
- trainedSchema?: SchemaMap,
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, trainedSchema);
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
- if (forceDeterministic) {
676
- return UITransformer.transform(question, sources, this.config, trainedSchema);
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, trainedSchema);
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 strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmRouter.get('fast'));
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 Error(`Unsupported vector provider: ${provider}`);
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 Error(`Unsupported graph provider: ${provider}`);
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
- addHint(value, field);
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 = this.normalizeHintValue(match[1] ?? '');
194
+ const field = match[1] ?? '';
165
195
  const value = match[2] ?? '';
166
196
 
167
- if (field && !this.isLikelyPromptPhrase(field)) {
168
- addHint(value, field);
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
+ }
@@ -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));