@retrivora-ai/rag-engine 2.0.4 → 2.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
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": "UNLICENSED",
@@ -75,7 +75,7 @@
75
75
  "scripts": {
76
76
  "dev": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --watch --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style",
77
77
  "build": "npm run build:pkg",
78
- "build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css && rm src/index.css",
78
+ "build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
79
79
  "lint": "eslint",
80
80
  "clean": "rm -rf dist"
81
81
  },
@@ -18,7 +18,8 @@
18
18
  * universal_rest→ VECTOR_BASE_URL, VECTOR_DB_REST_API_KEY
19
19
  */
20
20
 
21
- import { RagConfig } from './RagConfig';
21
+ import { RagConfig, LLMProvider, EmbeddingProvider } from './RagConfig';
22
+ import { LicenseVerifier } from '../core/LicenseVerifier';
22
23
  import {
23
24
  VECTOR_DB_PROVIDERS,
24
25
  LLM_PROVIDERS,
@@ -78,9 +79,7 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
78
79
  Boolean(licenseKey);
79
80
 
80
81
  const vectorProvider = readEnum(env, 'VECTOR_DB_PROVIDER', 'pinecone', VECTOR_DB_PROVIDERS);
81
- const llmProvider = readEnum(env, 'LLM_PROVIDER', 'openai', LLM_PROVIDERS);
82
- const embeddingProvider = readEnum(env, 'EMBEDDING_PROVIDER', 'openai', EMBEDDING_PROVIDERS);
83
- const embeddingDimensions = readNumber(env, 'EMBEDDING_DIMENSIONS', 1536);
82
+ const embeddingDimensions = readNumber(env, 'EMBEDDING_DIMENSIONS', 768);
84
83
 
85
84
  // Build provider-specific vectorDb options
86
85
  const vectorDbOptions: Record<string, unknown> = {};
@@ -160,9 +159,37 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
160
159
  custom: 'default',
161
160
  };
162
161
 
162
+ // Check if the license key belongs to a Free / Trial tier
163
+ const isFreeTier = (() => {
164
+ if (!licenseKey) return true;
165
+ try {
166
+ const payload = LicenseVerifier.verify(licenseKey, projectId);
167
+ const tier = (payload.tier || '').toLowerCase();
168
+ return tier === 'free_trial' || tier === 'free_tier' || tier === 'free' || tier === 'hobby';
169
+ } catch {
170
+ return true;
171
+ }
172
+ })();
173
+
174
+ const DEFAULT_FREE_GATEWAY = 'https://llm.retrivora.com/api/v1';
175
+
176
+ const defaultLlmProvider = isFreeTier ? 'universal_rest' : 'universal_rest';
177
+ const llmProvider = (readEnum(env, 'LLM_PROVIDER', defaultLlmProvider, LLM_PROVIDERS) ?? base?.llm?.provider ?? defaultLlmProvider) as LLMProvider;
178
+ const llmBaseUrl = readString(env, 'LLM_BASE_URL') ?? base?.llm?.baseUrl ?? DEFAULT_FREE_GATEWAY;
179
+ const llmModel = readString(env, 'LLM_MODEL') ?? base?.llm?.model ?? (isFreeTier ? 'llama-3.1-8b-instant' : (DEFAULT_MODEL_BY_PROVIDER[llmProvider] ?? 'gpt-4o'));
180
+ const llmApiKey = llmApiKeyByProvider[llmProvider] ?? readString(env, 'LLM_API_KEY') ?? base?.llm?.apiKey ?? 'sk-retrivora-cancer-280590';
181
+ const llmProfile = readString(env, 'LLM_UNIVERSAL_PROFILE') ?? (base?.llm?.options?.profile as string) ?? 'litellm';
182
+
183
+ const defaultEmbeddingProvider = isFreeTier ? 'universal_rest' : 'universal_rest';
184
+ const embeddingProvider = (readEnum(env, 'EMBEDDING_PROVIDER', defaultEmbeddingProvider, EMBEDDING_PROVIDERS) ?? base?.embedding?.provider ?? defaultEmbeddingProvider) as EmbeddingProvider;
185
+ const embeddingBaseUrl = readString(env, 'EMBEDDING_BASE_URL') ?? base?.embedding?.baseUrl ?? DEFAULT_FREE_GATEWAY;
186
+ const embeddingModel = readString(env, 'EMBEDDING_MODEL') ?? base?.embedding?.model ?? 'text-embedding-004';
187
+ const embeddingApiKey = embeddingApiKeyByProvider[embeddingProvider] ?? readString(env, 'EMBEDDING_API_KEY') ?? readString(env, 'LLM_API_KEY') ?? base?.embedding?.apiKey ?? 'sk-retrivora-cancer-280590';
188
+ const embeddingProfile = readString(env, 'EMBEDDING_UNIVERSAL_PROFILE') ?? (base?.embedding?.options?.profile as string) ?? 'litellm';
189
+
163
190
  return {
164
191
  projectId,
165
- licenseKey: readString(env, 'RETRIVORA_LICENSE_KEY') ?? readString(env, 'LICENSE_KEY') ?? base?.licenseKey,
192
+ licenseKey,
166
193
  vectorDb: {
167
194
  provider: vectorProvider,
168
195
  indexName:
@@ -173,28 +200,28 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
173
200
  },
174
201
  llm: {
175
202
  provider: llmProvider,
176
- model: readString(env, 'LLM_MODEL') ?? DEFAULT_MODEL_BY_PROVIDER[llmProvider] ?? 'gpt-4o',
177
- apiKey: llmApiKeyByProvider[llmProvider] ?? '',
178
- baseUrl: readString(env, 'LLM_BASE_URL'),
203
+ model: llmModel,
204
+ apiKey: llmApiKey,
205
+ baseUrl: llmBaseUrl,
179
206
  systemPrompt: readString(env, 'LLM_SYSTEM_PROMPT'),
180
207
  maxTokens: readNumber(env, 'LLM_MAX_TOKENS', 4096),
181
208
  temperature: readNumber(env, 'LLM_TEMPERATURE', 0.7),
182
209
  options: {
183
- profile: readString(env, 'LLM_UNIVERSAL_PROFILE'),
210
+ profile: llmProfile,
184
211
  thinking: readString(env, 'LLM_THINKING') === 'true' || readString(env, 'LLM_THINKING') === 'enabled' ? true : (readString(env, 'LLM_THINKING') === 'false' ? false : undefined),
185
212
  thinkingBudget: env.LLM_THINKING_BUDGET ? parseInt(env.LLM_THINKING_BUDGET, 10) : undefined,
186
213
  },
187
214
  },
188
215
  embedding: {
189
216
  provider: embeddingProvider,
190
- model: readString(env, 'EMBEDDING_MODEL') ?? 'text-embedding-3-small',
191
- apiKey: embeddingApiKeyByProvider[embeddingProvider],
192
- baseUrl: readString(env, 'EMBEDDING_BASE_URL'),
217
+ model: embeddingModel,
218
+ apiKey: embeddingApiKey,
219
+ baseUrl: embeddingBaseUrl,
193
220
  dimensions: embeddingDimensions,
194
221
  queryPrefix: readString(env, 'EMBEDDING_QUERY_PREFIX'),
195
222
  documentPrefix: readString(env, 'EMBEDDING_DOCUMENT_PREFIX'),
196
223
  options: {
197
- profile: readString(env, 'EMBEDDING_UNIVERSAL_PROFILE'),
224
+ profile: embeddingProfile,
198
225
  },
199
226
  },
200
227
  ui: {
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import * as os from 'os';
3
4
  import { RagConfig } from '../config/RagConfig';
4
5
  import { LicenseVerifier } from './LicenseVerifier';
5
6
 
@@ -35,7 +36,10 @@ export class DatabaseStorage {
35
36
  private feedbackTableName: string;
36
37
 
37
38
  // Filesystem fallback configuration
38
- private fallbackDir = path.join(process.cwd(), '.retrivora');
39
+ private isServerless = Boolean(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.LAMBDA_TASK_ROOT);
40
+ private fallbackDir = this.isServerless
41
+ ? path.join(os.tmpdir(), '.retrivora')
42
+ : path.join(process.cwd(), '.retrivora');
39
43
  private historyFile = path.join(this.fallbackDir, 'history.json');
40
44
  private feedbackFile = path.join(this.fallbackDir, 'feedback.json');
41
45
 
@@ -191,10 +195,28 @@ export class DatabaseStorage {
191
195
 
192
196
  // Helper for FS fallback writes
193
197
  private writeLocalFile(filePath: string, data: any[]): void {
194
- if (!fs.existsSync(this.fallbackDir)) {
195
- fs.mkdirSync(this.fallbackDir, { recursive: true });
198
+ try {
199
+ const dir = path.dirname(filePath);
200
+ if (!fs.existsSync(dir)) {
201
+ fs.mkdirSync(dir, { recursive: true });
202
+ }
203
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
204
+ } catch (err: any) {
205
+ if (err.code === 'EROFS') {
206
+ try {
207
+ const tmpDir = path.join(os.tmpdir(), '.retrivora');
208
+ if (!fs.existsSync(tmpDir)) {
209
+ fs.mkdirSync(tmpDir, { recursive: true });
210
+ }
211
+ const tmpFilePath = path.join(tmpDir, path.basename(filePath));
212
+ fs.writeFileSync(tmpFilePath, JSON.stringify(data, null, 2), 'utf-8');
213
+ } catch {
214
+ /* ignore write failure in read-only environment */
215
+ }
216
+ } else {
217
+ console.warn(`[DatabaseStorage] Failed to write local fallback file: ${err.message}`);
218
+ }
196
219
  }
197
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
198
220
  }
199
221
 
200
222
  // ─── Message History Operations ──────────────────────────────────────────
@@ -608,7 +608,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
608
608
 
609
609
  // 4. Context Augmentation - Review all retrieved/reranked sources for full comprehension
610
610
  let context = fullSources.length
611
- ? fullSources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
611
+ ? fullSources.map((m, i) => `[Source ${i + 1}]\n${m?.content ?? ''}`).join('\n\n---\n\n')
612
612
  : 'No relevant context found.';
613
613
 
614
614
  // Count the records of relevant sources to pass into the slice dynamically
@@ -722,7 +722,12 @@ You are a helpful assistant. Use the provided context to answer questions accura
722
722
  finalRestrictionSuffix += '\n\n(IMPORTANT: You must think step-by-step before answering. Write your thinking process inside a `<think>...</think>` block. Write the `<think>` block first, then follow it with your final response. Keep the thinking process thorough and detailed. Example: `<think>Thinking details here...</think>Final answer text here.`)';
723
723
  }
724
724
 
725
- const hardenedHistory = [...history];
725
+ const hardenedHistory = (Array.isArray(history) ? history : [])
726
+ .filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
727
+ .map((m) => ({
728
+ role: m.role || 'user',
729
+ content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
730
+ }));
726
731
  const userQuestion = { role: 'user', content: question + finalRestrictionSuffix } as ChatMessage;
727
732
  const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
728
733
 
@@ -880,11 +885,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
880
885
  rewrittenQuery,
881
886
  systemPrompt,
882
887
  userPrompt: question + finalRestrictionSuffix,
883
- chunks: sources.map((s) => ({
888
+ chunks: (sources || []).filter(Boolean).map((s) => ({
884
889
  id: s.id,
885
890
  score: s.score,
886
- content: s.content,
887
- metadata: s.metadata ?? {},
891
+ content: s?.content ?? '',
892
+ metadata: s?.metadata ?? {},
888
893
  namespace: ns,
889
894
  } satisfies RetrievedChunk)),
890
895
  latency,
@@ -271,14 +271,17 @@ export function createChatHandler(
271
271
 
272
272
  try {
273
273
  const body = await req.json();
274
- const { message: rawMessage, history = [], namespace, sessionId = 'default', messageId, userMessageId } = body as {
275
- message: string;
276
- history?: ChatMessage[];
277
- namespace?: string;
278
- sessionId?: string;
279
- messageId?: string;
280
- userMessageId?: string;
281
- };
274
+ const bodyObj = (body ?? {}) as any;
275
+ let rawMessage = bodyObj.message;
276
+ if (!rawMessage && Array.isArray(bodyObj.messages) && bodyObj.messages.length > 0) {
277
+ const lastMsg = bodyObj.messages[bodyObj.messages.length - 1];
278
+ rawMessage = typeof lastMsg === 'string' ? lastMsg : (lastMsg?.content || lastMsg?.text);
279
+ }
280
+ if (!rawMessage && typeof bodyObj.prompt === 'string') {
281
+ rawMessage = bodyObj.prompt;
282
+ }
283
+ const history = bodyObj.history || (Array.isArray(bodyObj.messages) ? bodyObj.messages.slice(0, -1) : []);
284
+ const { namespace, sessionId = 'default', messageId, userMessageId } = bodyObj;
282
285
 
283
286
  // 2. Input sanitization
284
287
  const sanitized = sanitizeInput(rawMessage ?? '');
@@ -359,7 +362,17 @@ export function createStreamHandler(
359
362
  });
360
363
  }
361
364
 
362
- const { message: rawMessage, history = [], namespace, sessionId = 'default', messageId, userMessageId } = body;
365
+ const bodyObj = (body ?? {}) as any;
366
+ let rawMessage = bodyObj.message;
367
+ if (!rawMessage && Array.isArray(bodyObj.messages) && bodyObj.messages.length > 0) {
368
+ const lastMsg = bodyObj.messages[bodyObj.messages.length - 1];
369
+ rawMessage = typeof lastMsg === 'string' ? lastMsg : (lastMsg?.content || lastMsg?.text);
370
+ }
371
+ if (!rawMessage && typeof bodyObj.prompt === 'string') {
372
+ rawMessage = bodyObj.prompt;
373
+ }
374
+ const history = bodyObj.history || (Array.isArray(bodyObj.messages) ? bodyObj.messages.slice(0, -1) : []);
375
+ const { namespace, sessionId = 'default', messageId, userMessageId } = bodyObj;
363
376
 
364
377
  // 2. Input sanitization
365
378
  const sanitized = sanitizeInput(rawMessage ?? '');