@retrivora-ai/rag-engine 0.3.1 → 0.3.3

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/server.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { a as RagConfig, C as ChatResponse, I as IngestDocument, c as VectorDBConfig, b as UpsertDocument, V as VectorMatch, L as LLMConfig, E as EmbeddingConfig, d as VectorDBProvider, e as LLMProvider, f as EmbeddingProvider } from './RagConfig-D3Inaf9N.js';
2
- export { R as RAGConfig, U as UIConfig } from './RagConfig-D3Inaf9N.js';
1
+ import { a as RagConfig, C as ChatResponse, I as IngestDocument, c as VectorDBConfig, b as UpsertDocument, V as VectorMatch, L as LLMConfig, E as EmbeddingConfig, d as VectorDBProvider, e as LLMProvider, f as EmbeddingProvider } from './RagConfig-Bpp39-um.js';
2
+ export { R as RAGConfig, U as UIConfig } from './RagConfig-Bpp39-um.js';
3
3
  import { C as ChatMessage, I as ILLMProvider, a as ChatOptions, E as EmbedOptions } from './DocumentChunker-BICIjSuG.js';
4
4
  export { b as Chunk, c as ChunkOptions, D as DocumentChunker } from './DocumentChunker-BICIjSuG.js';
5
- import { H as HealthCheckResult } from './index-BhNJQ2SS.js';
6
- export { P as ProviderHealthCheck, c as createChatHandler, a as createHealthHandler, b as createIngestHandler, d as createUploadHandler } from './index-BhNJQ2SS.js';
5
+ import { H as HealthCheckResult } from './index-DSaQwkKA.js';
6
+ export { P as ProviderHealthCheck, c as createChatHandler, a as createHealthHandler, b as createIngestHandler, d as createUploadHandler } from './index-DSaQwkKA.js';
7
7
  import 'next/server';
8
8
 
9
9
  /**
package/dist/server.js CHANGED
@@ -1223,7 +1223,7 @@ module.exports = __toCommonJS(server_exports);
1223
1223
  // src/core/ConfigResolver.ts
1224
1224
  init_templateUtils();
1225
1225
 
1226
- // src/config/serverConfig.ts
1226
+ // src/config/constants.ts
1227
1227
  var VECTOR_DB_PROVIDERS = [
1228
1228
  "pinecone",
1229
1229
  "pgvector",
@@ -1238,8 +1238,34 @@ var VECTOR_DB_PROVIDERS = [
1238
1238
  "universal_rest",
1239
1239
  "custom"
1240
1240
  ];
1241
- var LLM_PROVIDERS = ["openai", "anthropic", "ollama", "rest", "universal_rest", "custom"];
1242
- var EMBEDDING_PROVIDERS = ["openai", "ollama", "rest", "universal_rest", "custom"];
1241
+ var LLM_PROVIDERS = [
1242
+ "openai",
1243
+ "anthropic",
1244
+ "ollama",
1245
+ "gemini",
1246
+ "rest",
1247
+ "universal_rest",
1248
+ "custom"
1249
+ ];
1250
+ var EMBEDDING_PROVIDERS = [
1251
+ "openai",
1252
+ "ollama",
1253
+ "gemini",
1254
+ "rest",
1255
+ "universal_rest",
1256
+ "custom"
1257
+ ];
1258
+ var PROVIDERS_WITH_EMBEDDINGS = [
1259
+ "openai",
1260
+ "ollama",
1261
+ "gemini",
1262
+ "rest",
1263
+ "universal_rest"
1264
+ ];
1265
+ var UI_VISUAL_STYLES = ["glass", "solid"];
1266
+ var UI_BORDER_RADIUS_OPTIONS = ["none", "sm", "md", "lg", "xl", "full"];
1267
+
1268
+ // src/config/serverConfig.ts
1243
1269
  function readString(env, name) {
1244
1270
  var _a;
1245
1271
  const value = (_a = env[name]) == null ? void 0 : _a.trim();
@@ -1306,6 +1332,7 @@ function getRagConfig(env = process.env) {
1306
1332
  const llmApiKeyByProvider = {
1307
1333
  openai: readString(env, "OPENAI_API_KEY"),
1308
1334
  anthropic: readString(env, "ANTHROPIC_API_KEY"),
1335
+ gemini: readString(env, "GEMINI_API_KEY"),
1309
1336
  ollama: void 0,
1310
1337
  universal_rest: readString(env, "LLM_API_KEY"),
1311
1338
  rest: readString(env, "LLM_API_KEY"),
@@ -1313,6 +1340,7 @@ function getRagConfig(env = process.env) {
1313
1340
  };
1314
1341
  const embeddingApiKeyByProvider = {
1315
1342
  openai: readString(env, "OPENAI_API_KEY"),
1343
+ gemini: readString(env, "GEMINI_API_KEY"),
1316
1344
  ollama: void 0,
1317
1345
  universal_rest: (_q = readString(env, "EMBEDDING_API_KEY")) != null ? _q : readString(env, "OPENAI_API_KEY"),
1318
1346
  custom: (_r = readString(env, "EMBEDDING_API_KEY")) != null ? _r : readString(env, "OPENAI_API_KEY")
@@ -1727,6 +1755,16 @@ var ConfigValidator = class {
1727
1755
  });
1728
1756
  }
1729
1757
  break;
1758
+ case "gemini":
1759
+ if (!config.apiKey) {
1760
+ errors.push({
1761
+ field: "llm.apiKey",
1762
+ message: "Gemini API key is required",
1763
+ suggestion: "Set GEMINI_API_KEY environment variable",
1764
+ severity: "error"
1765
+ });
1766
+ }
1767
+ break;
1730
1768
  case "ollama":
1731
1769
  if (!config.baseUrl) {
1732
1770
  errors.push({
@@ -1793,6 +1831,14 @@ var ConfigValidator = class {
1793
1831
  severity: "error"
1794
1832
  });
1795
1833
  }
1834
+ if (config.provider === "gemini" && !config.apiKey) {
1835
+ errors.push({
1836
+ field: "embedding.apiKey",
1837
+ message: "Gemini API key is required for embedding",
1838
+ suggestion: "Set GEMINI_API_KEY environment variable",
1839
+ severity: "error"
1840
+ });
1841
+ }
1796
1842
  if (config.provider === "ollama" && !config.baseUrl) {
1797
1843
  errors.push({
1798
1844
  field: "embedding.baseUrl",
@@ -1823,21 +1869,19 @@ var ConfigValidator = class {
1823
1869
  }
1824
1870
  }
1825
1871
  if (config.borderRadius) {
1826
- const validValues = ["none", "sm", "md", "lg", "xl", "full"];
1827
- if (!validValues.includes(config.borderRadius)) {
1872
+ if (!UI_BORDER_RADIUS_OPTIONS.includes(config.borderRadius)) {
1828
1873
  errors.push({
1829
1874
  field: "ui.borderRadius",
1830
- message: `borderRadius must be one of: ${validValues.join(", ")}`,
1875
+ message: `borderRadius must be one of: ${UI_BORDER_RADIUS_OPTIONS.join(", ")}`,
1831
1876
  severity: "warning"
1832
1877
  });
1833
1878
  }
1834
1879
  }
1835
1880
  if (config.visualStyle) {
1836
- const validValues = ["glass", "solid"];
1837
- if (!validValues.includes(config.visualStyle)) {
1881
+ if (!UI_VISUAL_STYLES.includes(config.visualStyle)) {
1838
1882
  errors.push({
1839
1883
  field: "ui.visualStyle",
1840
- message: `visualStyle must be one of: ${validValues.join(", ")}`,
1884
+ message: `visualStyle must be one of: ${UI_VISUAL_STYLES.join(", ")}`,
1841
1885
  severity: "warning"
1842
1886
  });
1843
1887
  }
@@ -2171,6 +2215,101 @@ ${context}`;
2171
2215
  }
2172
2216
  };
2173
2217
 
2218
+ // src/llm/providers/GeminiProvider.ts
2219
+ var import_genai = require("@google/genai");
2220
+ var GeminiProvider = class {
2221
+ constructor(llmConfig, embeddingConfig) {
2222
+ if (!llmConfig.apiKey) throw new Error("[GeminiProvider] llmConfig.apiKey is required");
2223
+ this.client = new import_genai.GoogleGenAI({ apiKey: llmConfig.apiKey });
2224
+ this.llmConfig = llmConfig;
2225
+ this.embeddingConfig = embeddingConfig;
2226
+ }
2227
+ async chat(messages, context, options) {
2228
+ var _a, _b, _c, _d, _e, _f;
2229
+ const systemPrompt = (_a = this.llmConfig.systemPrompt) != null ? _a : `You are a helpful assistant. Answer questions based on the provided context.
2230
+
2231
+ Context:
2232
+ ${context}`;
2233
+ const systemInstruction = systemPrompt.includes("{{context}}") ? systemPrompt.replace("{{context}}", context) : `${systemPrompt}
2234
+
2235
+ Context:
2236
+ ${context}`;
2237
+ const geminiMessages = messages.map((m) => ({
2238
+ role: m.role === "assistant" ? "model" : "user",
2239
+ parts: [{ text: m.content }]
2240
+ }));
2241
+ const response = await this.client.models.generateContent({
2242
+ model: this.llmConfig.model,
2243
+ contents: geminiMessages,
2244
+ config: {
2245
+ systemInstruction,
2246
+ temperature: (_c = (_b = options == null ? void 0 : options.temperature) != null ? _b : this.llmConfig.temperature) != null ? _c : 0.7,
2247
+ maxOutputTokens: (_e = (_d = options == null ? void 0 : options.maxTokens) != null ? _d : this.llmConfig.maxTokens) != null ? _e : 1024,
2248
+ stopSequences: options == null ? void 0 : options.stop
2249
+ }
2250
+ });
2251
+ return (_f = response.text) != null ? _f : "";
2252
+ }
2253
+ async embed(text, options) {
2254
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
2255
+ const model = (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "text-embedding-004";
2256
+ const apiKey = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.apiKey) != null ? _e : this.llmConfig.apiKey;
2257
+ const client = apiKey !== this.llmConfig.apiKey ? new import_genai.GoogleGenAI({ apiKey }) : this.client;
2258
+ let content = text;
2259
+ const queryPrefix = (_f = this.embeddingConfig) == null ? void 0 : _f.queryPrefix;
2260
+ const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
2261
+ if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
2262
+ if (!content.startsWith(queryPrefix)) {
2263
+ content = `${queryPrefix}${text}`;
2264
+ }
2265
+ } else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
2266
+ if (!content.startsWith(docPrefix)) {
2267
+ content = `${docPrefix}${text}`;
2268
+ }
2269
+ }
2270
+ const response = await client.models.embedContent({
2271
+ model,
2272
+ contents: content
2273
+ });
2274
+ return (_j = (_i = (_h = response.embeddings) == null ? void 0 : _h[0]) == null ? void 0 : _i.values) != null ? _j : [];
2275
+ }
2276
+ async batchEmbed(texts, options) {
2277
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
2278
+ const vectors = [];
2279
+ const model = (_c = (_b = options == null ? void 0 : options.model) != null ? _b : (_a = this.embeddingConfig) == null ? void 0 : _a.model) != null ? _c : "text-embedding-004";
2280
+ const apiKey = (_e = (_d = this.embeddingConfig) == null ? void 0 : _d.apiKey) != null ? _e : this.llmConfig.apiKey;
2281
+ const client = apiKey !== this.llmConfig.apiKey ? new import_genai.GoogleGenAI({ apiKey }) : this.client;
2282
+ let contents = texts;
2283
+ const queryPrefix = (_f = this.embeddingConfig) == null ? void 0 : _f.queryPrefix;
2284
+ const docPrefix = (_g = this.embeddingConfig) == null ? void 0 : _g.documentPrefix;
2285
+ if ((options == null ? void 0 : options.taskType) === "query" && queryPrefix) {
2286
+ contents = texts.map((text) => text.startsWith(queryPrefix) ? text : `${queryPrefix}${text}`);
2287
+ } else if ((options == null ? void 0 : options.taskType) === "document" && docPrefix) {
2288
+ contents = texts.map((text) => text.startsWith(docPrefix) ? text : `${docPrefix}${text}`);
2289
+ }
2290
+ const response = await client.models.embedContent({
2291
+ model,
2292
+ contents
2293
+ });
2294
+ return (_i = (_h = response.embeddings) == null ? void 0 : _h.map((e) => {
2295
+ var _a2;
2296
+ return (_a2 = e.values) != null ? _a2 : [];
2297
+ })) != null ? _i : [];
2298
+ }
2299
+ async ping() {
2300
+ try {
2301
+ await this.client.models.embedContent({
2302
+ model: "text-embedding-004",
2303
+ contents: "ping"
2304
+ });
2305
+ return true;
2306
+ } catch (err) {
2307
+ console.error("[GeminiProvider] Ping failed:", err);
2308
+ return false;
2309
+ }
2310
+ }
2311
+ };
2312
+
2174
2313
  // src/llm/providers/UniversalLLMAdapter.ts
2175
2314
  var import_axios2 = __toESM(require("axios"));
2176
2315
  init_templateUtils();
@@ -2368,6 +2507,8 @@ var LLMFactory = class _LLMFactory {
2368
2507
  return new AnthropicProvider(llmConfig, embeddingConfig);
2369
2508
  case "ollama":
2370
2509
  return new OllamaProvider(llmConfig, embeddingConfig);
2510
+ case "gemini":
2511
+ return new GeminiProvider(llmConfig, embeddingConfig);
2371
2512
  case "rest":
2372
2513
  case "universal_rest":
2373
2514
  case "custom":
@@ -2759,8 +2900,7 @@ var EmbeddingStrategyResolver = class {
2759
2900
  * Check if an LLM provider natively supports embeddings
2760
2901
  */
2761
2902
  static supportsEmbedding(provider) {
2762
- const providersWithEmbedding = ["openai", "ollama", "rest", "universal_rest"];
2763
- return providersWithEmbedding.includes(provider);
2903
+ return PROVIDERS_WITH_EMBEDDINGS.includes(provider);
2764
2904
  }
2765
2905
  /**
2766
2906
  * Get a human-readable description of the strategy
package/dist/server.mjs CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  createIngestHandler,
35
35
  createUploadHandler,
36
36
  getRagConfig
37
- } from "./chunk-GT72OIOD.mjs";
37
+ } from "./chunk-SNPFZBDK.mjs";
38
38
  import "./chunk-EDLTMSNY.mjs";
39
39
  import {
40
40
  PineconeProvider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -85,6 +85,7 @@
85
85
  },
86
86
  "dependencies": {
87
87
  "@anthropic-ai/sdk": "^0.90.0",
88
+ "@google/genai": "^1.50.1",
88
89
  "@pinecone-database/pinecone": "^7.2.0",
89
90
  "axios": "^1.15.0",
90
91
  "lucide-react": "^1.8.0",
@@ -12,6 +12,7 @@
12
12
  import { ILLMProvider } from '../llm/ILLMProvider';
13
13
  import { LLMFactory } from '../llm/LLMFactory';
14
14
  import { LLMConfig, EmbeddingConfig, LLMProvider, EmbeddingProvider } from './RagConfig';
15
+ import { PROVIDERS_WITH_EMBEDDINGS } from './constants';
15
16
 
16
17
  export enum EmbeddingStrategy {
17
18
  /**
@@ -125,8 +126,7 @@ export class EmbeddingStrategyResolver {
125
126
  * Check if an LLM provider natively supports embeddings
126
127
  */
127
128
  private static supportsEmbedding(provider: LLMProvider | string): boolean {
128
- const providersWithEmbedding = ['openai', 'ollama', 'rest', 'universal_rest'];
129
- return providersWithEmbedding.includes(provider);
129
+ return (PROVIDERS_WITH_EMBEDDINGS as readonly string[]).includes(provider);
130
130
  }
131
131
 
132
132
  /**
@@ -8,7 +8,13 @@
8
8
  // Vector DB
9
9
  // ---------------------------------------------------------------------------
10
10
 
11
- export type VectorDBProvider = 'pinecone' | 'pgvector' | 'postgresql' | 'mongodb' | 'milvus' | 'qdrant' | 'chromadb' | 'redis' | 'weaviate' | 'rest' | 'universal_rest' | 'custom';
11
+ import {
12
+ VECTOR_DB_PROVIDERS,
13
+ LLM_PROVIDERS,
14
+ EMBEDDING_PROVIDERS,
15
+ } from './constants';
16
+
17
+ export type VectorDBProvider = typeof VECTOR_DB_PROVIDERS[number];
12
18
 
13
19
  export interface VectorDBConfig {
14
20
  /** Which vector database to use */
@@ -40,7 +46,7 @@ export interface VectorDBConfig {
40
46
  // LLM Provider
41
47
  // ---------------------------------------------------------------------------
42
48
 
43
- export type LLMProvider = 'openai' | 'anthropic' | 'ollama' | 'rest' | 'universal_rest' | 'custom';
49
+ export type LLMProvider = typeof LLM_PROVIDERS[number];
44
50
 
45
51
  export interface LLMConfig {
46
52
  /** Which LLM provider to use */
@@ -72,7 +78,7 @@ export interface LLMConfig {
72
78
  // Embedding
73
79
  // ---------------------------------------------------------------------------
74
80
 
75
- export type EmbeddingProvider = 'openai' | 'ollama' | 'rest' | 'universal_rest' | 'custom';
81
+ export type EmbeddingProvider = typeof EMBEDDING_PROVIDERS[number];
76
82
 
77
83
  export interface EmbeddingConfig {
78
84
  /** Which embedding provider to use */
@@ -0,0 +1,52 @@
1
+ /**
2
+ * constants.ts — Centralized definitions of acceptable configuration values.
3
+ *
4
+ * This file serves as the single source of truth for all statically supported
5
+ * providers and stylistic options in the Retrivora AI RAG Engine.
6
+ */
7
+
8
+ export const VECTOR_DB_PROVIDERS = [
9
+ 'pinecone',
10
+ 'pgvector',
11
+ 'postgresql',
12
+ 'mongodb',
13
+ 'milvus',
14
+ 'qdrant',
15
+ 'chromadb',
16
+ 'redis',
17
+ 'weaviate',
18
+ 'rest',
19
+ 'universal_rest',
20
+ 'custom',
21
+ ] as const;
22
+
23
+ export const LLM_PROVIDERS = [
24
+ 'openai',
25
+ 'anthropic',
26
+ 'ollama',
27
+ 'gemini',
28
+ 'rest',
29
+ 'universal_rest',
30
+ 'custom',
31
+ ] as const;
32
+
33
+ export const EMBEDDING_PROVIDERS = [
34
+ 'openai',
35
+ 'ollama',
36
+ 'gemini',
37
+ 'rest',
38
+ 'universal_rest',
39
+ 'custom',
40
+ ] as const;
41
+
42
+ export const PROVIDERS_WITH_EMBEDDINGS = [
43
+ 'openai',
44
+ 'ollama',
45
+ 'gemini',
46
+ 'rest',
47
+ 'universal_rest',
48
+ ] as const;
49
+
50
+ export const UI_VISUAL_STYLES = ['glass', 'solid'] as const;
51
+
52
+ export const UI_BORDER_RADIUS_OPTIONS = ['none', 'sm', 'md', 'lg', 'xl', 'full'] as const;
@@ -19,14 +19,11 @@
19
19
  */
20
20
 
21
21
  import { RagConfig } from './RagConfig';
22
-
23
- const VECTOR_DB_PROVIDERS = [
24
- 'pinecone', 'pgvector', 'postgresql', 'mongodb', 'milvus',
25
- 'qdrant', 'chromadb', 'redis', 'weaviate', 'rest', 'universal_rest', 'custom',
26
- ] as const;
27
-
28
- const LLM_PROVIDERS = ['openai', 'anthropic', 'ollama', 'rest', 'universal_rest', 'custom'] as const;
29
- const EMBEDDING_PROVIDERS = ['openai', 'ollama', 'rest', 'universal_rest', 'custom'] as const;
22
+ import {
23
+ VECTOR_DB_PROVIDERS,
24
+ LLM_PROVIDERS,
25
+ EMBEDDING_PROVIDERS,
26
+ } from './constants';
30
27
 
31
28
  function readString(env: Record<string, string | undefined>, name: string): string | undefined {
32
29
  const value = env[name]?.trim();
@@ -112,6 +109,7 @@ export function getRagConfig(env: Record<string, string | undefined> = process.e
112
109
  const llmApiKeyByProvider: Record<string, string | undefined> = {
113
110
  openai: readString(env, 'OPENAI_API_KEY'),
114
111
  anthropic: readString(env, 'ANTHROPIC_API_KEY'),
112
+ gemini: readString(env, 'GEMINI_API_KEY'),
115
113
  ollama: undefined,
116
114
  universal_rest: readString(env, 'LLM_API_KEY'),
117
115
  rest: readString(env, 'LLM_API_KEY'),
@@ -120,6 +118,7 @@ export function getRagConfig(env: Record<string, string | undefined> = process.e
120
118
 
121
119
  const embeddingApiKeyByProvider: Record<string, string | undefined> = {
122
120
  openai: readString(env, 'OPENAI_API_KEY'),
121
+ gemini: readString(env, 'GEMINI_API_KEY'),
123
122
  ollama: undefined,
124
123
  universal_rest: readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'OPENAI_API_KEY'),
125
124
  custom: readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'OPENAI_API_KEY'),
@@ -10,6 +10,10 @@
10
10
  */
11
11
 
12
12
  import { RagConfig, VectorDBConfig, LLMConfig, EmbeddingConfig } from '../config/RagConfig';
13
+ import {
14
+ UI_VISUAL_STYLES,
15
+ UI_BORDER_RADIUS_OPTIONS,
16
+ } from '../config/constants';
13
17
 
14
18
  export interface ValidationError {
15
19
  field: string;
@@ -380,6 +384,16 @@ export class ConfigValidator {
380
384
  });
381
385
  }
382
386
  break;
387
+ case 'gemini':
388
+ if (!config.apiKey) {
389
+ errors.push({
390
+ field: 'llm.apiKey',
391
+ message: 'Gemini API key is required',
392
+ suggestion: 'Set GEMINI_API_KEY environment variable',
393
+ severity: 'error',
394
+ });
395
+ }
396
+ break;
383
397
  case 'ollama':
384
398
  if (!config.baseUrl) {
385
399
  errors.push({
@@ -454,6 +468,15 @@ export class ConfigValidator {
454
468
  });
455
469
  }
456
470
 
471
+ if (config.provider === 'gemini' && !config.apiKey) {
472
+ errors.push({
473
+ field: 'embedding.apiKey',
474
+ message: 'Gemini API key is required for embedding',
475
+ suggestion: 'Set GEMINI_API_KEY environment variable',
476
+ severity: 'error',
477
+ });
478
+ }
479
+
457
480
  if (config.provider === 'ollama' && !config.baseUrl) {
458
481
  errors.push({
459
482
  field: 'embedding.baseUrl',
@@ -489,22 +512,20 @@ export class ConfigValidator {
489
512
  }
490
513
 
491
514
  if (config.borderRadius) {
492
- const validValues = ['none', 'sm', 'md', 'lg', 'xl', 'full'];
493
- if (!validValues.includes(config.borderRadius as string)) {
515
+ if (!UI_BORDER_RADIUS_OPTIONS.includes(config.borderRadius as any)) {
494
516
  errors.push({
495
517
  field: 'ui.borderRadius',
496
- message: `borderRadius must be one of: ${validValues.join(', ')}`,
518
+ message: `borderRadius must be one of: ${UI_BORDER_RADIUS_OPTIONS.join(', ')}`,
497
519
  severity: 'warning',
498
520
  });
499
521
  }
500
522
  }
501
523
 
502
524
  if (config.visualStyle) {
503
- const validValues = ['glass', 'solid'];
504
- if (!validValues.includes(config.visualStyle as string)) {
525
+ if (!UI_VISUAL_STYLES.includes(config.visualStyle as any)) {
505
526
  errors.push({
506
527
  field: 'ui.visualStyle',
507
- message: `visualStyle must be one of: ${validValues.join(', ')}`,
528
+ message: `visualStyle must be one of: ${UI_VISUAL_STYLES.join(', ')}`,
508
529
  severity: 'warning',
509
530
  });
510
531
  }
@@ -10,6 +10,7 @@ import { LLMConfig, EmbeddingConfig, LLMProvider } from '../config/RagConfig';
10
10
  import { OpenAIProvider } from './providers/OpenAIProvider';
11
11
  import { AnthropicProvider } from './providers/AnthropicProvider';
12
12
  import { OllamaProvider } from './providers/OllamaProvider';
13
+ import { GeminiProvider } from './providers/GeminiProvider';
13
14
  import { UniversalLLMAdapter } from './providers/UniversalLLMAdapter';
14
15
 
15
16
  export class LLMFactory {
@@ -21,6 +22,8 @@ export class LLMFactory {
21
22
  return new AnthropicProvider(llmConfig, embeddingConfig);
22
23
  case 'ollama':
23
24
  return new OllamaProvider(llmConfig, embeddingConfig);
25
+ case 'gemini':
26
+ return new GeminiProvider(llmConfig, embeddingConfig);
24
27
  case 'rest':
25
28
  case 'universal_rest':
26
29
  case 'custom':
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Gemini LLM Provider (Google Gen AI)
3
+ *
4
+ * Handles both chat completion (gemini-2.5-flash, gemini-2.5-pro, etc.) and
5
+ * embedding (text-embedding-004).
6
+ * Uses the modern @google/genai SDK.
7
+ *
8
+ * Required LLMConfig fields:
9
+ * - apiKey: string – Gemini API key
10
+ * - model: string – e.g. "gemini-2.5-flash"
11
+ *
12
+ * Required EmbeddingConfig fields (when used for embedding):
13
+ * - apiKey: string – same or separate key
14
+ * - model: string – e.g. "text-embedding-004"
15
+ */
16
+
17
+ import { GoogleGenAI } from '@google/genai';
18
+ import { ILLMProvider, ChatMessage, ChatOptions, EmbedOptions } from '../ILLMProvider';
19
+ import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
20
+
21
+ export class GeminiProvider implements ILLMProvider {
22
+ private readonly client: GoogleGenAI;
23
+ private readonly llmConfig: LLMConfig;
24
+ private readonly embeddingConfig?: EmbeddingConfig;
25
+
26
+ constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
27
+ if (!llmConfig.apiKey) throw new Error('[GeminiProvider] llmConfig.apiKey is required');
28
+ this.client = new GoogleGenAI({ apiKey: llmConfig.apiKey });
29
+ this.llmConfig = llmConfig;
30
+ this.embeddingConfig = embeddingConfig;
31
+ }
32
+
33
+ async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
34
+ const systemPrompt =
35
+ this.llmConfig.systemPrompt ??
36
+ `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
37
+
38
+ const systemInstruction = systemPrompt.includes('{{context}}')
39
+ ? systemPrompt.replace('{{context}}', context)
40
+ : `${systemPrompt}\n\nContext:\n${context}`;
41
+
42
+ // Gemini requires alternating user/model messages, starting with user.
43
+ // If the first message is not user, or there are consecutive same-role messages, we need to adapt.
44
+ // To simplify and comply with typical usage, we'll map 'assistant' to 'model'.
45
+ const geminiMessages = messages.map((m) => ({
46
+ role: m.role === 'assistant' ? 'model' : 'user',
47
+ parts: [{ text: m.content }],
48
+ }));
49
+
50
+ const response = await this.client.models.generateContent({
51
+ model: this.llmConfig.model,
52
+ contents: geminiMessages,
53
+ config: {
54
+ systemInstruction,
55
+ temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
56
+ maxOutputTokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
57
+ stopSequences: options?.stop,
58
+ },
59
+ });
60
+
61
+ return response.text ?? '';
62
+ }
63
+
64
+ async embed(text: string, options?: EmbedOptions): Promise<number[]> {
65
+ const model =
66
+ options?.model ??
67
+ this.embeddingConfig?.model ??
68
+ 'text-embedding-004';
69
+
70
+ const apiKey = this.embeddingConfig?.apiKey ?? this.llmConfig.apiKey;
71
+ const client = apiKey !== this.llmConfig.apiKey
72
+ ? new GoogleGenAI({ apiKey })
73
+ : this.client;
74
+
75
+ let content = text;
76
+
77
+ // Dynamically apply prefixes from configuration if they exist
78
+ const queryPrefix = this.embeddingConfig?.queryPrefix;
79
+ const docPrefix = this.embeddingConfig?.documentPrefix;
80
+
81
+ if (options?.taskType === 'query' && queryPrefix) {
82
+ if (!content.startsWith(queryPrefix)) {
83
+ content = `${queryPrefix}${text}`;
84
+ }
85
+ } else if (options?.taskType === 'document' && docPrefix) {
86
+ if (!content.startsWith(docPrefix)) {
87
+ content = `${docPrefix}${text}`;
88
+ }
89
+ }
90
+
91
+ const response = await client.models.embedContent({
92
+ model,
93
+ contents: content,
94
+ });
95
+
96
+ return response.embeddings?.[0]?.values ?? [];
97
+ }
98
+
99
+ async batchEmbed(texts: string[], options?: EmbedOptions): Promise<number[][]> {
100
+ const vectors: number[][] = [];
101
+ // Sequential fallback for Gemini (API doesn't have a direct batch endpoint in the same way, though you can pass multiple contents.
102
+ // For simplicity and to match the other providers' structure, we'll iterate or pass array.
103
+ // The new SDK supports an array of contents.
104
+ const model =
105
+ options?.model ??
106
+ this.embeddingConfig?.model ??
107
+ 'text-embedding-004';
108
+
109
+ const apiKey = this.embeddingConfig?.apiKey ?? this.llmConfig.apiKey;
110
+ const client = apiKey !== this.llmConfig.apiKey
111
+ ? new GoogleGenAI({ apiKey })
112
+ : this.client;
113
+
114
+ let contents = texts;
115
+
116
+ // Apply prefixes if needed
117
+ const queryPrefix = this.embeddingConfig?.queryPrefix;
118
+ const docPrefix = this.embeddingConfig?.documentPrefix;
119
+
120
+ if (options?.taskType === 'query' && queryPrefix) {
121
+ contents = texts.map(text => text.startsWith(queryPrefix) ? text : `${queryPrefix}${text}`);
122
+ } else if (options?.taskType === 'document' && docPrefix) {
123
+ contents = texts.map(text => text.startsWith(docPrefix) ? text : `${docPrefix}${text}`);
124
+ }
125
+
126
+ const response = await client.models.embedContent({
127
+ model,
128
+ contents,
129
+ });
130
+
131
+ return response.embeddings?.map(e => e.values ?? []) ?? [];
132
+ }
133
+
134
+ async ping(): Promise<boolean> {
135
+ try {
136
+ // Perform a lightweight embed call to verify connectivity
137
+ await this.client.models.embedContent({
138
+ model: 'text-embedding-004',
139
+ contents: 'ping',
140
+ });
141
+ return true;
142
+ } catch (err) {
143
+ console.error('[GeminiProvider] Ping failed:', err);
144
+ return false;
145
+ }
146
+ }
147
+ }