@retrivora-ai/rag-engine 0.4.5 → 1.0.1

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 (74) hide show
  1. package/README.md +32 -57
  2. package/dist/{ChromaDBProvider-GI7TB7GJ.mjs → ChromaDBProvider-MIDOR4FW.mjs} +2 -2
  3. package/dist/{DocumentChunker-3yElxTO3.d.mts → DocumentChunker-C-sCZPhi.d.mts} +6 -6
  4. package/dist/{DocumentChunker-3yElxTO3.d.ts → DocumentChunker-C-sCZPhi.d.ts} +6 -6
  5. package/dist/{MilvusProvider-WDVTFB7D.mjs → MilvusProvider-U7SKC27V.mjs} +2 -2
  6. package/dist/{MongoDBProvider-RE3Q5S5B.mjs → MongoDBProvider-Z6ALOVDN.mjs} +2 -2
  7. package/dist/{PineconeProvider-BE2JWSPD.mjs → PineconeProvider-QZNRKTN2.mjs} +2 -2
  8. package/dist/{PostgreSQLProvider-5HHTK4SU.mjs → PostgreSQLProvider-BMOETDZA.mjs} +2 -2
  9. package/dist/{QdrantProvider-XVDVBNIG.mjs → QdrantProvider-YNUNEOZH.mjs} +2 -2
  10. package/dist/{RagConfig-BgRDL9Vy.d.mts → RagConfig-DRJO4hGU.d.mts} +12 -1
  11. package/dist/{RagConfig-BgRDL9Vy.d.ts → RagConfig-DRJO4hGU.d.ts} +12 -1
  12. package/dist/{RedisProvider-EK2R2PQH.mjs → RedisProvider-SR65SCKV.mjs} +2 -2
  13. package/dist/{SimpleGraphProvider-M6T7SE7D.mjs → SimpleGraphProvider-SLOXO4M7.mjs} +1 -1
  14. package/dist/{UniversalVectorProvider-YIDRX6VT.mjs → UniversalVectorProvider-IN67OS56.mjs} +3 -3
  15. package/dist/{WeaviateProvider-4CAPQ7UY.mjs → WeaviateProvider-5FWDFITI.mjs} +2 -2
  16. package/dist/{chunk-5KNBWQM6.mjs → chunk-3DSHW676.mjs} +5 -1
  17. package/dist/{chunk-PRC5CZIZ.mjs → chunk-5W2YWFT3.mjs} +1248 -1361
  18. package/dist/{chunk-H6RKMU7W.mjs → chunk-5YGUXK7Z.mjs} +1 -1
  19. package/dist/{chunk-PQKTC73Y.mjs → chunk-CD6TSNL4.mjs} +67 -6
  20. package/dist/{chunk-RK2UDJA2.mjs → chunk-CFVEZTBJ.mjs} +1 -1
  21. package/dist/{chunk-LJWWPTWE.mjs → chunk-FLOSGE6A.mjs} +76 -14
  22. package/dist/{chunk-GQT5LF4G.mjs → chunk-LR3VMDVK.mjs} +2 -2
  23. package/dist/{chunk-KTS3LLHY.mjs → chunk-M6JSPGAR.mjs} +5 -5
  24. package/dist/{chunk-3QWAK3RZ.mjs → chunk-U55XRW3U.mjs} +6 -2
  25. package/dist/{chunk-XCNXPECE.mjs → chunk-VUQJVIJT.mjs} +55 -1
  26. package/dist/chunk-X4TOT24V.mjs +89 -0
  27. package/dist/{chunk-EDLTMSNY.mjs → chunk-YLTMFW4M.mjs} +1 -1
  28. package/dist/handlers/index.d.mts +2 -2
  29. package/dist/handlers/index.d.ts +2 -2
  30. package/dist/handlers/index.js +1488 -1371
  31. package/dist/handlers/index.mjs +5 -3
  32. package/dist/index-B2mutkgp.d.ts +116 -0
  33. package/dist/index-Bjy0es5a.d.mts +116 -0
  34. package/dist/index.d.mts +17 -11
  35. package/dist/index.d.ts +17 -11
  36. package/dist/index.js +253 -363
  37. package/dist/index.mjs +243 -353
  38. package/dist/server.d.mts +108 -158
  39. package/dist/server.d.ts +108 -158
  40. package/dist/server.js +1452 -1386
  41. package/dist/server.mjs +12 -12
  42. package/package.json +5 -1
  43. package/src/config/RagConfig.ts +7 -0
  44. package/src/core/ConfigValidator.ts +66 -492
  45. package/src/core/LangChainAgent.ts +78 -0
  46. package/src/core/Pipeline.ts +210 -240
  47. package/src/core/ProviderHealthCheck.ts +35 -406
  48. package/src/core/ProviderInterfaces.ts +37 -0
  49. package/src/core/ProviderRegistry.ts +70 -55
  50. package/src/core/QueryProcessor.ts +173 -0
  51. package/src/core/VectorPlugin.ts +7 -0
  52. package/src/handlers/index.ts +45 -0
  53. package/src/llm/ILLMProvider.ts +10 -0
  54. package/src/llm/LLMFactory.ts +33 -13
  55. package/src/llm/providers/AnthropicProvider.ts +55 -15
  56. package/src/llm/providers/GeminiProvider.ts +51 -0
  57. package/src/llm/providers/OllamaProvider.ts +100 -15
  58. package/src/llm/providers/OpenAIProvider.ts +60 -11
  59. package/src/providers/vectordb/BaseVectorProvider.ts +11 -0
  60. package/src/providers/vectordb/MilvusProvider.ts +4 -0
  61. package/src/providers/vectordb/MongoDBProvider.ts +72 -8
  62. package/src/providers/vectordb/PineconeProvider.ts +60 -5
  63. package/src/providers/vectordb/PostgreSQLProvider.ts +84 -14
  64. package/src/providers/vectordb/QdrantProvider.ts +4 -0
  65. package/src/providers/vectordb/WeaviateProvider.ts +8 -4
  66. package/src/rag/DocumentChunker.ts +15 -19
  67. package/src/rag/EntityExtractor.ts +3 -0
  68. package/src/rag/LlamaIndexIngestor.ts +61 -0
  69. package/src/rag/Reranker.ts +20 -0
  70. package/src/server.ts +1 -1
  71. package/src/types/index.ts +9 -0
  72. package/dist/chunk-FWCSY2DS.mjs +0 -37
  73. package/dist/index-7qeLTPBL.d.mts +0 -114
  74. package/dist/index-DowY4_K0.d.ts +0 -114
@@ -3,123 +3,138 @@ import { BaseGraphProvider } from '@/providers/graphdb/BaseGraphProvider';
3
3
  import { VectorDBConfig, GraphDBConfig, LLMConfig, EmbeddingConfig } from '../config/RagConfig';
4
4
  import { LLMFactory } from '../llm/LLMFactory';
5
5
  import { ILLMProvider } from '../llm/ILLMProvider';
6
+ import { IProviderValidator, IProviderHealthChecker } from './ProviderInterfaces';
6
7
 
7
8
  /**
8
9
  * ProviderRegistry — dynamic provider loader for Vector DBs and LLMs.
9
- *
10
- * Supports:
11
- * - Built-in providers loaded on-demand via dynamic imports (tree-shaking friendly)
12
- * - Custom provider registration via registerVectorProvider()
13
10
  */
11
+ type VectorProviderClass = {
12
+ new (config: VectorDBConfig): BaseVectorProvider;
13
+ getValidator?: () => IProviderValidator;
14
+ getHealthChecker?: () => IProviderHealthChecker;
15
+ };
16
+
17
+ type GraphProviderClass = new (config: GraphDBConfig) => BaseGraphProvider;
18
+
14
19
  export class ProviderRegistry {
15
- private static vectorProviders: Record<string, new (config: VectorDBConfig) => BaseVectorProvider> = {};
16
- private static graphProviders: Record<string, new (config: GraphDBConfig) => BaseGraphProvider> = {};
20
+ private static vectorProviders: Record<string, VectorProviderClass> = {};
21
+ private static graphProviders: Record<string, GraphProviderClass> = {};
22
+
23
+ private static vectorValidators: Record<string, IProviderValidator> = {};
24
+ private static vectorHealthCheckers: Record<string, IProviderHealthChecker> = {};
25
+
26
+ private static llmValidators: Record<string, IProviderValidator> = {};
27
+ private static llmHealthCheckers: Record<string, IProviderHealthChecker> = {};
17
28
 
18
- static registerVectorProvider(name: string, providerClass: new (config: VectorDBConfig) => BaseVectorProvider) {
29
+ static registerVectorProvider(name: string, providerClass: VectorProviderClass) {
19
30
  this.vectorProviders[name] = providerClass;
31
+ if (providerClass.getValidator) {
32
+ this.vectorValidators[name] = providerClass.getValidator();
33
+ }
34
+ if (providerClass.getHealthChecker) {
35
+ this.vectorHealthCheckers[name] = providerClass.getHealthChecker();
36
+ }
20
37
  }
21
38
 
22
- /**
23
- * Register a custom graph provider class by name.
24
- */
25
- static registerGraphProvider(name: string, providerClass: new (config: GraphDBConfig) => BaseGraphProvider) {
26
- this.graphProviders[name] = providerClass;
39
+ static async getVectorValidator(provider: string): Promise<IProviderValidator | null> {
40
+ if (this.vectorValidators[provider]) return this.vectorValidators[provider];
41
+
42
+ // Try to load built-in validator
43
+ try {
44
+ const providerClass = await this.loadVectorProviderClass(provider);
45
+ if (providerClass.getValidator) {
46
+ this.vectorValidators[provider] = providerClass.getValidator();
47
+ return this.vectorValidators[provider];
48
+ }
49
+ } catch (e) {
50
+ console.warn(`[ProviderRegistry] Failed to load validator for ${provider}:`, e);
51
+ }
52
+ return null;
27
53
  }
28
54
 
29
- /**
30
- * Creates a vector database provider based on the configuration.
31
- * Built-in providers are dynamically imported to avoid bundling all SDKs.
32
- */
33
- static async createVectorProvider(config: VectorDBConfig): Promise<BaseVectorProvider> {
34
- const { provider } = config;
35
-
36
- // Custom registered provider takes priority
37
- if (this.vectorProviders[provider]) {
38
- return new this.vectorProviders[provider](config);
55
+ static async getVectorHealthChecker(provider: string): Promise<IProviderHealthChecker | null> {
56
+ if (this.vectorHealthCheckers[provider]) return this.vectorHealthCheckers[provider];
57
+
58
+ try {
59
+ const providerClass = await this.loadVectorProviderClass(provider);
60
+ if (providerClass.getHealthChecker) {
61
+ this.vectorHealthCheckers[provider] = providerClass.getHealthChecker();
62
+ return this.vectorHealthCheckers[provider];
63
+ }
64
+ } catch (e) {
65
+ console.warn(`[ProviderRegistry] Failed to load health checker for ${provider}:`, e);
39
66
  }
67
+ return null;
68
+ }
69
+
70
+ private static async loadVectorProviderClass(provider: string): Promise<VectorProviderClass> {
71
+ if (this.vectorProviders[provider]) return this.vectorProviders[provider];
40
72
 
41
- // Built-in providers — lazy-loaded
42
73
  switch (provider) {
43
74
  case 'pinecone': {
44
75
  const { PineconeProvider } = await import('../providers/vectordb/PineconeProvider');
45
- return new PineconeProvider(config);
76
+ return PineconeProvider;
46
77
  }
47
78
  case 'pgvector':
48
79
  case 'postgresql': {
49
80
  const { PostgreSQLProvider } = await import('../providers/vectordb/PostgreSQLProvider');
50
- return new PostgreSQLProvider(config);
81
+ return PostgreSQLProvider;
51
82
  }
52
83
  case 'mongodb': {
53
84
  const { MongoDBProvider } = await import('../providers/vectordb/MongoDBProvider');
54
- return new MongoDBProvider(config);
85
+ return MongoDBProvider;
55
86
  }
56
87
  case 'milvus': {
57
88
  const { MilvusProvider } = await import('../providers/vectordb/MilvusProvider');
58
- return new MilvusProvider(config);
89
+ return MilvusProvider;
59
90
  }
60
91
  case 'qdrant': {
61
92
  const { QdrantProvider } = await import('../providers/vectordb/QdrantProvider');
62
- return new QdrantProvider(config);
93
+ return QdrantProvider;
63
94
  }
64
95
  case 'chromadb': {
65
96
  const { ChromaDBProvider } = await import('../providers/vectordb/ChromaDBProvider');
66
- return new ChromaDBProvider(config);
97
+ return ChromaDBProvider;
67
98
  }
68
99
  case 'redis': {
69
100
  const { RedisProvider } = await import('../providers/vectordb/RedisProvider');
70
- return new RedisProvider(config);
101
+ return RedisProvider;
71
102
  }
72
103
  case 'weaviate': {
73
104
  const { WeaviateProvider } = await import('../providers/vectordb/WeaviateProvider');
74
- return new WeaviateProvider(config);
105
+ return WeaviateProvider;
75
106
  }
76
107
  case 'universal_rest':
77
108
  case 'rest': {
78
109
  const { UniversalVectorProvider } = await import('../providers/vectordb/UniversalVectorProvider');
79
- return new UniversalVectorProvider(config);
110
+ return UniversalVectorProvider;
80
111
  }
81
112
  default:
82
- throw new Error(
83
- `[ProviderRegistry] Unsupported vector provider: "${provider}". ` +
84
- `Built-in providers: pinecone | pgvector | postgresql | mongodb | milvus | qdrant | chromadb | redis | weaviate | universal_rest. ` +
85
- `For custom providers, call ProviderRegistry.registerVectorProvider("${provider}", YourClass).`
86
- );
113
+ throw new Error(`Unsupported vector provider: ${provider}`);
87
114
  }
88
115
  }
89
116
 
90
- /**
91
- * Creates a graph database provider based on the configuration.
92
- */
117
+ static async createVectorProvider(config: VectorDBConfig): Promise<BaseVectorProvider> {
118
+ const providerClass = await this.loadVectorProviderClass(config.provider);
119
+ return new providerClass(config);
120
+ }
121
+
93
122
  static async createGraphProvider(config: GraphDBConfig): Promise<BaseGraphProvider> {
94
123
  const { provider } = config;
95
-
96
- // Custom registered provider takes priority
97
124
  if (this.graphProviders[provider]) {
98
125
  return new this.graphProviders[provider](config);
99
126
  }
100
127
 
101
- // Built-in providers — lazy-loaded
102
128
  switch (provider) {
103
- case 'neo4j': {
104
- // Placeholder for real Neo4j provider
105
- throw new Error('[ProviderRegistry] Neo4j provider not implemented yet.');
106
- }
107
129
  case 'simple': {
108
130
  const { SimpleGraphProvider } = await import('@/providers/graphdb/SimpleGraphProvider');
109
131
  return new SimpleGraphProvider(config);
110
132
  }
111
133
  default:
112
- throw new Error(
113
- `[ProviderRegistry] Unsupported graph provider: "${provider}". ` +
114
- `Built-in providers: simple. ` +
115
- `For custom providers, call ProviderRegistry.registerGraphProvider("${provider}", YourClass).`
116
- );
134
+ throw new Error(`Unsupported graph provider: ${provider}`);
117
135
  }
118
136
  }
119
137
 
120
- /**
121
- * Creates an LLM provider based on the configuration.
122
- */
123
138
  static createLLMProvider(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig): ILLMProvider {
124
139
  return LLMFactory.create(llmConfig, embeddingConfig);
125
140
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * QueryProcessor.ts — Utilities for pre-processing user questions.
3
+ *
4
+ * Responsibilities:
5
+ * - Extracting semantic hints from natural language (emails, IDs, quoted phrases).
6
+ * - Normalizing text values for consistent filtering.
7
+ * - Building structured query filters for vector databases.
8
+ */
9
+
10
+ export interface QueryFieldHint {
11
+ field?: string;
12
+ value: string;
13
+ }
14
+
15
+ export interface QueryFilter {
16
+ metadata?: Record<string, string>;
17
+ keywords?: string[];
18
+ queryText?: string;
19
+ [key: string]: unknown;
20
+ }
21
+
22
+ export class QueryProcessor {
23
+ /**
24
+ * Normalizes a string value by collapsing whitespace and trimming.
25
+ */
26
+ static normalizeHintValue(value: string): string {
27
+ return value.replace(/\s+/g, ' ').trim();
28
+ }
29
+
30
+ /**
31
+ * Checks if a string is likely a question word or common prompt phrase.
32
+ */
33
+ static isLikelyPromptPhrase(value: string): boolean {
34
+ return /^(what|which|who|where|when|why|how)\b/i.test(value.trim());
35
+ }
36
+
37
+ /**
38
+ * Scans a natural language question for potential metadata hints and keywords.
39
+ */
40
+ static extractQueryFieldHints(question: string): QueryFieldHint[] {
41
+ if (!question.trim()) return [];
42
+
43
+ const hints = new Map<string, QueryFieldHint>();
44
+
45
+ const addHint = (value: string, field?: string) => {
46
+ const normalizedValue = this.normalizeHintValue(value);
47
+ if (!normalizedValue) return;
48
+
49
+ const normalizedField = field
50
+ ? field
51
+ .toLowerCase()
52
+ .replace(/[^a-z0-9]+/g, ' ')
53
+ .trim()
54
+ : undefined;
55
+
56
+ const key = `${normalizedField ?? '*'}::${normalizedValue.toLowerCase()}`;
57
+ if (!hints.has(key)) {
58
+ hints.set(key, {
59
+ value: normalizedValue,
60
+ ...(normalizedField ? { field: normalizedField } : {}),
61
+ });
62
+ }
63
+ };
64
+
65
+ // 1. Quoted phrases
66
+ for (const match of question.matchAll(/["']([^"']{2,100})["']/g)) {
67
+ addHint(match[1]);
68
+ }
69
+
70
+ // 2. Natural question patterns
71
+ const naturalQuestionPatterns = [
72
+ /\b(?:what|which)\s+(?:is|are|was|were)\s+(?:the\s+)?([^?.!,]{1,60}?)\s+of\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
73
+ /\b(?:who|what)\s+(?:is|are|was|were)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
74
+ /\b(?:about|for|regarding)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
75
+ ];
76
+
77
+ // 3. Person/Company targeted patterns
78
+ const personCompanyPatterns = [
79
+ /\bcompany(?:\s+name)?\s+(?:of|for)\s+["']?([^"'\n?.!,]{2,120})["']?(?=[?.!,]|$)/gi,
80
+ /\b(?:which|what)\s+company\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
81
+ /\bwhere\s+does\s+["']?([^"'\n?.!,]{2,120})["']?\s+work(?:\s+for|\s+at)?(?=[?.!,]|$)/gi,
82
+ ];
83
+
84
+ for (const pattern of personCompanyPatterns) {
85
+ for (const match of question.matchAll(pattern)) {
86
+ const name = match[1];
87
+ if (name) addHint(name, 'name');
88
+ }
89
+ }
90
+
91
+ // 4. Universal patterns (email, phone, etc.)
92
+ const universalPatterns: Array<{ regex: RegExp; field?: string; group?: number }> = [
93
+ { regex: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/gi, field: 'email', group: 1 },
94
+ { regex: /(\+?\d[\d\-\.\s\(\)]{6,}\d)/g, field: 'phone', group: 1 },
95
+ { regex: /(\b\d{4}-\d{2}-\d{2}\b|\b\d{1,2}\/\d{1,2}\/\d{2,4}\b)/g, field: 'date', group: 1 },
96
+ { regex: /(\$\s?\d{1,3}(?:,\d{3})*(?:\.\d+)?)/g, field: 'amount', group: 1 },
97
+ { regex: /\b(ID|id|identifier)[: ]\s*([A-Za-z0-9\-]{3,})\b/gi, field: 'id', group: 2 },
98
+ { regex: /"([^"]{2,120})"/g, group: 1 },
99
+ { regex: /'([^']{2,120})'/g, group: 1 },
100
+ ];
101
+
102
+ for (const p of universalPatterns) {
103
+ for (const match of question.matchAll(p.regex)) {
104
+ const val = p.group ? match[p.group] ?? match[0] : match[0];
105
+ if (!val) continue;
106
+ if (p.field) addHint(val, p.field);
107
+ else addHint(val);
108
+ }
109
+ }
110
+
111
+ for (const pattern of naturalQuestionPatterns) {
112
+ for (const match of question.matchAll(pattern)) {
113
+ const value = match[2] ?? match[1];
114
+ if (value) addHint(value);
115
+ }
116
+ }
117
+
118
+ // 5. Explicit field-value pairs (e.g., status=active, priority:high)
119
+ const fieldPattern = `([^\\n:=?.!,]{1,60}?)`;
120
+ const valuePattern = `([^\\n?.!,]{1,120}?)`;
121
+ const fieldValuePatterns = [
122
+ new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
123
+ new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
124
+ new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
125
+ ];
126
+
127
+ for (const pattern of fieldValuePatterns) {
128
+ for (const match of question.matchAll(pattern)) {
129
+ const field = this.normalizeHintValue(match[1] ?? '');
130
+ const value = match[2] ?? '';
131
+
132
+ if (field && !this.isLikelyPromptPhrase(field)) {
133
+ addHint(value, field);
134
+ } else {
135
+ addHint(value);
136
+ }
137
+ }
138
+ }
139
+
140
+ // 6. Capitalized proper nouns
141
+ for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
142
+ addHint(match[0]);
143
+ }
144
+
145
+ return [...hints.values()];
146
+ }
147
+
148
+ /**
149
+ * Constructs a QueryFilter object from extracted hints.
150
+ */
151
+ static buildQueryFilter(question: string, hints: QueryFieldHint[]): QueryFilter {
152
+ const filter: QueryFilter = { metadata: {}, keywords: [], queryText: question };
153
+
154
+ for (const hint of hints) {
155
+ if (hint.field) {
156
+ filter.metadata![hint.field] = hint.value;
157
+ } else {
158
+ filter.keywords!.push(hint.value);
159
+ }
160
+ }
161
+
162
+ // Capture proper-noun sequences as keywords
163
+ for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
164
+ const term = this.normalizeHintValue(match[0]);
165
+ if (term && !filter.keywords!.includes(term)) filter.keywords!.push(term);
166
+ }
167
+
168
+ if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
169
+ if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
170
+
171
+ return filter;
172
+ }
173
+ }
@@ -83,6 +83,13 @@ export class VectorPlugin {
83
83
  return this.pipeline.ask(message, history, namespace);
84
84
  }
85
85
 
86
+ /**
87
+ * Run a streaming chat query.
88
+ */
89
+ async *chatStream(message: string, history: ChatMessage[] = [], namespace?: string) {
90
+ yield* this.pipeline.askStream(message, history, namespace);
91
+ }
92
+
86
93
  /**
87
94
  * Ingest documents into the vector database.
88
95
  */
@@ -32,6 +32,51 @@ export function createChatHandler(config?: Partial<RagConfig>) {
32
32
  };
33
33
  }
34
34
 
35
+ /**
36
+ * createStreamHandler — factory that returns a streaming Next.js App Router POST handler
37
+ */
38
+ export function createStreamHandler(config?: Partial<RagConfig>) {
39
+ const plugin = new VectorPlugin(config);
40
+
41
+ return async function POST(req: NextRequest) {
42
+ try {
43
+ const { message, history = [], namespace } = await req.json();
44
+
45
+ const encoder = new TextEncoder();
46
+ const stream = new ReadableStream({
47
+ async start(controller) {
48
+ const pipelineStream = plugin.chatStream(message, history, namespace);
49
+
50
+ for await (const chunk of pipelineStream) {
51
+ if (typeof chunk === 'string') {
52
+ controller.enqueue(encoder.encode(chunk));
53
+ } else {
54
+ // Yield metadata/sources at the end
55
+ controller.enqueue(encoder.encode(`\n\n__METADATA__${JSON.stringify(chunk)}`));
56
+ }
57
+ }
58
+ controller.close();
59
+ },
60
+ });
61
+
62
+ return new Response(stream, {
63
+ headers: {
64
+ 'Content-Type': 'text/event-stream',
65
+ 'Cache-Control': 'no-cache',
66
+ 'Connection': 'keep-alive',
67
+ },
68
+ });
69
+ } catch (err) {
70
+ const message = err instanceof Error ? err.message : 'Internal server error';
71
+ return new Response(JSON.stringify({ error: message }), {
72
+ status: 500,
73
+ headers: { 'Content-Type': 'application/json' },
74
+ });
75
+ }
76
+ };
77
+ }
78
+
79
+
35
80
  /**
36
81
  * createIngestHandler — factory for the document ingestion endpoint.
37
82
  */
@@ -39,6 +39,16 @@ export interface ILLMProvider {
39
39
  options?: ChatOptions
40
40
  ): Promise<string>;
41
41
 
42
+ /**
43
+ * Send a streaming chat completion request.
44
+ * @returns – an async iterable of text chunks
45
+ */
46
+ chatStream?(
47
+ messages: ChatMessage[],
48
+ context: string,
49
+ options?: ChatOptions
50
+ ): AsyncIterable<string>;
51
+
42
52
  /**
43
53
  * Generate an embedding vector for the given text.
44
54
  * @param text – text to embed
@@ -1,10 +1,3 @@
1
- /**
2
- * LLMFactory — instantiates the correct ILLMProvider based on llmConfig.provider.
3
- *
4
- * Also accepts an optional EmbeddingConfig so providers that support embedding
5
- * can be initialised with separate embedding credentials.
6
- */
7
-
8
1
  import { ILLMProvider } from './ILLMProvider';
9
2
  import { LLMConfig, EmbeddingConfig, LLMProvider } from '../config/RagConfig';
10
3
  import { OpenAIProvider } from './providers/OpenAIProvider';
@@ -12,7 +5,16 @@ import { AnthropicProvider } from './providers/AnthropicProvider';
12
5
  import { OllamaProvider } from './providers/OllamaProvider';
13
6
  import { GeminiProvider } from './providers/GeminiProvider';
14
7
  import { UniversalLLMAdapter } from './providers/UniversalLLMAdapter';
8
+ import { IProviderValidator, IProviderHealthChecker } from '../core/ProviderInterfaces';
15
9
 
10
+ interface LLMProviderStatic {
11
+ getValidator?: () => IProviderValidator;
12
+ getHealthChecker?: () => IProviderHealthChecker;
13
+ }
14
+
15
+ /**
16
+ * LLMFactory — instantiates the correct ILLMProvider based on llmConfig.provider.
17
+ */
16
18
  export class LLMFactory {
17
19
  static create(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig): ILLMProvider {
18
20
  switch (llmConfig.provider) {
@@ -29,20 +31,38 @@ export class LLMFactory {
29
31
  case 'custom':
30
32
  return new UniversalLLMAdapter(llmConfig);
31
33
  default:
32
- // If baseUrl is provided but provider is unknown, fallback to Universal Adapter
33
34
  if (llmConfig.baseUrl || (llmConfig.options as Record<string, unknown>)?.baseUrl) {
34
35
  return new UniversalLLMAdapter(llmConfig);
35
36
  }
36
- throw new Error(
37
- `[LLMFactory] Unknown provider "${llmConfig.provider}". ` +
38
- `Supported: openai | anthropic | ollama | rest | custom`
39
- );
37
+ throw new Error(`[LLMFactory] Unknown provider "${llmConfig.provider}"`);
38
+ }
39
+ }
40
+
41
+ static getValidator(provider: LLMProvider): IProviderValidator | null {
42
+ const providerClass = this.getProviderClass(provider);
43
+ return providerClass && providerClass.getValidator ? providerClass.getValidator() : null;
44
+ }
45
+
46
+ static getHealthChecker(provider: LLMProvider): IProviderHealthChecker | null {
47
+ const providerClass = this.getProviderClass(provider);
48
+ return providerClass && providerClass.getHealthChecker ? providerClass.getHealthChecker() : null;
49
+ }
50
+
51
+ private static getProviderClass(provider: string): LLMProviderStatic | null {
52
+ switch (provider) {
53
+ case 'openai': return OpenAIProvider;
54
+ case 'anthropic': return AnthropicProvider;
55
+ case 'ollama': return OllamaProvider;
56
+ case 'gemini': return GeminiProvider;
57
+ case 'rest':
58
+ case 'universal_rest':
59
+ case 'custom': return UniversalLLMAdapter as unknown as LLMProviderStatic;
60
+ default: return null;
40
61
  }
41
62
  }
42
63
 
43
64
  /**
44
65
  * Creates a dedicated embedding-only provider.
45
- * Useful when the LLM provider (e.g. Anthropic) doesn't support embeddings.
46
66
  */
47
67
  static createEmbeddingProvider(embeddingConfig: EmbeddingConfig): ILLMProvider {
48
68
  const fakeLLMConfig: LLMConfig = {
@@ -1,18 +1,12 @@
1
1
  /**
2
2
  * Anthropic (Claude) LLM Provider
3
- *
4
- * Handles chat completion via Anthropic's Messages API.
5
- * Note: Anthropic does NOT provide embedding models — use OpenAIProvider
6
- * or OllamaProvider for embedding when pairing with this provider.
7
- *
8
- * Required LLMConfig fields:
9
- * - apiKey: string – Anthropic API key
10
- * - model: string – e.g. "claude-3-5-sonnet-20241022"
11
3
  */
12
4
 
13
5
  import Anthropic from '@anthropic-ai/sdk';
14
6
  import { ILLMProvider, ChatMessage, ChatOptions, EmbedOptions } from '../ILLMProvider';
15
7
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
8
+ import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
9
+ import { ValidationError } from '../../core/ConfigValidator';
16
10
 
17
11
  export class AnthropicProvider implements ILLMProvider {
18
12
  private readonly client: Anthropic;
@@ -26,6 +20,59 @@ export class AnthropicProvider implements ILLMProvider {
26
20
  this.embeddingConfig = embeddingConfig;
27
21
  }
28
22
 
23
+ static getValidator(): IProviderValidator {
24
+ return {
25
+ validate(config: Record<string, unknown>): ValidationError[] {
26
+ const errors: ValidationError[] = [];
27
+ if (!config.apiKey) {
28
+ errors.push({
29
+ field: 'llm.apiKey',
30
+ message: 'Anthropic API key is required',
31
+ suggestion: 'Set ANTHROPIC_API_KEY environment variable',
32
+ severity: 'error',
33
+ });
34
+ }
35
+ if (!config.model) {
36
+ errors.push({
37
+ field: 'llm.model',
38
+ message: 'Anthropic model name is required',
39
+ suggestion: 'e.g., "claude-3-5-sonnet-20241022"',
40
+ severity: 'error',
41
+ });
42
+ }
43
+ return errors;
44
+ }
45
+ };
46
+ }
47
+
48
+ static getHealthChecker(): IProviderHealthChecker {
49
+ return {
50
+ async check(config: Record<string, unknown>): Promise<HealthCheckResult> {
51
+ const timestamp = Date.now();
52
+ const apiKey = config.apiKey as string;
53
+ const modelName = config.model as string;
54
+
55
+ try {
56
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
57
+ const client = new Anthropic({ apiKey });
58
+ await client.messages.create({
59
+ model: modelName,
60
+ max_tokens: 10,
61
+ messages: [{ role: 'user', content: 'ping' }],
62
+ });
63
+ return { healthy: true, provider: 'anthropic', capabilities: { model: modelName }, timestamp };
64
+ } catch (error) {
65
+ return {
66
+ healthy: false,
67
+ provider: 'anthropic',
68
+ error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
69
+ timestamp,
70
+ };
71
+ }
72
+ }
73
+ };
74
+ }
75
+
29
76
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
30
77
  const systemPrompt =
31
78
  this.llmConfig.systemPrompt ??
@@ -35,7 +82,6 @@ export class AnthropicProvider implements ILLMProvider {
35
82
  ? systemPrompt.replace('{{context}}', context)
36
83
  : `${systemPrompt}\n\nContext:\n${context}`;
37
84
 
38
- // Anthropic requires alternating user/assistant messages
39
85
  const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
40
86
  role: m.role === 'assistant' ? 'assistant' : 'user',
41
87
  content: m.content,
@@ -52,11 +98,6 @@ export class AnthropicProvider implements ILLMProvider {
52
98
  return block.type === 'text' ? block.text : '';
53
99
  }
54
100
 
55
- /**
56
- * Anthropic does not offer an embedding API.
57
- * This method throws with a clear error so developers know to configure
58
- * a separate embedding provider (OpenAI or Ollama).
59
- */
60
101
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
61
102
  void text;
62
103
  void options;
@@ -76,7 +117,6 @@ export class AnthropicProvider implements ILLMProvider {
76
117
 
77
118
  async ping(): Promise<boolean> {
78
119
  try {
79
- // Lightweight model list call to verify connectivity
80
120
  await this.client.models.list();
81
121
  return true;
82
122
  } catch (err) {