@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.
Files changed (69) hide show
  1. package/README.md +59 -7
  2. package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
  3. package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +737 -237
  7. package/dist/handlers/index.mjs +736 -237
  8. package/dist/index-B9J_XEh0.d.ts +187 -0
  9. package/dist/index-BJ4cd-t5.d.mts +187 -0
  10. package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
  11. package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
  12. package/dist/index.css +237 -10
  13. package/dist/index.d.mts +13 -5
  14. package/dist/index.d.ts +13 -5
  15. package/dist/index.js +365 -164
  16. package/dist/index.mjs +350 -158
  17. package/dist/server.d.mts +15 -94
  18. package/dist/server.d.ts +15 -94
  19. package/dist/server.js +850 -239
  20. package/dist/server.mjs +839 -238
  21. package/package.json +2 -4
  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 +168 -148
  28. package/src/app/layout.tsx +5 -18
  29. package/src/app/types.ts +17 -17
  30. package/src/components/ChatWidget.tsx +3 -1
  31. package/src/components/ChatWindow.tsx +5 -1
  32. package/src/components/DocViewer.tsx +71 -5
  33. package/src/components/Documentation.tsx +74 -11
  34. package/src/components/MessageBubble.tsx +39 -2
  35. package/src/components/ThinkingBlock.tsx +75 -0
  36. package/src/components/VisualizationRenderer.tsx +27 -1
  37. package/src/components/constants.tsx +275 -0
  38. package/src/config/RagConfig.ts +47 -0
  39. package/src/config/constants.ts +1 -0
  40. package/src/config/serverConfig.ts +25 -0
  41. package/src/core/ConfigResolver.ts +73 -22
  42. package/src/core/ConfigValidator.ts +2 -1
  43. package/src/core/Pipeline.ts +226 -68
  44. package/src/core/ProviderRegistry.ts +16 -7
  45. package/src/core/QueryProcessor.ts +38 -4
  46. package/src/core/Retrivora.ts +91 -0
  47. package/src/core/VectorPlugin.ts +62 -8
  48. package/src/exceptions/index.ts +111 -0
  49. package/src/handlers/index.ts +73 -0
  50. package/src/hooks/useRagChat.ts +30 -5
  51. package/src/index.ts +27 -1
  52. package/src/lib/plugin.ts +24 -0
  53. package/src/llm/LLMFactory.ts +8 -4
  54. package/src/llm/providers/AnthropicProvider.ts +70 -20
  55. package/src/llm/providers/GeminiProvider.ts +14 -15
  56. package/src/llm/providers/OllamaProvider.ts +13 -16
  57. package/src/llm/providers/OpenAIProvider.ts +9 -14
  58. package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
  59. package/src/llm/utils.ts +46 -0
  60. package/src/providers/vectordb/MongoDBProvider.ts +9 -4
  61. package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
  62. package/src/rag/EntityExtractor.ts +2 -2
  63. package/src/rag/Reranker.ts +9 -16
  64. package/src/server.ts +30 -1
  65. package/src/types/chat.ts +9 -0
  66. package/src/types/props.ts +38 -1
  67. package/src/utils/UITransformer.ts +73 -4
  68. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  69. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
@@ -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: prompt }],
121
+ [{ role: 'user', content: `CONTEXT:\n${truncatedContext}\n\nANSWER:\n${answer}` }],
134
122
  '',
135
- { temperature: 0, maxTokens: 120 },
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
- this.config.llm.systemPrompt = chartInstruction;
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 Error(
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
- const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.0;
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 blocks the stream.
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 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
- });
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 + restrictionSuffix } as ChatMessage;
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
- fullReply += chunk;
585
- yield chunk;
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
- fullReply = reply;
590
- yield reply;
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. Yield metadata frame IMMEDIATELY after generation don't wait for scoring/UI.
616
- // Hallucination scoring and UI transformation are kicked off as background tasks.
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 + restrictionSuffix,
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
- } 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 */ });
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
- trainedSchema?: SchemaMap,
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, trainedSchema);
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
- if (forceDeterministic) {
676
- return UITransformer.transform(question, sources, this.config, trainedSchema);
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, trainedSchema);
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 strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmRouter.get('fast'));
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 (config: VectorDBConfig): BaseVectorProvider;
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 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,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
+