@retrivora-ai/rag-engine 1.7.9 → 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.
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
- package/dist/{index-wCRqMtdX.d.mts → ILLMProvider-BfRgI1Xh.d.mts} +59 -1
- package/dist/{index-wCRqMtdX.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +59 -1
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
- package/dist/{chunk-UHGYLTTY.mjs → chunk-BFYLQYQU.mjs} +846 -458
- package/dist/chunk-R3RGUMHE.mjs +218 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1033 -622
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-BDqz2_Yu.d.ts → index-1Z4GuYBi.d.ts} +7 -1
- package/dist/{index-DhsG2o5q.d.mts → index-BV0z5mb6.d.mts} +7 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +507 -352
- package/dist/index.mjs +427 -253
- package/dist/server.d.mts +35 -5
- package/dist/server.d.ts +35 -5
- package/dist/server.js +1183 -795
- package/dist/server.mjs +163 -176
- package/package.json +4 -2
- package/src/app/page.tsx +1 -2
- package/src/components/DynamicChart.tsx +10 -35
- package/src/components/MessageBubble.tsx +223 -74
- package/src/components/ProductCard.tsx +2 -2
- package/src/components/VisualizationRenderer.tsx +347 -247
- package/src/config/RagConfig.ts +5 -0
- package/src/config/serverConfig.ts +3 -1
- package/src/core/Pipeline.ts +70 -209
- package/src/core/ProviderRegistry.ts +2 -2
- package/src/core/VectorPlugin.ts +9 -0
- package/src/handlers/index.ts +23 -7
- package/src/hooks/useRagChat.ts +2 -2
- package/src/llm/LLMFactory.ts +54 -2
- package/src/llm/providers/AnthropicProvider.ts +12 -8
- package/src/llm/providers/GeminiProvider.ts +188 -143
- package/src/llm/providers/OllamaProvider.ts +7 -3
- package/src/llm/providers/OpenAIProvider.ts +12 -8
- package/src/types/chat.ts +6 -0
- package/src/types/index.ts +81 -0
- package/src/utils/SchemaMapper.ts +129 -0
- package/src/utils/UITransformer.ts +524 -194
- package/dist/DocumentChunker-Bmscbh-X.d.ts +0 -93
- package/dist/DocumentChunker-DCuxrOdM.d.mts +0 -93
- package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
- 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 {
|
|
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:
|
|
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)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.llmConfig = {
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
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({
|
|
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)
|
|
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 {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
201
|
+
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
202
|
+
const model = this.client.getGenerativeModel({
|
|
121
203
|
model: this.llmConfig.model,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
245
|
+
const content = applyPrefix(
|
|
246
|
+
text,
|
|
247
|
+
options?.taskType,
|
|
248
|
+
this.embeddingConfig?.queryPrefix,
|
|
249
|
+
this.embeddingConfig?.documentPrefix,
|
|
180
250
|
);
|
|
181
251
|
|
|
182
|
-
const
|
|
183
|
-
const
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
262
|
+
return response.embedding.values;
|
|
213
263
|
}
|
|
214
264
|
|
|
215
265
|
async batchEmbed(texts: string[], options?: EmbedOptions): Promise<number[][]> {
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
-
|
|
257
|
-
|
|
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
|
|
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:
|
|
90
|
-
?
|
|
91
|
-
:
|
|
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
|
|
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:
|
|
121
|
-
?
|
|
122
|
-
:
|
|
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 {
|
package/src/types/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface ChatResponse {
|
|
|
22
22
|
reply: string;
|
|
23
23
|
sources: VectorMatch[];
|
|
24
24
|
graphData?: GraphSearchResult;
|
|
25
|
+
ui_transformation?: unknown;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface GraphNode {
|
|
@@ -65,5 +66,85 @@ export interface Product {
|
|
|
65
66
|
description?: string;
|
|
66
67
|
}
|
|
67
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
|
+
|
|
68
149
|
export * from './props';
|
|
69
150
|
export * from './chat';
|