@retrivora-ai/rag-engine 1.8.0 → 1.8.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 (46) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BfRgI1Xh.d.mts} +58 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +58 -1
  5. package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
  6. package/dist/{chunk-PV3MFHWU.mjs → chunk-BFYLQYQU.mjs} +808 -439
  7. package/dist/chunk-R3RGUMHE.mjs +218 -0
  8. package/dist/handlers/index.d.mts +2 -2
  9. package/dist/handlers/index.d.ts +2 -2
  10. package/dist/handlers/index.js +995 -603
  11. package/dist/handlers/index.mjs +1 -1
  12. package/dist/{index-Bb2yEopi.d.mts → index-1Z4GuYBi.d.ts} +7 -1
  13. package/dist/{index-CkbTzj9J.d.ts → index-BV0z5mb6.d.mts} +7 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +491 -316
  17. package/dist/index.mjs +411 -217
  18. package/dist/server.d.mts +35 -5
  19. package/dist/server.d.ts +35 -5
  20. package/dist/server.js +1146 -777
  21. package/dist/server.mjs +163 -176
  22. package/package.json +4 -2
  23. package/src/app/page.tsx +1 -2
  24. package/src/components/MessageBubble.tsx +211 -69
  25. package/src/components/ProductCard.tsx +2 -2
  26. package/src/components/VisualizationRenderer.tsx +347 -247
  27. package/src/config/RagConfig.ts +5 -0
  28. package/src/config/serverConfig.ts +3 -1
  29. package/src/core/Pipeline.ts +65 -206
  30. package/src/core/ProviderRegistry.ts +2 -2
  31. package/src/core/VectorPlugin.ts +9 -0
  32. package/src/handlers/index.ts +23 -7
  33. package/src/hooks/useRagChat.ts +2 -2
  34. package/src/llm/LLMFactory.ts +54 -2
  35. package/src/llm/providers/AnthropicProvider.ts +12 -8
  36. package/src/llm/providers/GeminiProvider.ts +188 -143
  37. package/src/llm/providers/OllamaProvider.ts +7 -3
  38. package/src/llm/providers/OpenAIProvider.ts +12 -8
  39. package/src/types/chat.ts +6 -0
  40. package/src/types/index.ts +80 -0
  41. package/src/utils/SchemaMapper.ts +129 -0
  42. package/src/utils/UITransformer.ts +491 -181
  43. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  44. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  45. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  46. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -14,53 +14,103 @@
14
14
  * - model: string – e.g. "text-embedding-004"
15
15
  */
16
16
 
17
- import { GoogleGenAI } from '@google/genai';
17
+ import { GoogleGenerativeAI } from '@google/generative-ai';
18
18
  import { ILLMProvider, EmbedOptions } from '../ILLMProvider';
19
19
  import { ChatMessage, ChatOptions } from '../../types';
20
20
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
21
21
  import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
22
22
  import { ValidationError } from '../../core/ConfigValidator';
23
23
 
24
+ const DEFAULT_EMBEDDING_MODEL = 'text-embedding-004';
25
+ const DEFAULT_TEMPERATURE = 0.7;
26
+ const DEFAULT_MAX_TOKENS = 1024;
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Internal helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /** Strip suffixes like `:latest` that are valid in Ollama but not in Google SDK. */
33
+ function sanitizeModel(model: string): string {
34
+ return model ? model.split(':')[0] : model;
35
+ }
36
+
37
+ /** Build a GoogleGenerativeAI client for the given API key. */
38
+ function buildClient(apiKey: string): GoogleGenerativeAI {
39
+ return new GoogleGenerativeAI(apiKey);
40
+ }
41
+
42
+ /**
43
+ * Apply a task-specific prefix to a text string when configured to do so.
44
+ * Returns the original string unchanged when no prefix applies.
45
+ */
46
+ function applyPrefix(
47
+ text: string,
48
+ taskType: EmbedOptions['taskType'] | undefined,
49
+ queryPrefix: string | undefined,
50
+ docPrefix: string | undefined,
51
+ ): string {
52
+ if (taskType === 'query' && queryPrefix && !text.startsWith(queryPrefix)) {
53
+ return `${queryPrefix}${text}`;
54
+ }
55
+ if (taskType === 'document' && docPrefix && !text.startsWith(docPrefix)) {
56
+ return `${docPrefix}${text}`;
57
+ }
58
+ return text;
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Provider
63
+ // ---------------------------------------------------------------------------
64
+
24
65
  export class GeminiProvider implements ILLMProvider {
25
- private readonly client: GoogleGenAI;
66
+ private readonly client: GoogleGenerativeAI;
26
67
  private readonly llmConfig: LLMConfig;
27
68
  private readonly embeddingConfig?: EmbeddingConfig;
28
69
 
29
70
  constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
30
- if (!llmConfig.apiKey) throw new Error('[GeminiProvider] llmConfig.apiKey is required');
31
- this.client = new GoogleGenAI({ apiKey: llmConfig.apiKey });
32
-
33
- // Sanitize model names (e.g. remove ':latest' suffix used by Ollama)
34
- this.llmConfig = {
35
- ...llmConfig,
36
- model: this.sanitizeModel(llmConfig.model)
37
- };
38
-
71
+ if (!llmConfig.apiKey) {
72
+ throw new Error('[GeminiProvider] llmConfig.apiKey is required');
73
+ }
74
+
75
+ this.llmConfig = { ...llmConfig, model: sanitizeModel(llmConfig.model) };
76
+ this.client = buildClient(this.llmConfig.apiKey!);
77
+
39
78
  if (embeddingConfig) {
40
79
  this.embeddingConfig = {
41
80
  ...embeddingConfig,
42
- model: this.sanitizeModel(embeddingConfig.model)
81
+ model: sanitizeModel(embeddingConfig.model),
43
82
  };
44
83
  }
45
84
  }
46
85
 
86
+ // -------------------------------------------------------------------------
87
+ // Static factory helpers
88
+ // -------------------------------------------------------------------------
89
+
47
90
  static getValidator(): IProviderValidator {
48
91
  return {
49
92
  validate(config: Record<string, unknown>): ValidationError[] {
50
93
  const errors: ValidationError[] = [];
94
+
51
95
  if (!config.apiKey && !process.env.GOOGLE_GENAI_API_KEY) {
52
96
  errors.push({
53
97
  field: 'llm.apiKey',
54
98
  message: 'Gemini API key is required',
55
99
  suggestion: 'Set GOOGLE_GENAI_API_KEY environment variable or provide in config',
56
- severity: 'error'
100
+ severity: 'error',
57
101
  });
58
102
  }
103
+
59
104
  if (!config.model) {
60
- errors.push({ field: 'llm.model', message: 'Gemini model name is required', severity: 'error' });
105
+ errors.push({
106
+ field: 'llm.model',
107
+ message: 'Gemini model name is required',
108
+ severity: 'error',
109
+ });
61
110
  }
111
+
62
112
  return errors;
63
- }
113
+ },
64
114
  };
65
115
  }
66
116
 
@@ -68,13 +118,17 @@ export class GeminiProvider implements ILLMProvider {
68
118
  return {
69
119
  async check(config: Record<string, unknown>): Promise<HealthCheckResult> {
70
120
  const timestamp = Date.now();
71
- const apiKey = (config.apiKey as string) || process.env.GOOGLE_GENAI_API_KEY || '';
72
- const modelName = config.model as string;
121
+ const apiKey = (config.apiKey as string | undefined) ?? process.env.GOOGLE_GENAI_API_KEY ?? '';
122
+ const modelName = sanitizeModel(config.model as string);
123
+
73
124
  try {
74
- const { GoogleGenAI } = await import('@google/genai');
75
- const genAI = new GoogleGenAI({ apiKey });
76
- // Verify model availability
77
- await genAI.models.get({ model: modelName });
125
+ const { GoogleGenerativeAI } = await import('@google/generative-ai');
126
+ const ai = new GoogleGenerativeAI(apiKey);
127
+
128
+ // Lightweight verification: attempt a minimal embed call
129
+ const model = ai.getGenerativeModel({ model: DEFAULT_EMBEDDING_MODEL });
130
+ await model.embedContent('health-check');
131
+
78
132
  return {
79
133
  healthy: true,
80
134
  provider: 'gemini',
@@ -89,179 +143,170 @@ export class GeminiProvider implements ILLMProvider {
89
143
  timestamp,
90
144
  };
91
145
  }
92
- }
146
+ },
93
147
  };
94
148
  }
95
149
 
96
- private sanitizeModel(model: string): string {
97
- if (!model) return model;
98
- // Strip :latest suffix common in Ollama but invalid in Google SDK
99
- return model.split(':')[0];
150
+ // -------------------------------------------------------------------------
151
+ // Private utilities
152
+ // -------------------------------------------------------------------------
153
+
154
+ /** Resolve the embedding client — uses a separate client when the embedding
155
+ * API key differs from the LLM API key. */
156
+ private get embeddingClient(): GoogleGenerativeAI {
157
+ if (this.embeddingConfig?.apiKey && this.embeddingConfig.apiKey !== this.llmConfig.apiKey) {
158
+ return buildClient(this.embeddingConfig.apiKey);
159
+ }
160
+ return this.client;
100
161
  }
101
162
 
163
+ /** Resolve the embedding model to use, in order of specificity. */
164
+ private resolveEmbeddingModel(optionsModel?: string): string {
165
+ return sanitizeModel(optionsModel ?? this.embeddingConfig?.model ?? DEFAULT_EMBEDDING_MODEL);
166
+ }
102
167
 
103
- async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
104
- const systemPrompt =
168
+ /**
169
+ * Build the system instruction string, inserting the RAG context either via
170
+ * the `{{context}}` placeholder or by appending it.
171
+ */
172
+ private buildSystemInstruction(context: string): string {
173
+ const base =
105
174
  this.llmConfig.systemPrompt ??
106
175
  `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
107
176
 
108
- const systemInstruction = systemPrompt.includes('{{context}}')
109
- ? systemPrompt.replace('{{context}}', context)
110
- : `${systemPrompt}\n\nContext:\n${context}`;
177
+ return base.includes('{{context}}') ? base.replace('{{context}}', context) : `${base}\n\nContext:\n${context}`;
178
+ }
179
+
180
+ /**
181
+ * Convert ChatMessage[] to the Gemini contents format.
182
+ *
183
+ * Rules enforced by the Gemini API:
184
+ * - Only `user` and `model` roles are allowed in `contents`.
185
+ * - System messages must be passed via `systemInstruction`, not here.
186
+ * - Messages must strictly alternate between `user` and `model`.
187
+ */
188
+ private buildGeminiContents(messages: ChatMessage[]): Array<{ role: 'user' | 'model'; parts: [{ text: string }] }> {
189
+ return messages
190
+ .filter((m) => m.role !== 'system')
191
+ .map((m) => ({
192
+ role: m.role === 'assistant' ? ('model' as const) : ('user' as const),
193
+ parts: [{ text: m.content }] as [{ text: string }],
194
+ }));
195
+ }
111
196
 
112
- // Gemini requires alternating user/model messages, starting with user.
113
- // If the first message is not user, or there are consecutive same-role messages, we need to adapt.
114
- // To simplify and comply with typical usage, we'll map 'assistant' to 'model'.
115
- const geminiMessages = messages.map((m) => ({
116
- role: m.role === 'assistant' ? 'model' : 'user',
117
- parts: [{ text: m.content }],
118
- }));
197
+ // -------------------------------------------------------------------------
198
+ // ILLMProvider chat
199
+ // -------------------------------------------------------------------------
119
200
 
120
- const response = await this.client.models.generateContent({
201
+ async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
202
+ const model = this.client.getGenerativeModel({
121
203
  model: this.llmConfig.model,
122
- contents: geminiMessages,
123
- config: {
124
- systemInstruction,
125
- temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
126
- maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
204
+ systemInstruction: this.buildSystemInstruction(context),
205
+ });
206
+
207
+ const result = await model.generateContent({
208
+ contents: this.buildGeminiContents(messages),
209
+ generationConfig: {
210
+ temperature: options?.temperature ?? this.llmConfig.temperature ?? DEFAULT_TEMPERATURE,
211
+ maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? DEFAULT_MAX_TOKENS,
127
212
  stopSequences: options?.stop,
128
213
  },
129
214
  });
130
215
 
131
- const text = typeof (response as { text: unknown }).text === 'function'
132
- ? (response as { text: () => string }).text()
133
- : (response.text ?? '');
134
- return text;
216
+ return result.response.text();
135
217
  }
136
218
 
137
219
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
138
- const systemPrompt =
139
- this.llmConfig.systemPrompt ??
140
- `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
141
-
142
- const systemInstruction = systemPrompt.includes('{{context}}')
143
- ? systemPrompt.replace('{{context}}', context)
144
- : `${systemPrompt}\n\nContext:\n${context}`;
145
-
146
- const geminiMessages = messages.map((m) => ({
147
- role: m.role === 'assistant' ? 'model' : 'user',
148
- parts: [{ text: m.content }],
149
- }));
150
-
151
- const result = await this.client.models.generateContentStream({
220
+ const model = this.client.getGenerativeModel({
152
221
  model: this.llmConfig.model,
153
- contents: geminiMessages,
154
- config: {
155
- systemInstruction,
156
- temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
157
- maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
222
+ systemInstruction: this.buildSystemInstruction(context),
223
+ });
224
+
225
+ const result = await model.generateContentStream({
226
+ contents: this.buildGeminiContents(messages),
227
+ generationConfig: {
228
+ temperature: options?.temperature ?? this.llmConfig.temperature ?? DEFAULT_TEMPERATURE,
229
+ maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? DEFAULT_MAX_TOKENS,
158
230
  stopSequences: options?.stop,
159
231
  },
160
232
  });
161
233
 
162
- const stream = ((result as unknown as Record<string, unknown>)?.stream ?? result) as AsyncIterable<{ text?: string }>;
163
- if (!stream) {
164
- throw new Error('[GeminiProvider] generateContentStream returned undefined');
165
- }
166
-
167
- for await (const chunk of stream) {
168
- const text = typeof (chunk as { text: unknown }).text === 'function'
169
- ? (chunk as { text: () => string }).text()
170
- : (chunk as { text?: string }).text;
234
+ for await (const chunk of result.stream) {
235
+ const text = chunk.text();
171
236
  if (text) yield text;
172
237
  }
173
238
  }
174
239
 
240
+ // -------------------------------------------------------------------------
241
+ // ILLMProvider — embeddings
242
+ // -------------------------------------------------------------------------
243
+
175
244
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
176
- const model = this.sanitizeModel(
177
- options?.model ??
178
- this.embeddingConfig?.model ??
179
- 'text-embedding-004'
245
+ const content = applyPrefix(
246
+ text,
247
+ options?.taskType,
248
+ this.embeddingConfig?.queryPrefix,
249
+ this.embeddingConfig?.documentPrefix,
180
250
  );
181
251
 
182
- const apiKey = this.embeddingConfig?.apiKey ?? this.llmConfig.apiKey;
183
- const client = apiKey !== this.llmConfig.apiKey
184
- ? new GoogleGenAI({ apiKey })
185
- : this.client;
186
-
187
- let content = text;
188
-
189
- // Dynamically apply prefixes from configuration if they exist
190
- const queryPrefix = this.embeddingConfig?.queryPrefix;
191
- const docPrefix = this.embeddingConfig?.documentPrefix;
192
-
193
- if (options?.taskType === 'query' && queryPrefix) {
194
- if (!content.startsWith(queryPrefix)) {
195
- content = `${queryPrefix}${text}`;
196
- }
197
- } else if (options?.taskType === 'document' && docPrefix) {
198
- if (!content.startsWith(docPrefix)) {
199
- content = `${docPrefix}${text}`;
200
- }
201
- }
252
+ const modelName = this.resolveEmbeddingModel(options?.model);
253
+ const model = this.embeddingClient.getGenerativeModel({ model: modelName });
202
254
 
203
- const response = await client.models.embedContent({
204
- model,
205
- contents: content,
206
- config: this.embeddingConfig?.dimensions ? { outputDimensionality: this.embeddingConfig.dimensions } : undefined
207
- }).catch(err => {
208
- console.error(`[GeminiProvider] Embedding failed for model "${model}":`, err.message);
255
+ const response = await model.embedContent({
256
+ content: { role: 'user', parts: [{ text: content }] },
257
+ }).catch((err: Error) => {
258
+ console.error(`[GeminiProvider] Embedding failed for model "${modelName}":`, err.message);
209
259
  throw err;
210
260
  });
211
261
 
212
- return response.embeddings?.[0]?.values ?? [];
262
+ return response.embedding.values;
213
263
  }
214
264
 
215
265
  async batchEmbed(texts: string[], options?: EmbedOptions): Promise<number[][]> {
216
- // Sequential fallback for Gemini (API doesn't have a direct batch endpoint in the same way, though you can pass multiple contents.
217
- // For simplicity and to match the other providers' structure, we'll iterate or pass array.
218
- // The new SDK supports an array of contents.
219
- const model = this.sanitizeModel(
220
- options?.model ??
221
- this.embeddingConfig?.model ??
222
- 'text-embedding-004'
223
- );
266
+ const modelName = this.resolveEmbeddingModel(options?.model);
267
+ const model = this.embeddingClient.getGenerativeModel({ model: modelName });
224
268
 
225
- const apiKey = this.embeddingConfig?.apiKey ?? this.llmConfig.apiKey;
226
- const client = apiKey !== this.llmConfig.apiKey
227
- ? new GoogleGenAI({ apiKey })
228
- : this.client;
229
-
230
- let contents = texts;
231
-
232
- // Apply prefixes if needed
233
- const queryPrefix = this.embeddingConfig?.queryPrefix;
234
- const docPrefix = this.embeddingConfig?.documentPrefix;
235
-
236
- if (options?.taskType === 'query' && queryPrefix) {
237
- contents = texts.map(text => text.startsWith(queryPrefix) ? text : `${queryPrefix}${text}`);
238
- } else if (options?.taskType === 'document' && docPrefix) {
239
- contents = texts.map(text => text.startsWith(docPrefix) ? text : `${docPrefix}${text}`);
240
- }
269
+ const requests = texts.map((text) => ({
270
+ content: {
271
+ role: 'user',
272
+ parts: [{
273
+ text: applyPrefix(
274
+ text,
275
+ options?.taskType,
276
+ this.embeddingConfig?.queryPrefix,
277
+ this.embeddingConfig?.documentPrefix,
278
+ )
279
+ }]
280
+ },
281
+ }));
241
282
 
242
- const response = await client.models.embedContent({
243
- model,
244
- contents,
245
- config: this.embeddingConfig?.dimensions ? { outputDimensionality: this.embeddingConfig.dimensions } : undefined
246
- }).catch(err => {
247
- console.error(`[GeminiProvider] Batch embedding failed for model "${model}":`, err.message);
283
+ const response = await model.batchEmbedContents({
284
+ requests: requests.map(r => ({
285
+ model: `models/${modelName}`,
286
+ content: r.content,
287
+ }))
288
+ }).catch((err: Error) => {
289
+ console.error(`[GeminiProvider] Batch embedding failed for model "${modelName}":`, err.message);
248
290
  throw err;
249
291
  });
250
292
 
251
- return response.embeddings?.map(e => e.values ?? []) ?? [];
293
+ return response.embeddings.map((e) => e.values ?? []);
252
294
  }
253
295
 
296
+ // -------------------------------------------------------------------------
297
+ // ILLMProvider — health
298
+ // -------------------------------------------------------------------------
299
+
254
300
  async ping(): Promise<boolean> {
255
301
  try {
256
- // Perform a lightweight embed call to verify connectivity
257
- await this.client.models.embedContent({
258
- model: 'text-embedding-004',
259
- contents: 'ping',
302
+ const model = this.embeddingClient.getGenerativeModel({
303
+ model: this.resolveEmbeddingModel(),
260
304
  });
305
+ await model.embedContent('ping');
261
306
  return true;
262
307
  } catch (err) {
263
308
  console.error('[GeminiProvider] Ping failed:', err);
264
309
  return false;
265
310
  }
266
311
  }
267
- }
312
+ }
@@ -85,7 +85,7 @@ export class OllamaProvider implements ILLMProvider {
85
85
  }
86
86
 
87
87
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
88
- const system = this.buildSystemPrompt(context);
88
+ const system = this.buildSystemPrompt(context, options?.systemPrompt);
89
89
 
90
90
  const { data } = await this.http.post<OllamaChatResponse>('/api/chat', {
91
91
  model: this.llmConfig.model,
@@ -104,7 +104,7 @@ export class OllamaProvider implements ILLMProvider {
104
104
  }
105
105
 
106
106
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
107
- const system = this.buildSystemPrompt(context);
107
+ const system = this.buildSystemPrompt(context, options?.systemPrompt);
108
108
 
109
109
  const response = await this.http.post('/api/chat', {
110
110
  model: this.llmConfig.model,
@@ -159,7 +159,11 @@ export class OllamaProvider implements ILLMProvider {
159
159
  }
160
160
  }
161
161
 
162
- private buildSystemPrompt(context: string): string {
162
+ private buildSystemPrompt(context: string, overridePrompt?: string): string {
163
+ if (overridePrompt) {
164
+ // Per-call override: use as-is (context is already embedded in user message)
165
+ return overridePrompt;
166
+ }
163
167
  const systemPrompt =
164
168
  this.llmConfig.systemPrompt ??
165
169
  `You are a helpful assistant. Use the provided context to answer the user's question.\n\nContext:\n${context}`;
@@ -80,15 +80,17 @@ export class OpenAIProvider implements ILLMProvider {
80
80
  }
81
81
 
82
82
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
83
- const systemContent =
83
+ const resolvedSystemPrompt = options?.systemPrompt ??
84
84
  this.llmConfig.systemPrompt ??
85
85
  `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
86
86
 
87
87
  const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
88
88
  role: 'system',
89
- content: systemContent.includes('{{context}}')
90
- ? systemContent.replace('{{context}}', context)
91
- : `${systemContent}\n\nContext:\n${context}`,
89
+ content: resolvedSystemPrompt.includes('{{context}}')
90
+ ? resolvedSystemPrompt.replace('{{context}}', context)
91
+ : options?.systemPrompt
92
+ ? resolvedSystemPrompt // per-call override: use as-is, context already in user message
93
+ : `${resolvedSystemPrompt}\n\nContext:\n${context}`,
92
94
  };
93
95
 
94
96
  const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -111,15 +113,17 @@ export class OpenAIProvider implements ILLMProvider {
111
113
  }
112
114
 
113
115
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
114
- const systemContent =
116
+ const resolvedSystemPrompt = options?.systemPrompt ??
115
117
  this.llmConfig.systemPrompt ??
116
118
  `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
117
119
 
118
120
  const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
119
121
  role: 'system',
120
- content: systemContent.includes('{{context}}')
121
- ? systemContent.replace('{{context}}', context)
122
- : `${systemContent}\n\nContext:\n${context}`,
122
+ content: resolvedSystemPrompt.includes('{{context}}')
123
+ ? resolvedSystemPrompt.replace('{{context}}', context)
124
+ : options?.systemPrompt
125
+ ? resolvedSystemPrompt
126
+ : `${resolvedSystemPrompt}\n\nContext:\n${context}`,
123
127
  };
124
128
 
125
129
  const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
package/src/types/chat.ts CHANGED
@@ -21,6 +21,12 @@ export interface ChatOptions {
21
21
  temperature?: number;
22
22
  /** Stop sequences */
23
23
  stop?: string[];
24
+ /**
25
+ * Per-call system prompt override. When provided, providers should use this
26
+ * instead of (or in addition to) their configured llmConfig.systemPrompt.
27
+ * Useful for one-off analytical calls (e.g. UITransformer.analyzeAndDecide).
28
+ */
29
+ systemPrompt?: string;
24
30
  }
25
31
 
26
32
  export interface UseRagChatOptions {
@@ -66,5 +66,85 @@ export interface Product {
66
66
  description?: string;
67
67
  }
68
68
 
69
+ /**
70
+ * Supported UI visualization types
71
+ */
72
+ export type VisualizationType =
73
+ | 'pie_chart'
74
+ | 'bar_chart'
75
+ | 'line_chart'
76
+ | 'radar_chart'
77
+ | 'table'
78
+ | 'product_carousel'
79
+ | 'carousel'
80
+ | 'text';
81
+
82
+ /**
83
+ * Base structure for all UI transformation responses
84
+ */
85
+ export interface UITransformationResponse {
86
+ type: VisualizationType;
87
+ title: string;
88
+ description?: string;
89
+ data: unknown;
90
+ }
91
+
92
+ /**
93
+ * Pie chart data structure
94
+ */
95
+ export interface PieChartData {
96
+ label: string;
97
+ value: number;
98
+ inStockCount?: number;
99
+ outOfStockCount?: number;
100
+ [key: string]: unknown;
101
+ }
102
+
103
+ /**
104
+ * Bar chart data structure
105
+ */
106
+ export interface BarChartData {
107
+ category: string;
108
+ value: number;
109
+ inStockCount?: number;
110
+ outOfStockCount?: number;
111
+ [key: string]: unknown;
112
+ }
113
+
114
+ /**
115
+ * Line chart data point
116
+ */
117
+ export interface LineChartDataPoint {
118
+ timestamp: string | number;
119
+ value: number;
120
+ label?: string;
121
+ [key: string]: unknown;
122
+ }
123
+
124
+ /**
125
+ * Table representation
126
+ */
127
+ export interface TableData {
128
+ columns: string[];
129
+ rows: (string | number | boolean)[][];
130
+ }
131
+
132
+
133
+ /**
134
+ * Product carousel item
135
+ */
136
+ export interface CarouselProduct {
137
+ id: string | number;
138
+ name: string;
139
+ price?: number | string;
140
+ image?: string;
141
+ inStock?: boolean;
142
+ brand?: string;
143
+ description?: string;
144
+ [key: string]: unknown;
145
+ }
146
+
147
+
148
+
69
149
  export * from './props';
70
150
  export * from './chat';