@retrivora-ai/rag-engine 1.0.4 → 1.0.5

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 (39) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{RagConfig-DRJO4hGU.d.mts → RagConfig-BEmz4hVP.d.mts} +58 -1
  4. package/dist/{RagConfig-DRJO4hGU.d.ts → RagConfig-BEmz4hVP.d.ts} +58 -1
  5. package/dist/{chunk-6MLZHQZT.mjs → chunk-MWL4AGQI.mjs} +189 -98
  6. package/dist/handlers/index.d.mts +2 -2
  7. package/dist/handlers/index.d.ts +2 -2
  8. package/dist/handlers/index.js +191 -100
  9. package/dist/handlers/index.mjs +11 -3
  10. package/dist/index-CSfaCeux.d.ts +206 -0
  11. package/dist/index-DFSy0CG6.d.mts +206 -0
  12. package/dist/index.d.mts +3 -4
  13. package/dist/index.d.ts +3 -4
  14. package/dist/index.js +103 -89
  15. package/dist/index.mjs +91 -95
  16. package/dist/server.d.mts +45 -97
  17. package/dist/server.d.ts +45 -97
  18. package/dist/server.js +271 -215
  19. package/dist/server.mjs +78 -167
  20. package/package.json +12 -10
  21. package/src/components/ChatWindow.tsx +2 -2
  22. package/src/components/ConfigProvider.tsx +2 -2
  23. package/src/components/MessageBubble.tsx +2 -2
  24. package/src/components/SourceCard.tsx +1 -1
  25. package/src/components/ThemeToggle.tsx +1 -1
  26. package/src/config/ConfigBuilder.ts +86 -211
  27. package/src/config/uiConstants.ts +23 -0
  28. package/src/core/ConfigResolver.ts +57 -29
  29. package/src/core/LangChainAgent.ts +73 -40
  30. package/src/core/Pipeline.ts +63 -7
  31. package/src/core/ProviderRegistry.ts +2 -2
  32. package/src/core/QueryProcessor.ts +9 -8
  33. package/src/handlers/index.ts +138 -49
  34. package/src/hooks/useRagChat.ts +71 -32
  35. package/src/server.ts +12 -2
  36. package/dist/DocumentChunker-C-sCZPhi.d.mts +0 -102
  37. package/dist/DocumentChunker-C-sCZPhi.d.ts +0 -102
  38. package/dist/index-B2mutkgp.d.ts +0 -116
  39. package/dist/index-Bjy0es5a.d.mts +0 -116
@@ -3,76 +3,109 @@ import { RagConfig } from "../config/RagConfig";
3
3
 
4
4
  /**
5
5
  * LangChainAgent — Orchestration layer that turns the RAG pipeline into an Agent.
6
- *
7
- * Uses dynamic imports to remain an optional dependency.
6
+ *
7
+ * Uses dynamic imports to keep LangChain an optional peer dependency.
8
+ * Requires `langchain` (v1.x) and `@langchain/core` to be installed.
9
+ *
10
+ * LangChain v1.x migration notes:
11
+ * - `AgentExecutor` and `createOpenAIFunctionsAgent` are removed.
12
+ * - The replacement is `createAgent` from the `langchain` root package,
13
+ * which returns a `ReactAgent` (LangGraph-backed ReAct agent).
14
+ * - Invocation uses `{ messages: [...] }` instead of `{ input, chat_history }`.
8
15
  */
9
16
  export class LangChainAgent {
10
- private executor?: unknown;
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ private agent?: any;
11
19
 
12
20
  constructor(private pipeline: Pipeline, private config: RagConfig) {}
13
21
 
14
22
  /**
15
- * Initializes the agent with the RAG pipeline as a primary tool.
16
- * Dynamically imports LangChain dependencies to avoid build errors if missing.
23
+ * Initializes the LangChain ReAct agent with the RAG pipeline as a tool.
24
+ * Dynamically imports langchain so the package doesn't crash if it isn't installed.
17
25
  */
18
26
  async initialize(chatModel: unknown) {
19
27
  try {
20
- // Dynamic imports for optional dependency support using template strings to hide from TS static analysis
21
- const { DynamicTool } = await import(`${"@langchain/core/tools"}`);
22
- const { ChatPromptTemplate, MessagesPlaceholder } = await import(`${"@langchain/core/prompts"}`);
23
- const { AgentExecutor, createOpenAIFunctionsAgent } = await import(`${"langchain/agents"}`);
28
+ const [{ DynamicTool }, { HumanMessage }, { createAgent }] = await Promise.all([
29
+ import('@langchain/core/tools'),
30
+ import('@langchain/core/messages'),
31
+ import('langchain'),
32
+ ]);
24
33
 
25
34
  // 1. Define the RAG Search Tool
26
35
  const searchTool = new DynamicTool({
27
- name: "document_search",
28
- description: "Use this tool to search through the knowledge base and document repository. Input should be a specific search query.",
36
+ name: 'document_search',
37
+ description:
38
+ 'Use this tool to search through the knowledge base and document repository. Input should be a specific search query.',
29
39
  func: async (query: string) => {
30
40
  const response = await this.pipeline.ask(query);
31
- return `Search Results:\n${response.reply}\n\nSources Used: ${JSON.stringify(response.sources.map(s => s.id))}`;
41
+ return `Search Results:\n${response.reply}\n\nSources Used: ${JSON.stringify(
42
+ response.sources.map((s) => s.id)
43
+ )}`;
32
44
  },
33
45
  });
34
46
 
35
- const tools = [searchTool];
36
-
37
- // 2. Define the Agent Prompt
38
- const prompt = ChatPromptTemplate.fromMessages([
39
- ["system", this.config.llm.systemPrompt || "You are a helpful AI assistant with access to a document search tool."],
40
- new MessagesPlaceholder("chat_history"),
41
- ["human", "{input}"],
42
- new MessagesPlaceholder("agent_scratchpad"),
43
- ]);
44
-
45
- // 3. Create the Agent
46
- const agent = await createOpenAIFunctionsAgent({
47
- llm: chatModel,
48
- tools,
49
- prompt,
47
+ // 2. Create the ReAct agent (langchain v1.x API)
48
+ // - `model` replaces the old `llm` parameter
49
+ // - `createAgent` replaces `createOpenAIFunctionsAgent` + `AgentExecutor`
50
+ this.agent = createAgent({
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ model: chatModel as any,
53
+ tools: [searchTool],
54
+ systemPrompt: this.config.llm.systemPrompt ||
55
+ 'You are a helpful AI assistant with access to a document search tool.',
50
56
  });
51
57
 
52
- this.executor = new AgentExecutor({
53
- agent,
54
- tools,
55
- });
58
+ // Suppress unused import warning — HumanMessage is used in run()
59
+ void HumanMessage;
56
60
  } catch (error) {
57
- console.error("[LangChainAgent] Failed to initialize. Ensure 'langchain' and '@langchain/core' are installed.");
58
- throw error;
61
+ const isMissing =
62
+ error instanceof Error && error.message.includes('Cannot find module');
63
+ const hint = isMissing
64
+ ? " Make sure 'langchain' and '@langchain/core' are installed:\n npm install langchain @langchain/core"
65
+ : '';
66
+ throw new Error(
67
+ `[LangChainAgent] Failed to initialize.${hint}\n${
68
+ error instanceof Error ? error.message : String(error)
69
+ }`
70
+ );
59
71
  }
60
72
  }
61
73
 
62
74
  /**
63
75
  * Run the agentic flow.
76
+ *
77
+ * LangChain v1.x: invoke takes `{ messages: [...] }` instead of `{ input, chat_history }`.
78
+ * The agent returns `{ messages: [...] }` — the last message is the final answer.
64
79
  */
65
80
  async run(input: string, chatHistory: unknown[] = []) {
66
- if (!this.executor) {
67
- throw new Error("[LangChainAgent] Agent not initialized. Call initialize() first.");
81
+ if (!this.agent) {
82
+ throw new Error('[LangChainAgent] Agent not initialized. Call initialize() first.');
68
83
  }
69
84
 
70
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
- const response = await (this.executor as any).invoke({
72
- input,
73
- chat_history: chatHistory,
85
+ // Dynamically import HumanMessage to construct the message list
86
+ const { HumanMessage, AIMessage } = await import('@langchain/core/messages');
87
+
88
+ const historyMessages = (chatHistory as Array<{ role: string; content: string }>).map((m) =>
89
+ m.role === 'user' ? new HumanMessage(m.content) : new AIMessage(m.content)
90
+ );
91
+
92
+ const response = await this.agent.invoke({
93
+ messages: [...historyMessages, new HumanMessage(input)],
74
94
  });
75
95
 
76
- return response.output;
96
+ // Extract the text from the last message in the response
97
+ const lastMessage = response?.messages?.at(-1);
98
+ if (lastMessage && typeof lastMessage.content === 'string') {
99
+ return lastMessage.content;
100
+ }
101
+ // Fallback for structured content arrays
102
+ if (Array.isArray(lastMessage?.content)) {
103
+ return lastMessage.content
104
+ .filter((c: { type: string }) => c.type === 'text')
105
+ .map((c: { text: string }) => c.text)
106
+ .join('');
107
+ }
108
+
109
+ return String(response?.output ?? response ?? '');
77
110
  }
78
111
  }
@@ -13,6 +13,52 @@ import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
13
13
  import { QueryProcessor } from './QueryProcessor';
14
14
  import { IngestDocument, ChatResponse, VectorMatch, GraphSearchResult, RetrievalResult, UpsertDocument } from '../types';
15
15
 
16
+ // ─── LRU Embedding Cache ───────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * A simple size-capped LRU cache for embedding vectors.
20
+ * Prevents the unbounded `Map` from consuming infinite memory under production load.
21
+ */
22
+ class LRUEmbeddingCache {
23
+ private readonly cache = new Map<string, number[]>();
24
+ private readonly maxSize: number;
25
+
26
+ constructor(maxSize = 500) {
27
+ this.maxSize = maxSize;
28
+ }
29
+
30
+ get(key: string): number[] | undefined {
31
+ const value = this.cache.get(key);
32
+ if (value !== undefined) {
33
+ // Promote to most-recently-used position
34
+ this.cache.delete(key);
35
+ this.cache.set(key, value);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ set(key: string, value: number[]): void {
41
+ if (this.cache.has(key)) {
42
+ this.cache.delete(key);
43
+ } else if (this.cache.size >= this.maxSize) {
44
+ // Evict the oldest (first) entry
45
+ const oldestKey = this.cache.keys().next().value;
46
+ if (oldestKey !== undefined) this.cache.delete(oldestKey);
47
+ }
48
+ this.cache.set(key, value);
49
+ }
50
+
51
+ clear(): void {
52
+ this.cache.clear();
53
+ }
54
+
55
+ get size(): number {
56
+ return this.cache.size;
57
+ }
58
+ }
59
+
60
+ // ─── Pipeline ─────────────────────────────────────────────────────────────────
61
+
16
62
  /**
17
63
  * Pipeline — orchestrates the RAG flow: Embed → Search → Augment → Generate.
18
64
  *
@@ -22,6 +68,7 @@ import { IngestDocument, ChatResponse, VectorMatch, GraphSearchResult, Retrieval
22
68
  * - Smart embedding strategy (integrated / separate / external)
23
69
  * - Error recovery for transient failures
24
70
  * - Multi-tenancy support via namespacing
71
+ * - LRU-bounded embedding cache (max 500 entries, prevents memory leaks)
25
72
  */
26
73
  export class Pipeline {
27
74
  private vectorDB!: BaseVectorProvider;
@@ -33,7 +80,8 @@ export class Pipeline {
33
80
  private entityExtractor?: EntityExtractor;
34
81
  private reranker: Reranker;
35
82
  private agent?: LangChainAgent;
36
- private embeddingCache = new Map<string, number[]>();
83
+ /** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
84
+ private embeddingCache = new LRUEmbeddingCache(500);
37
85
  private initialised = false;
38
86
 
39
87
  constructor(private config: RagConfig) {
@@ -144,6 +192,7 @@ export class Pipeline {
144
192
 
145
193
  /**
146
194
  * Step 2: Generate embeddings for chunks with retry logic.
195
+ * Uses batchEmbed when available for efficiency; falls back to sequential embedding.
147
196
  */
148
197
  private async processEmbeddings(chunks: Chunk[]): Promise<number[][]> {
149
198
  const embedBatchOptions: BatchOptions = {
@@ -158,8 +207,12 @@ export class Pipeline {
158
207
  embedBatchOptions
159
208
  );
160
209
 
210
+ // mapWithRetry filters out undefined on failure; guard against mismatch explicitly
161
211
  if (vectors.length !== chunks.length) {
162
- throw new Error(`Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks`);
212
+ throw new Error(
213
+ `[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. ` +
214
+ `Check embedding provider logs for individual chunk failures.`
215
+ );
163
216
  }
164
217
 
165
218
  return vectors;
@@ -313,12 +366,12 @@ export class Pipeline {
313
366
 
314
367
  /**
315
368
  * Universal retrieval method combining all enabled providers.
369
+ * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
316
370
  */
317
371
  async retrieve(query: string, options: { namespace?: string; topK?: number; filter?: Record<string, unknown> }): Promise<RetrievalResult> {
318
372
  const ns = options.namespace ?? this.config.projectId;
319
373
  const topK = options.topK ?? 5;
320
374
 
321
- // Performance Optimization: Cache query embeddings to avoid redundant LLM calls
322
375
  const cacheKey = `${ns}::${query}`;
323
376
  let queryVector = this.embeddingCache.get(cacheKey);
324
377
 
@@ -354,10 +407,13 @@ New Question: ${question}
354
407
 
355
408
  Optimized Search Query:`;
356
409
 
357
- const rewrite = await this.llmProvider.chat([
358
- { role: 'system', content: 'You are an assistant that optimizes search queries for RAG systems.' },
359
- { role: 'user', content: prompt }
360
- ], '');
410
+ const rewrite = await this.llmProvider.chat(
411
+ [
412
+ { role: 'system', content: 'You are an assistant that optimizes search queries for RAG systems.' },
413
+ { role: 'user', content: prompt }
414
+ ],
415
+ ''
416
+ );
361
417
 
362
418
  return rewrite.trim() || question;
363
419
  }
@@ -1,5 +1,5 @@
1
1
  import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
2
- import { BaseGraphProvider } from '@/providers/graphdb/BaseGraphProvider';
2
+ 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';
@@ -127,7 +127,7 @@ export class ProviderRegistry {
127
127
 
128
128
  switch (provider) {
129
129
  case 'simple': {
130
- const { SimpleGraphProvider } = await import('@/providers/graphdb/SimpleGraphProvider');
130
+ const { SimpleGraphProvider } = await import('../providers/graphdb/SimpleGraphProvider');
131
131
  return new SimpleGraphProvider(config);
132
132
  }
133
133
  default:
@@ -137,7 +137,7 @@ export class QueryProcessor {
137
137
  }
138
138
  }
139
139
 
140
- // 6. Capitalized proper nouns
140
+ // 6. Capitalized proper nouns — added here so buildQueryFilter can reuse them
141
141
  for (const match of question.matchAll(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,3}\b/g)) {
142
142
  addHint(match[0]);
143
143
  }
@@ -147,6 +147,10 @@ export class QueryProcessor {
147
147
 
148
148
  /**
149
149
  * Constructs a QueryFilter object from extracted hints.
150
+ *
151
+ * Note: proper nouns are extracted inside `extractQueryFieldHints` and surfaced
152
+ * as field-less hints; `buildQueryFilter` adds them to `keywords` without
153
+ * re-running the proper noun regex to avoid duplication.
150
154
  */
151
155
  static buildQueryFilter(question: string, hints: QueryFieldHint[]): QueryFilter {
152
156
  const filter: QueryFilter = { metadata: {}, keywords: [], queryText: question };
@@ -155,16 +159,13 @@ export class QueryProcessor {
155
159
  if (hint.field) {
156
160
  filter.metadata![hint.field] = hint.value;
157
161
  } else {
158
- filter.keywords!.push(hint.value);
162
+ // hint.value already includes proper nouns from extractQueryFieldHints
163
+ if (!filter.keywords!.includes(hint.value)) {
164
+ filter.keywords!.push(hint.value);
165
+ }
159
166
  }
160
167
  }
161
168
 
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
169
  if (Object.keys(filter.metadata || {}).length === 0) delete filter.metadata;
169
170
  if (filter.keywords && filter.keywords.length === 0) delete filter.keywords;
170
171
 
@@ -4,11 +4,60 @@ import { RagConfig } from '../config/RagConfig';
4
4
  import { ChatMessage } from '../llm/ILLMProvider';
5
5
  import { DocumentParser } from '../utils/DocumentParser';
6
6
 
7
+ // ─── SSE helpers ──────────────────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Encode a payload as a Server-Sent Events frame.
11
+ * data: <json>\n\n
12
+ */
13
+ function sseFrame(payload: unknown): string {
14
+ return `data: ${JSON.stringify(payload)}\n\n`;
15
+ }
16
+
17
+ /** Encode a plain text chunk as an SSE frame. */
18
+ function sseTextFrame(text: string): string {
19
+ return `data: ${JSON.stringify({ type: 'text', text })}\n\n`;
20
+ }
21
+
22
+ /** Encode the retrieval metadata as an SSE frame. */
23
+ function sseMetaFrame(meta: unknown): string {
24
+ return `data: ${JSON.stringify({ type: 'metadata', ...((meta as object) ?? {}) })}\n\n`;
25
+ }
26
+
27
+ /** Encode a stream error as an SSE frame. */
28
+ function sseErrorFrame(message: string): string {
29
+ return `data: ${JSON.stringify({ type: 'error', error: message })}\n\n`;
30
+ }
31
+
32
+ /** SSE response headers. */
33
+ const SSE_HEADERS: Record<string, string> = {
34
+ 'Content-Type': 'text/event-stream; charset=utf-8',
35
+ 'Cache-Control': 'no-cache, no-transform',
36
+ Connection: 'keep-alive',
37
+ 'X-Accel-Buffering': 'no', // Disable Nginx buffering for streaming
38
+ };
39
+
40
+ // ─── Handler Factories ────────────────────────────────────────────────────────
41
+
7
42
  /**
8
- * createChatHandler — factory that returns a Next.js App Router POST handler
43
+ * createChatHandler — factory that returns a Next.js App Router POST handler.
44
+ *
45
+ * Accepts either a full/partial `RagConfig` **or** a pre-built `VectorPlugin`
46
+ * instance (preferred for singleton/multi-tenant setups).
47
+ *
48
+ * @example
49
+ * // Option A — pass config (plugin created once at module load time)
50
+ * export const POST = createChatHandler(getRagConfig());
51
+ *
52
+ * // Option B — pass a pre-built plugin (most flexible)
53
+ * const plugin = new VectorPlugin(getRagConfig());
54
+ * export const POST = createChatHandler(plugin);
9
55
  */
10
- export function createChatHandler(config?: Partial<RagConfig>) {
11
- const plugin = new VectorPlugin(config);
56
+ export function createChatHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
57
+ const plugin =
58
+ configOrPlugin instanceof VectorPlugin
59
+ ? configOrPlugin
60
+ : new VectorPlugin(configOrPlugin);
12
61
 
13
62
  return async function POST(req: NextRequest) {
14
63
  try {
@@ -33,63 +82,94 @@ export function createChatHandler(config?: Partial<RagConfig>) {
33
82
  }
34
83
 
35
84
  /**
36
- * createStreamHandler — factory that returns a streaming Next.js App Router POST handler
85
+ * createStreamHandler — factory for a streaming SSE Next.js App Router POST handler.
86
+ *
87
+ * Streams the assistant response as Server-Sent Events (SSE). Each frame is a
88
+ * JSON object with a `type` discriminant:
89
+ * - `{ type: "text", text: "..." }` — incremental text chunk
90
+ * - `{ type: "metadata", sources: [...] }` — retrieval metadata (last frame)
91
+ * - `{ type: "error", error: "..." }` — stream-level error
92
+ *
93
+ * @example
94
+ * // src/app/api/chat/route.ts
95
+ * export const POST = createStreamHandler(getRagConfig());
96
+ *
97
+ * // Client-side parsing in useRagChat.ts:
98
+ * // const lines = chunk.split('\n');
99
+ * // for (const line of lines) {
100
+ * // if (line.startsWith('data: ')) {
101
+ * // const frame = JSON.parse(line.slice(6));
102
+ * // if (frame.type === 'text') { ... }
103
+ * // }
104
+ * // }
37
105
  */
38
- export function createStreamHandler(config?: Partial<RagConfig>) {
39
- const plugin = new VectorPlugin(config);
106
+ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
107
+ const plugin =
108
+ configOrPlugin instanceof VectorPlugin
109
+ ? configOrPlugin
110
+ : new VectorPlugin(configOrPlugin);
40
111
 
41
112
  return async function POST(req: NextRequest) {
113
+ // Parse body first so we can return a normal JSON 400 before opening the stream
114
+ let body: { message?: string; history?: ChatMessage[]; namespace?: string };
42
115
  try {
43
- const body = await req.json();
44
- const { message, history = [], namespace } = body;
45
-
46
- const encoder = new TextEncoder();
47
- const stream = new ReadableStream({
48
- async start(controller) {
49
- try {
50
- const pipelineStream = plugin.chatStream(message, history, namespace);
51
-
52
- for await (const chunk of pipelineStream) {
53
- if (typeof chunk === 'string') {
54
- controller.enqueue(encoder.encode(chunk));
55
- } else {
56
- // Yield metadata/sources at the end
57
- controller.enqueue(encoder.encode(`\n\n__METADATA__${JSON.stringify(chunk)}`));
58
- }
59
- }
60
- controller.close();
61
- } catch (streamError) {
62
- console.error('[createStreamHandler] Stream processing error:', streamError);
63
- const errorMessage = streamError instanceof Error ? streamError.message : String(streamError);
64
- controller.enqueue(encoder.encode(`\n\n__ERROR__${JSON.stringify({ error: errorMessage })}`));
65
- controller.close();
66
- }
67
- },
116
+ body = await req.json();
117
+ } catch {
118
+ return new Response(JSON.stringify({ error: 'Invalid JSON body' }), {
119
+ status: 400,
120
+ headers: { 'Content-Type': 'application/json' },
68
121
  });
122
+ }
69
123
 
70
- return new Response(stream, {
71
- headers: {
72
- 'Content-Type': 'text/event-stream',
73
- 'Cache-Control': 'no-cache',
74
- 'Connection': 'keep-alive',
75
- },
76
- });
77
- } catch (err) {
78
- const message = err instanceof Error ? err.message : 'Internal server error';
79
- return new Response(JSON.stringify({ error: message }), {
80
- status: 500,
124
+ const { message, history = [], namespace } = body;
125
+
126
+ if (!message?.trim()) {
127
+ return new Response(JSON.stringify({ error: 'message is required' }), {
128
+ status: 400,
81
129
  headers: { 'Content-Type': 'application/json' },
82
130
  });
83
131
  }
132
+
133
+ const encoder = new TextEncoder();
134
+
135
+ const stream = new ReadableStream({
136
+ async start(controller) {
137
+ const enqueue = (text: string) => controller.enqueue(encoder.encode(text));
138
+
139
+ try {
140
+ const pipelineStream = plugin.chatStream(message, history, namespace);
141
+
142
+ for await (const chunk of pipelineStream) {
143
+ if (typeof chunk === 'string') {
144
+ enqueue(sseTextFrame(chunk));
145
+ } else {
146
+ // Retrieval metadata object — always the final frame
147
+ enqueue(sseMetaFrame(chunk));
148
+ }
149
+ }
150
+ } catch (streamError) {
151
+ const errorMessage =
152
+ streamError instanceof Error ? streamError.message : String(streamError);
153
+ console.error('[createStreamHandler] Stream error:', streamError);
154
+ enqueue(sseErrorFrame(errorMessage));
155
+ } finally {
156
+ controller.close();
157
+ }
158
+ },
159
+ });
160
+
161
+ return new Response(stream, { headers: SSE_HEADERS });
84
162
  };
85
163
  }
86
164
 
87
-
88
165
  /**
89
166
  * createIngestHandler — factory for the document ingestion endpoint.
90
167
  */
91
- export function createIngestHandler(config?: Partial<RagConfig>) {
92
- const plugin = new VectorPlugin(config);
168
+ export function createIngestHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
169
+ const plugin =
170
+ configOrPlugin instanceof VectorPlugin
171
+ ? configOrPlugin
172
+ : new VectorPlugin(configOrPlugin);
93
173
 
94
174
  return async function POST(req: NextRequest) {
95
175
  try {
@@ -115,8 +195,11 @@ export function createIngestHandler(config?: Partial<RagConfig>) {
115
195
  /**
116
196
  * createHealthHandler — factory for the health-check endpoint.
117
197
  */
118
- export function createHealthHandler(config?: Partial<RagConfig>) {
119
- const plugin = new VectorPlugin(config);
198
+ export function createHealthHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
199
+ const plugin =
200
+ configOrPlugin instanceof VectorPlugin
201
+ ? configOrPlugin
202
+ : new VectorPlugin(configOrPlugin);
120
203
 
121
204
  return async function GET() {
122
205
  try {
@@ -137,8 +220,11 @@ export function createHealthHandler(config?: Partial<RagConfig>) {
137
220
  /**
138
221
  * createUploadHandler — factory for the file upload ingestion endpoint.
139
222
  */
140
- export function createUploadHandler(config?: Partial<RagConfig>) {
141
- const plugin = new VectorPlugin(config);
223
+ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
224
+ const plugin =
225
+ configOrPlugin instanceof VectorPlugin
226
+ ? configOrPlugin
227
+ : new VectorPlugin(configOrPlugin);
142
228
 
143
229
  return async function POST(req: NextRequest) {
144
230
  try {
@@ -174,3 +260,6 @@ export function createUploadHandler(config?: Partial<RagConfig>) {
174
260
  }
175
261
  };
176
262
  }
263
+
264
+ // Re-export SSE helper so host apps can use it in custom handlers
265
+ export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame };