@exulu/backend 1.58.0 → 1.59.0

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/index.js CHANGED
@@ -2,18 +2,22 @@ import {
2
2
  ExuluContext,
3
3
  ExuluStorage,
4
4
  ExuluTool,
5
+ ResolveModelError,
5
6
  STATISTICS_TYPE_ENUM,
6
7
  applyAccessControl,
7
8
  applyFilters,
8
9
  applySorting,
9
10
  authentication,
11
+ buildTags,
10
12
  checkLicense,
13
+ checkRecordAccess,
11
14
  convertContextToTableDefinition,
12
15
  convertExuluToolsToAiSdkTools,
13
16
  copyS3Object,
14
17
  coreSchemas,
15
18
  createAgenticRetrievalToolV3,
16
19
  createProjectItemsRetrievalTool,
20
+ createTaggedFetch,
17
21
  createUppyRoutes,
18
22
  deleteS3Object,
19
23
  downloadKeyIntoSandbox,
@@ -26,18 +30,26 @@ import {
26
30
  getS3SignedUploadUrl,
27
31
  getTableName,
28
32
  getToken,
33
+ isLiteLLMEnabled,
29
34
  listS3ObjectsByPrefix,
30
35
  mapType,
31
36
  postgresClient,
32
37
  reportSystemDependencies,
38
+ resolveModel,
33
39
  sanitizeName,
34
40
  sanitizeToolName,
41
+ setLiteLLMPackageRoot,
42
+ startLiteLLMSupervisor,
35
43
  trajectoryRegistry,
36
44
  updateStatistic,
37
45
  uploadFile,
38
46
  vectorSearch,
47
+ waitForLiteLLMReady,
39
48
  withRetry
40
- } from "./chunk-RVLZ5EL3.js";
49
+ } from "./chunk-U36VJDZ7.js";
50
+ import {
51
+ findLiteLLMModel
52
+ } from "./chunk-YS27XOXI.js";
41
53
 
42
54
  // src/index.ts
43
55
  import "dotenv/config";
@@ -267,71 +279,15 @@ import { makeExecutableSchema } from "@graphql-tools/schema";
267
279
  import GraphQLJSON from "graphql-type-json";
268
280
  import cron from "cron-validator";
269
281
 
270
- // src/utils/check-record-access.ts
271
- var checkRecordAccessCache = /* @__PURE__ */ new Map();
272
- var checkRecordAccess = async (record, request, user) => {
273
- const setRecordAccessCache = (hasAccess2) => {
274
- checkRecordAccessCache.set(`${record.id}-${request}-${user?.id}`, {
275
- hasAccess: hasAccess2,
276
- expiresAt: new Date(Date.now() + 1e3 * 60 * 1)
277
- // 1 minute
278
- });
279
- };
280
- const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
281
- if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
282
- return cachedAccess.hasAccess;
283
- }
284
- const isPublic = record.rights_mode === "public";
285
- const byUsers = record.rights_mode === "users";
286
- const byRoles = record.rights_mode === "roles";
287
- const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
288
- const isCreator = user ? createdBy === user.id.toString() : false;
289
- const isAdmin = user ? user.super_admin : false;
290
- const isApi = user ? user.type === "api" : false;
291
- const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
292
- const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
293
- let hasAccess = "none";
294
- if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
295
- setRecordAccessCache(true);
296
- return true;
297
- }
298
- if (byUsers) {
299
- if (!user) {
300
- setRecordAccessCache(false);
301
- return false;
302
- }
303
- hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
304
- if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
305
- console.error(
306
- `[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
307
- );
308
- setRecordAccessCache(false);
309
- return false;
310
- } else {
311
- setRecordAccessCache(true);
312
- return true;
313
- }
314
- }
315
- if (byRoles) {
316
- if (!user) {
317
- setRecordAccessCache(false);
318
- return false;
319
- }
320
- hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
321
- if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
322
- console.error(
323
- `[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
324
- );
325
- setRecordAccessCache(false);
326
- return false;
327
- } else {
328
- setRecordAccessCache(true);
329
- return true;
330
- }
331
- }
332
- setRecordAccessCache(false);
333
- return false;
334
- };
282
+ // src/exulu/resolve-agent-provider.ts
283
+ async function resolveAgentProvider(agent, providers) {
284
+ if (isLiteLLMEnabled()) return void 0;
285
+ if (!agent.model) return void 0;
286
+ const { db } = await postgresClient();
287
+ const modelRow = await db.from("models").where({ id: agent.model }).first();
288
+ if (!modelRow?.provider) return void 0;
289
+ return providers.find((p) => p.id === modelRow.provider);
290
+ }
335
291
 
336
292
  // ee/queues/queues.ts
337
293
  import { Queue } from "bullmq";
@@ -577,15 +533,26 @@ var exuluProviderFields = [
577
533
 
578
534
  // src/graphql/utilities/sanitize-and-hydrate-fields.ts
579
535
  var addProviderFields = async (args, requestedFields, providers, result, tools, user, contexts, rerankers) => {
580
- let provider = providers.find((a) => a.id === result?.provider);
536
+ let provider;
537
+ let modelRow;
538
+ let litellmEntry;
539
+ if (isLiteLLMEnabled() && result?.model) {
540
+ litellmEntry = await findLiteLLMModel(result.model);
541
+ } else if (result?.model) {
542
+ const { db } = await postgresClient();
543
+ modelRow = await db.from("models").where({ id: result.model }).first();
544
+ if (modelRow?.provider) {
545
+ provider = providers.find((a) => a.id === modelRow.provider);
546
+ }
547
+ }
581
548
  if (requestedFields.includes("providerName")) {
582
- result.providerName = provider?.providerName || "";
549
+ result.providerName = isLiteLLMEnabled() ? "LiteLLM" : provider?.providerName || "";
583
550
  }
584
551
  if (requestedFields.includes("modelName")) {
585
- result.modelName = provider?.modelName || "";
552
+ result.modelName = isLiteLLMEnabled() ? result?.model ?? "" : modelRow?.name || provider?.modelName || "";
586
553
  }
587
554
  if (requestedFields.includes("slug")) {
588
- result.slug = provider?.slug || "";
555
+ result.slug = isLiteLLMEnabled() ? "/agents/litellm/run" : provider?.slug || "";
589
556
  }
590
557
  if (requestedFields.includes("rateLimit")) {
591
558
  result.rateLimit = provider?.rateLimit || "";
@@ -594,9 +561,9 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
594
561
  if (result.tools) {
595
562
  result.tools = await Promise.all(
596
563
  result.tools.map(
597
- async (tool) => {
564
+ async (tool2) => {
598
565
  let hydrated;
599
- if (tool.id === "agentic_context_search") {
566
+ if (tool2.id === "agentic_context_search") {
600
567
  const instance = createAgenticRetrievalToolV3({
601
568
  contexts: [],
602
569
  rerankers: [],
@@ -612,38 +579,60 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
612
579
  name: instance.name,
613
580
  description: instance.description,
614
581
  category: instance.category,
615
- config: tool.config
582
+ config: tool2.config
616
583
  };
617
584
  }
618
- if (tool.type === "agent") {
619
- if (tool.id === result.id) {
585
+ if (tool2.type === "agent") {
586
+ if (tool2.id === result.id) {
620
587
  return null;
621
588
  }
622
- const instance = await exuluApp.get().agent(tool.id);
589
+ const instance = await exuluApp.get().agent(tool2.id);
623
590
  if (!instance) {
624
591
  throw new Error(
625
- "Trying to load a tool of type 'agent', but the associated agent with id " + tool.id + " was not found in the database."
592
+ "Trying to load a tool of type 'agent', but the associated agent with id " + tool2.id + " was not found in the database."
626
593
  );
627
594
  }
628
- const provider2 = providers.find((a) => a.id === instance.provider);
629
- if (!provider2) {
595
+ if (!instance.model) {
630
596
  throw new Error(
631
- "Trying to load a tool of type 'agent', but the associated agent with id " + tool.id + " does not have a provider set for it."
597
+ "Trying to load a tool of type 'agent', but the associated agent with id " + tool2.id + " does not have a model set for it."
632
598
  );
633
599
  }
634
600
  const hasAccessToAgent = await checkRecordAccess(instance, "read", user);
635
601
  if (!hasAccessToAgent) {
636
602
  return null;
637
603
  }
638
- hydrated = await provider2.tool(instance.id, providers, contexts, rerankers);
604
+ if (isLiteLLMEnabled()) {
605
+ hydrated = {
606
+ id: instance.id,
607
+ name: instance.name,
608
+ description: `This tool calls an agent named: ${instance.name}. The agent does the following: ${instance.description ?? ""}.`,
609
+ type: "agent",
610
+ category: "agents"
611
+ };
612
+ } else {
613
+ const { db } = await postgresClient();
614
+ const innerModelRow = await db.from("models").where({ id: instance.model }).first();
615
+ const provider2 = innerModelRow?.provider ? providers.find((a) => a.id === innerModelRow.provider) : void 0;
616
+ if (!provider2) {
617
+ throw new Error(
618
+ "Trying to load a tool of type 'agent', but the model referenced by agent with id " + tool2.id + " does not point at a registered ExuluProvider."
619
+ );
620
+ }
621
+ hydrated = await provider2.tool(
622
+ instance.id,
623
+ providers,
624
+ contexts,
625
+ rerankers
626
+ );
627
+ }
639
628
  } else {
640
- hydrated = tools.find((t) => t.id === tool.id);
629
+ hydrated = tools.find((t) => t.id === tool2.id);
641
630
  }
642
631
  const hydratedTool = {
643
- ...tool,
632
+ ...tool2,
644
633
  name: hydrated?.name || "",
645
634
  description: hydrated?.description || "",
646
- category: tool?.category || "default"
635
+ category: tool2?.category || "default"
647
636
  };
648
637
  console.log("[EXULU] hydratedTool", hydratedTool);
649
638
  return hydratedTool;
@@ -661,28 +650,42 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
661
650
  result.tools.unshift(projectTool);
662
651
  }
663
652
  }
664
- result.tools = result.tools.filter((tool) => tool !== null);
653
+ result.tools = result.tools.filter((tool2) => tool2 !== null);
665
654
  } else {
666
655
  result.tools = [];
667
656
  }
668
657
  }
669
658
  if (requestedFields.includes("streaming")) {
670
- result.streaming = provider?.streaming || false;
659
+ result.streaming = isLiteLLMEnabled() ? true : provider?.streaming || false;
671
660
  }
672
661
  if (requestedFields.includes("capabilities")) {
673
- result.capabilities = provider?.capabilities || [];
662
+ if (isLiteLLMEnabled()) {
663
+ result.capabilities = {
664
+ text: true,
665
+ images: litellmEntry?.supports_vision ? [".png", ".jpg", ".jpeg", ".webp", ".gif"] : [],
666
+ files: litellmEntry?.supports_pdf_input ? [".pdf"] : [],
667
+ audio: litellmEntry?.supports_audio_input ? [".mp3", ".wav", ".m4a"] : [],
668
+ video: []
669
+ };
670
+ } else {
671
+ result.capabilities = provider?.capabilities || [];
672
+ }
674
673
  }
675
674
  if (requestedFields.includes("maxContextLength")) {
676
- result.maxContextLength = provider?.maxContextLength || 0;
675
+ if (isLiteLLMEnabled()) {
676
+ result.maxContextLength = litellmEntry?.max_input_tokens ?? litellmEntry?.max_tokens ?? 0;
677
+ } else {
678
+ result.maxContextLength = provider?.maxContextLength || 0;
679
+ }
677
680
  }
678
681
  if (requestedFields.includes("authenticationInformation")) {
679
- result.authenticationInformation = provider?.authenticationInformation || "";
682
+ result.authenticationInformation = isLiteLLMEnabled() ? "" : provider?.authenticationInformation || "";
680
683
  }
681
684
  if (requestedFields.includes("provider")) {
682
- result.provider = provider?.provider || "";
685
+ result.provider = isLiteLLMEnabled() ? "litellm" : provider?.provider || "";
683
686
  }
684
687
  if (requestedFields.includes("systemInstructions")) {
685
- result.systemInstructions = provider?.config?.instructions || void 0;
688
+ result.systemInstructions = isLiteLLMEnabled() ? void 0 : provider?.config?.instructions || void 0;
686
689
  }
687
690
  if (!requestedFields.includes("provider")) {
688
691
  delete result.provider;
@@ -690,7 +693,7 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
690
693
  if (requestedFields.includes("workflows")) {
691
694
  let enabled = false;
692
695
  let queueName = void 0;
693
- if (provider?.workflows) {
696
+ if (!isLiteLLMEnabled() && provider?.workflows) {
694
697
  enabled = provider?.workflows?.enabled || false;
695
698
  if (provider?.workflows?.queue) {
696
699
  const queue = await provider?.workflows?.queue;
@@ -859,7 +862,7 @@ var itemsPaginationRequest = async ({
859
862
  };
860
863
  var removeProviderFields = (requestedFields) => {
861
864
  const filtered = requestedFields.filter((field) => !exuluProviderFields.includes(field));
862
- filtered.push("provider");
865
+ filtered.push("model");
863
866
  return filtered;
864
867
  };
865
868
  var sanitizeRequestedFields = (table, requestedFields) => {
@@ -2167,7 +2170,7 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, allRerankers, di
2167
2170
  }
2168
2171
  console.log("[EXULU] available tools", enabledTools?.length);
2169
2172
  console.log("[EXULU] disabled tools", disabledTools?.length);
2170
- enabledTools = enabledTools.filter((tool) => !disabledTools.includes(tool.id));
2173
+ enabledTools = enabledTools.filter((tool2) => !disabledTools.includes(tool2.id));
2171
2174
  return enabledTools;
2172
2175
  };
2173
2176
 
@@ -2175,7 +2178,7 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, allRerankers, di
2175
2178
  import "@opentelemetry/api";
2176
2179
  import { v4 as uuidv4 } from "uuid";
2177
2180
  import "ai";
2178
- import CryptoJS2 from "crypto-js";
2181
+ import "crypto-js";
2179
2182
  var redisConnection;
2180
2183
  var unhandledRejectionHandlerInstalled = false;
2181
2184
  var poolMonitoringInterval;
@@ -2312,7 +2315,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2312
2315
  );
2313
2316
  if (attempt < retries) {
2314
2317
  const backoffMs = 500 * Math.pow(2, attempt - 1);
2315
- await new Promise((resolve3) => setTimeout(resolve3, backoffMs));
2318
+ await new Promise((resolve4) => setTimeout(resolve4, backoffMs));
2316
2319
  }
2317
2320
  }
2318
2321
  }
@@ -2516,7 +2519,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2516
2519
  } = await validateWorkflowPayload(data, providers);
2517
2520
  const retries2 = 3;
2518
2521
  let attempts = 0;
2519
- const promise = new Promise(async (resolve3, reject) => {
2522
+ const promise = new Promise(async (resolve4, reject) => {
2520
2523
  while (attempts < retries2) {
2521
2524
  try {
2522
2525
  const messages2 = await processUiMessagesFlow({
@@ -2531,7 +2534,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2531
2534
  config,
2532
2535
  variables: data.inputs
2533
2536
  });
2534
- resolve3(messages2);
2537
+ resolve4(messages2);
2535
2538
  break;
2536
2539
  } catch (error) {
2537
2540
  console.error(
@@ -2542,7 +2545,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2542
2545
  if (attempts >= retries2) {
2543
2546
  reject(new Error(error instanceof Error ? error.message : String(error)));
2544
2547
  }
2545
- await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
2548
+ await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
2546
2549
  }
2547
2550
  }
2548
2551
  });
@@ -2592,7 +2595,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2592
2595
  } = await validateEvalPayload(data, providers);
2593
2596
  const retries2 = 3;
2594
2597
  let attempts = 0;
2595
- const promise = new Promise(async (resolve3, reject) => {
2598
+ const promise = new Promise(async (resolve4, reject) => {
2596
2599
  while (attempts < retries2) {
2597
2600
  try {
2598
2601
  const messages2 = await processUiMessagesFlow({
@@ -2606,7 +2609,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2606
2609
  tools,
2607
2610
  config
2608
2611
  });
2609
- resolve3(messages2);
2612
+ resolve4(messages2);
2610
2613
  break;
2611
2614
  } catch (error) {
2612
2615
  console.error(
@@ -2617,7 +2620,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
2617
2620
  if (attempts >= retries2) {
2618
2621
  reject(new Error(error instanceof Error ? error.message : String(error)));
2619
2622
  }
2620
- await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
2623
+ await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
2621
2624
  }
2622
2625
  }
2623
2626
  });
@@ -3092,7 +3095,7 @@ var pollJobResult = async ({
3092
3095
  attempts++;
3093
3096
  const job = await Job.fromId(queue.queue, jobId);
3094
3097
  if (!job) {
3095
- await new Promise((resolve3) => setTimeout((resolve4) => resolve4(true), 2e3));
3098
+ await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
3096
3099
  continue;
3097
3100
  }
3098
3101
  const elapsedTime = Date.now() - startTime;
@@ -3122,7 +3125,7 @@ var pollJobResult = async ({
3122
3125
  console.log(`[EXULU] eval function ${job.id} result: ${result}`);
3123
3126
  break;
3124
3127
  }
3125
- await new Promise((resolve3) => setTimeout(() => resolve3(true), 2e3));
3128
+ await new Promise((resolve4) => setTimeout(() => resolve4(true), 2e3));
3126
3129
  }
3127
3130
  return result;
3128
3131
  };
@@ -3158,27 +3161,19 @@ var processUiMessagesFlow = async ({
3158
3161
  "[EXULU] enabled tools",
3159
3162
  enabledTools?.map((x) => x.name + " (" + x.id + ")")
3160
3163
  );
3161
- const variableName = agent.providerapikey;
3162
- const { db } = await postgresClient();
3163
- let providerapikey;
3164
- if (variableName) {
3165
- const variable = await db.from("variables").where({ name: variableName }).first();
3166
- if (!variable) {
3167
- throw new Error(
3168
- `Provider API key variable not found for agent ${agent.name} (${agent.id}).`
3169
- );
3170
- }
3171
- providerapikey = variable.value;
3172
- if (!variable.encrypted) {
3173
- throw new Error(
3174
- `Provider API key variable not encrypted for agent ${agent.name} (${agent.id}), for security reasons you are only allowed to use encrypted variables for provider API keys.`
3175
- );
3176
- }
3177
- if (variable.encrypted) {
3178
- const bytes = CryptoJS2.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
3179
- providerapikey = bytes.toString(CryptoJS2.enc.Utf8);
3180
- }
3164
+ if (!agent.model) {
3165
+ throw new Error(
3166
+ `Agent ${agent.name} (${agent.id}) has no model configured.`
3167
+ );
3181
3168
  }
3169
+ const resolved = await resolveModel({
3170
+ modelId: agent.model,
3171
+ user,
3172
+ providers,
3173
+ agent: { id: agent.id }
3174
+ });
3175
+ const providerapikey = resolved.apiKey;
3176
+ const resolvedLanguageModel = resolved.languageModel;
3182
3177
  const messagesWithoutPlaceholder = inputMessages.filter(
3183
3178
  (message) => message.metadata?.type !== "placeholder"
3184
3179
  );
@@ -3208,18 +3203,18 @@ var processUiMessagesFlow = async ({
3208
3203
  const text = part.text;
3209
3204
  const variableNames = [...text.matchAll(/{([^}]+)}/g)].map((match) => match[1]);
3210
3205
  if (variableNames) {
3211
- for (const variableName2 of variableNames) {
3212
- if (!variableName2) {
3206
+ for (const variableName of variableNames) {
3207
+ if (!variableName) {
3213
3208
  continue;
3214
3209
  }
3215
- console.log("[EXULU] variableName", variableName2);
3216
- const variableValue = variables?.[variableName2];
3210
+ console.log("[EXULU] variableName", variableName);
3211
+ const variableValue = variables?.[variableName];
3217
3212
  console.log("[EXULU] variableValue", variableValue);
3218
3213
  if (variableValue) {
3219
- part.text = part.text.replaceAll(`{${variableName2}}`, variableValue);
3214
+ part.text = part.text.replaceAll(`{${variableName}}`, variableValue);
3220
3215
  } else {
3221
3216
  throw new Error(
3222
- `Value for variable ${variableName2} not provided in variables for processing message flow. Either remove it from the messages, or provide it as an argument.`
3217
+ `Value for variable ${variableName} not provided in variables for processing message flow. Either remove it from the messages, or provide it as an argument.`
3223
3218
  );
3224
3219
  }
3225
3220
  }
@@ -3230,7 +3225,7 @@ var processUiMessagesFlow = async ({
3230
3225
  label: agent.name,
3231
3226
  trigger: "agent"
3232
3227
  };
3233
- messageHistory = await new Promise(async (resolve3, reject) => {
3228
+ messageHistory = await new Promise(async (resolve4, reject) => {
3234
3229
  const startTime = Date.now();
3235
3230
  try {
3236
3231
  const result = await provider.generateStream({
@@ -3238,13 +3233,14 @@ var processUiMessagesFlow = async ({
3238
3233
  rerankers,
3239
3234
  agent,
3240
3235
  user,
3241
- approvedTools: tools.map((tool) => "tool-" + sanitizeToolName(tool.name)),
3236
+ approvedTools: tools.map((tool2) => "tool-" + sanitizeToolName(tool2.name)),
3242
3237
  instructions: agent.instructions,
3243
3238
  session: void 0,
3244
3239
  previousMessages: messageHistory.messages,
3245
3240
  message: currentMessage,
3246
3241
  currentTools: enabledTools,
3247
3242
  allExuluTools: tools,
3243
+ languageModel: resolvedLanguageModel,
3248
3244
  providerapikey,
3249
3245
  toolConfigs: agent.tools,
3250
3246
  exuluConfig: config
@@ -3307,7 +3303,7 @@ var processUiMessagesFlow = async ({
3307
3303
  })
3308
3304
  ] : []
3309
3305
  ]);
3310
- resolve3({
3306
+ resolve4({
3311
3307
  messages,
3312
3308
  metadata: {
3313
3309
  tokens: {
@@ -3361,6 +3357,7 @@ function getAverage(arr) {
3361
3357
  }
3362
3358
 
3363
3359
  // src/graphql/schemas/index.ts
3360
+ import "fs";
3364
3361
  function createExuluContextsTypeDefs(table) {
3365
3362
  const enumDefs = table.fields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
3366
3363
  if (!field.enumValues) {
@@ -3730,6 +3727,9 @@ type PageInfo {
3730
3727
  typeDefs += `
3731
3728
  providers: ProviderPaginationResult
3732
3729
  `;
3730
+ typeDefs += `
3731
+ litellmCatalog: [LiteLLMModel!]!
3732
+ `;
3733
3733
  typeDefs += `
3734
3734
  workflowSchedule(workflow: ID!): WorkflowScheduleResult
3735
3735
  `;
@@ -3793,6 +3793,24 @@ type RateLimitUsageRow {
3793
3793
  inputTokens: Int!
3794
3794
  outputTokens: Int!
3795
3795
  }
3796
+ `;
3797
+ modelDefs += `
3798
+ type LiteLLMModel {
3799
+ model_name: String!
3800
+ upstream_model: String
3801
+ active: Boolean
3802
+ tags: [String!]
3803
+ type: String
3804
+ brand: String
3805
+ region: String
3806
+ max_tokens: Int
3807
+ max_input_tokens: Int
3808
+ max_output_tokens: Int
3809
+ supports_vision: Boolean
3810
+ supports_function_calling: Boolean
3811
+ supports_pdf_input: Boolean
3812
+ supports_audio_input: Boolean
3813
+ }
3796
3814
  `;
3797
3815
  resolvers.Query["agentRateLimitUsage"] = async (_, args, context) => {
3798
3816
  if (!checkLicense()["rate-limits"]) return [];
@@ -3846,6 +3864,10 @@ type RateLimitUsageRow {
3846
3864
  })
3847
3865
  };
3848
3866
  };
3867
+ resolvers.Query["litellmCatalog"] = async () => {
3868
+ const { fetchLiteLLMCatalog } = await import("./catalog-EOKGOHTY.js");
3869
+ return fetchLiteLLMCatalog();
3870
+ };
3849
3871
  resolvers.Query["workflowSchedule"] = async (_, args, context, info) => {
3850
3872
  if (!args.workflow) {
3851
3873
  throw new Error("Workflow template ID is required");
@@ -3866,10 +3888,10 @@ type RateLimitUsageRow {
3866
3888
  if (!agent) {
3867
3889
  throw new Error("Agent instance not found for workflow template.");
3868
3890
  }
3869
- const provider = providers.find((provider2) => provider2.id === agent.provider);
3891
+ const provider = await resolveAgentProvider(agent, providers);
3870
3892
  if (!provider) {
3871
3893
  throw new Error(
3872
- "Agent provider: " + agent.provider + " not found for agent instance " + agent.id + "."
3894
+ "ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
3873
3895
  );
3874
3896
  }
3875
3897
  let queue;
@@ -3941,10 +3963,10 @@ type RateLimitUsageRow {
3941
3963
  if (!agent) {
3942
3964
  throw new Error("Agent instance not found for workflow template.");
3943
3965
  }
3944
- const provider = providers.find((provider2) => provider2.id === agent.provider);
3966
+ const provider = await resolveAgentProvider(agent, providers);
3945
3967
  if (!provider) {
3946
3968
  throw new Error(
3947
- "Agent provider: " + agent.provider + " not found for agent instance " + agent.id + "."
3969
+ "ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
3948
3970
  );
3949
3971
  }
3950
3972
  let queue;
@@ -3982,10 +4004,10 @@ type RateLimitUsageRow {
3982
4004
  if (!agent) {
3983
4005
  throw new Error("Agent instance not found for workflow template.");
3984
4006
  }
3985
- const provider = providers.find((provider2) => provider2.id === agent.provider);
4007
+ const provider = await resolveAgentProvider(agent, providers);
3986
4008
  if (!provider) {
3987
4009
  throw new Error(
3988
- "Provider: " + agent.provider + " not found for agent instance " + agent.id + "."
4010
+ "ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
3989
4011
  );
3990
4012
  }
3991
4013
  let queue;
@@ -4046,10 +4068,10 @@ type RateLimitUsageRow {
4046
4068
  if (!agent) {
4047
4069
  throw new Error("Agent instance not found for workflow template.");
4048
4070
  }
4049
- const provider = providers.find((provider2) => provider2.id === agent.provider);
4071
+ const provider = await resolveAgentProvider(agent, providers);
4050
4072
  if (!provider) {
4051
4073
  throw new Error(
4052
- "Agent provider: " + agent.provider + " not found for agent instance " + agent.id + "."
4074
+ "ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
4053
4075
  );
4054
4076
  }
4055
4077
  let queue;
@@ -4107,7 +4129,7 @@ type RateLimitUsageRow {
4107
4129
  } = await validateWorkflowPayload(jobData, providers);
4108
4130
  const retries = 3;
4109
4131
  let attempts = 0;
4110
- const promise = new Promise(async (resolve3, reject) => {
4132
+ const promise = new Promise(async (resolve4, reject) => {
4111
4133
  while (attempts < retries) {
4112
4134
  try {
4113
4135
  const messages2 = await processUiMessagesFlow({
@@ -4122,7 +4144,7 @@ type RateLimitUsageRow {
4122
4144
  config,
4123
4145
  variables: args.variables
4124
4146
  });
4125
- resolve3(messages2);
4147
+ resolve4(messages2);
4126
4148
  break;
4127
4149
  } catch (error) {
4128
4150
  console.error(
@@ -4136,7 +4158,7 @@ type RateLimitUsageRow {
4136
4158
  if (attempts >= retries) {
4137
4159
  reject(error instanceof Error ? error : new Error(String(error)));
4138
4160
  }
4139
- await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
4161
+ await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
4140
4162
  }
4141
4163
  }
4142
4164
  });
@@ -4389,10 +4411,10 @@ type RateLimitUsageRow {
4389
4411
  contexts.map(async (context2) => {
4390
4412
  let processor = null;
4391
4413
  if (context2.processor) {
4392
- processor = await new Promise(async (resolve3, reject) => {
4414
+ processor = await new Promise(async (resolve4, reject) => {
4393
4415
  const config2 = context2.processor?.config;
4394
4416
  const queue = await config2?.queue;
4395
- resolve3({
4417
+ resolve4({
4396
4418
  name: context2.processor.name,
4397
4419
  description: context2.processor.description,
4398
4420
  queue: queue?.queue?.name || void 0,
@@ -4473,10 +4495,10 @@ type RateLimitUsageRow {
4473
4495
  }
4474
4496
  let processor = null;
4475
4497
  if (data.processor) {
4476
- processor = await new Promise(async (resolve3, reject) => {
4498
+ processor = await new Promise(async (resolve4, reject) => {
4477
4499
  const config2 = data.processor?.config;
4478
4500
  const queue = await config2?.queue;
4479
- resolve3({
4501
+ resolve4({
4480
4502
  name: data.processor.name,
4481
4503
  description: data.processor.description,
4482
4504
  queue: queue?.queue?.name || void 0,
@@ -4557,7 +4579,7 @@ type RateLimitUsageRow {
4557
4579
  const instances = await exuluApp.get().agents();
4558
4580
  let agentTools = await Promise.all(
4559
4581
  instances.map(async (agent) => {
4560
- const provider = providers.find((a) => a.id === agent.provider);
4582
+ const provider = await resolveAgentProvider(agent, providers);
4561
4583
  if (!provider) {
4562
4584
  return null;
4563
4585
  }
@@ -4565,7 +4587,7 @@ type RateLimitUsageRow {
4565
4587
  })
4566
4588
  );
4567
4589
  let agenticRetrievalTool = void 0;
4568
- const filtered = agentTools.filter((tool) => tool !== null);
4590
+ const filtered = agentTools.filter((tool2) => tool2 !== null);
4569
4591
  let allTools = [...filtered, ...tools];
4570
4592
  if (contexts?.length) {
4571
4593
  agenticRetrievalTool = createAgenticRetrievalToolV3({
@@ -4583,21 +4605,21 @@ type RateLimitUsageRow {
4583
4605
  if (search && search.trim()) {
4584
4606
  const searchTerm = search.toLowerCase().trim();
4585
4607
  allTools = allTools.filter(
4586
- (tool) => tool.name?.toLowerCase().includes(searchTerm) || tool.description?.toLowerCase().includes(searchTerm)
4608
+ (tool2) => tool2.name?.toLowerCase().includes(searchTerm) || tool2.description?.toLowerCase().includes(searchTerm)
4587
4609
  );
4588
4610
  }
4589
4611
  if (category && category.trim()) {
4590
- allTools = allTools.filter((tool) => tool.category === category);
4612
+ allTools = allTools.filter((tool2) => tool2.category === category);
4591
4613
  }
4592
4614
  const total = allTools.length;
4593
4615
  const start = page * limit;
4594
4616
  const end = start + limit;
4595
4617
  const paginatedTools = allTools.slice(start, end);
4596
4618
  return {
4597
- items: paginatedTools.map((tool) => {
4619
+ items: paginatedTools.map((tool2) => {
4598
4620
  const object = {};
4599
4621
  requestedFields.forEach((field) => {
4600
- object[field] = tool[field];
4622
+ object[field] = tool2[field];
4601
4623
  });
4602
4624
  return object;
4603
4625
  }),
@@ -4607,7 +4629,7 @@ type RateLimitUsageRow {
4607
4629
  };
4608
4630
  };
4609
4631
  resolvers.Query["toolCategories"] = async () => {
4610
- const array = tools.map((tool) => tool.category).filter((category) => category && typeof category === "string");
4632
+ const array = tools.map((tool2) => tool2.category).filter((category) => category && typeof category === "string");
4611
4633
  array.push("contexts");
4612
4634
  array.push("agents");
4613
4635
  return [...new Set(array)].sort();
@@ -4798,6 +4820,9 @@ type Provider {
4798
4820
  provider: String
4799
4821
  modelName: String
4800
4822
  type: EnumProviderType!
4823
+ authenticationInformation: String
4824
+ maxContextLength: Int
4825
+ capabilities: JSON
4801
4826
  }
4802
4827
 
4803
4828
  type Eval {
@@ -5158,37 +5183,10 @@ import { InMemoryLRUCache } from "@apollo/utils.keyvaluecache";
5158
5183
  import bodyParser from "body-parser";
5159
5184
  import CryptoJS5 from "crypto-js";
5160
5185
  import OpenAI from "openai";
5161
- import fs2 from "fs";
5186
+ import fs3 from "fs";
5162
5187
  import { randomUUID as randomUUID2 } from "crypto";
5163
5188
  import "@opentelemetry/api";
5164
- import Anthropic from "@anthropic-ai/sdk";
5165
-
5166
- // src/utils/claude-messages.ts
5167
- var CLAUDE_MESSAGES = {
5168
- anthropic_token_variable_not_encrypted: `
5169
- \x1B[41m -- Anthropic token variable set by your admin is not encrypted. This poses a security risk. Please contact your admin to fix the variable used for your key. --
5170
- \x1B[0m`,
5171
- anthropic_token_variable_not_found: `
5172
- \x1B[41m -- Anthropic token variable not found. Please contact your Exulu adminto fix the variable used for the key. --
5173
- \x1B[0m`,
5174
- authentication_error: `
5175
- \x1B[41m -- Authentication error please check your IMP token and try again. --
5176
- \x1B[0m`,
5177
- missing_body: `
5178
- \x1B[41m -- Missing body Anthropic response. --
5179
- \x1B[0m`,
5180
- missing_nextauth_secret: `
5181
- \x1B[41m -- Missing NEXTAUTH_SECRET in environment variables on the server. --
5182
- \x1B[0m`,
5183
- not_enabled: `
5184
- \x1B[41m -- The agent you selected does not have a valid API key set for it. --
5185
- \x1B[0m`,
5186
- missing_project: `
5187
- \x1B[41m -- Project not found or you do not have access to it. --
5188
- \x1B[0m`
5189
- };
5190
-
5191
- // src/exulu/routes.ts
5189
+ import JSZip2 from "jszip";
5192
5190
  import { createIdGenerator } from "ai";
5193
5191
  import cookieParser from "cookie-parser";
5194
5192
 
@@ -5196,7 +5194,6 @@ import cookieParser from "cookie-parser";
5196
5194
  import { z } from "zod";
5197
5195
  import {
5198
5196
  convertToModelMessages,
5199
- Output,
5200
5197
  generateText as generateText2,
5201
5198
  streamText,
5202
5199
  validateUIMessages,
@@ -5212,7 +5209,7 @@ function generateSlug(name) {
5212
5209
  }
5213
5210
 
5214
5211
  // src/exulu/provider.ts
5215
- import CryptoJS3 from "crypto-js";
5212
+ import "crypto-js";
5216
5213
  import { parseOfficeAsync } from "officeparser";
5217
5214
 
5218
5215
  // src/exulu/task-description.ts
@@ -5258,7 +5255,7 @@ async function clearSessionCurrentTask(session) {
5258
5255
  }
5259
5256
 
5260
5257
  // src/exulu/provider.ts
5261
- import fs from "fs";
5258
+ import fs2 from "fs";
5262
5259
  var ExuluProvider = class {
5263
5260
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
5264
5261
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -5360,27 +5357,18 @@ var ExuluProvider = class {
5360
5357
  providers,
5361
5358
  user
5362
5359
  );
5363
- const variableName = agent.providerapikey;
5364
- let providerapikey;
5365
- if (variableName) {
5366
- const { db } = await postgresClient();
5367
- const variable = await db.from("variables").where({ name: variableName }).first();
5368
- if (!variable) {
5369
- throw new Error(
5370
- "Provider API key variable not found for agent: " + agent.name + " (" + agent.id + ") being called as a tool."
5371
- );
5372
- }
5373
- providerapikey = variable.value;
5374
- if (!variable.encrypted) {
5375
- throw new Error(
5376
- "Provider API key variable not encrypted for agent: " + agent.name + " (" + agent.id + ") being called as a tool, for security reasons you are only allowed to use encrypted variables for provider API keys."
5377
- );
5378
- }
5379
- if (variable.encrypted) {
5380
- const bytes = CryptoJS3.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
5381
- providerapikey = bytes.toString(CryptoJS3.enc.Utf8);
5382
- }
5360
+ if (!agent.model) {
5361
+ throw new Error(
5362
+ `Agent ${agent.name} (${agent.id}) has no model configured (called as a tool).`
5363
+ );
5383
5364
  }
5365
+ const resolved = await resolveModel({
5366
+ modelId: agent.model,
5367
+ user,
5368
+ providers,
5369
+ agent: { id: agent.id }
5370
+ });
5371
+ const providerapikey = resolved.apiKey;
5384
5372
  console.log(
5385
5373
  "[EXULU] Enabled tools for agent '" + agent.name + " (" + agent.id + ") that is being called as a tool",
5386
5374
  enabledTools.map((x) => x.name + " (" + x.id + ")")
@@ -5399,6 +5387,7 @@ var ExuluProvider = class {
5399
5387
  rerankers,
5400
5388
  instructions: agent.instructions,
5401
5389
  prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
5390
+ languageModel: resolved.languageModel,
5402
5391
  providerapikey,
5403
5392
  user,
5404
5393
  currentTools: enabledTools,
@@ -5436,10 +5425,10 @@ var ExuluProvider = class {
5436
5425
  statistics,
5437
5426
  toolConfigs,
5438
5427
  providerapikey,
5428
+ languageModel,
5439
5429
  contexts,
5440
5430
  rerankers,
5441
5431
  exuluConfig,
5442
- outputSchema,
5443
5432
  agent,
5444
5433
  instructions,
5445
5434
  maxStepCount,
@@ -5449,9 +5438,6 @@ var ExuluProvider = class {
5449
5438
  "[EXULU] Called generate sync for agent: " + this.name,
5450
5439
  "with prompt: " + prompt?.slice(0, 100) + "..."
5451
5440
  );
5452
- if (!this.model) {
5453
- throw new Error("Model is required for streaming.");
5454
- }
5455
5441
  if (!this.config) {
5456
5442
  throw new Error("Config is required for generating.");
5457
5443
  }
@@ -5468,13 +5454,7 @@ var ExuluProvider = class {
5468
5454
  sessionItems = sessionData.session_items;
5469
5455
  project = sessionData.project;
5470
5456
  }
5471
- const model = this.model.create({
5472
- ...providerapikey ? { apiKey: providerapikey } : {},
5473
- user: user?.id,
5474
- role: user?.role?.id,
5475
- project,
5476
- agent: agent?.id
5477
- });
5457
+ const model = languageModel;
5478
5458
  console.log("[EXULU] Model for agent: " + this.name, " created for generating sync.");
5479
5459
  let messages = inputMessages || [];
5480
5460
  if (messages && session && user) {
@@ -5553,12 +5533,12 @@ var ExuluProvider = class {
5553
5533
  system += "\n\n" + memoryContext;
5554
5534
  }
5555
5535
  const includesContextSearchTool = currentTools?.some(
5556
- (tool) => tool.name.toLowerCase().includes("context_search") || tool.id.includes("context_search") || tool.type === "context"
5536
+ (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
5557
5537
  );
5558
5538
  const includesWebSearchTool = currentTools?.some(
5559
- (tool) => tool.name.toLowerCase().includes("web_search") || tool.id.includes("web_search") || tool.type === "web_search"
5539
+ (tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
5560
5540
  );
5561
- console.log("[EXULU] Current tools: " + currentTools?.map((tool) => tool.name).join("\n"));
5541
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
5562
5542
  console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
5563
5543
  if (includesContextSearchTool) {
5564
5544
  system += `
@@ -5617,66 +5597,47 @@ When a tool execution is not approved by the user, do not retry it unless explic
5617
5597
  let result = { object: null, text: "" };
5618
5598
  let inputTokens = 0;
5619
5599
  let outputTokens = 0;
5620
- if (outputSchema) {
5621
- const { output, usage } = await generateText2({
5622
- temperature: 0,
5623
- // TODO Make this configurable
5624
- model,
5625
- system,
5626
- maxRetries: 3,
5627
- output: Output.object({
5628
- schema: outputSchema
5629
- }),
5630
- prompt,
5631
- stopWhen: [stepCountIs(maxStepCount || 5)]
5632
- // make configurable
5633
- });
5634
- result.object = output;
5635
- inputTokens = usage.inputTokens || 0;
5636
- outputTokens = usage.outputTokens || 0;
5637
- } else {
5638
- console.log(
5639
- "[EXULU] Generating text for agent: " + this.name,
5640
- "with prompt: " + prompt?.slice(0, 100) + "..."
5641
- );
5642
- const output = await generateText2({
5643
- temperature: 0,
5644
- // TODO Make this configurable
5600
+ console.log(
5601
+ "[EXULU] Generating text for agent: " + this.name,
5602
+ "with prompt: " + prompt?.slice(0, 100) + "..."
5603
+ );
5604
+ const output = await generateText2({
5605
+ temperature: 0,
5606
+ // TODO Make this configurable
5607
+ model,
5608
+ system,
5609
+ prompt,
5610
+ maxRetries: 2,
5611
+ tools: await convertExuluToolsToAiSdkTools(
5612
+ currentTools,
5613
+ currentSkills,
5614
+ approvedTools,
5615
+ allExuluTools,
5616
+ toolConfigs,
5617
+ providerapikey,
5618
+ contexts,
5619
+ rerankers,
5620
+ user,
5621
+ exuluConfig,
5622
+ session,
5623
+ req,
5624
+ project,
5625
+ sessionItems,
5645
5626
  model,
5646
- system,
5647
- prompt,
5648
- maxRetries: 2,
5649
- tools: await convertExuluToolsToAiSdkTools(
5650
- currentTools,
5651
- currentSkills,
5652
- approvedTools,
5653
- allExuluTools,
5654
- toolConfigs,
5655
- providerapikey,
5656
- contexts,
5657
- rerankers,
5658
- user,
5659
- exuluConfig,
5660
- session,
5661
- req,
5662
- project,
5663
- sessionItems,
5664
- model,
5665
- agent,
5666
- memoryItems
5667
- ),
5668
- stopWhen: [stepCountIs(maxStepCount || 5)]
5669
- // make configurable
5670
- });
5671
- console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
5672
- const {
5673
- text,
5674
- totalUsage
5675
- } = output;
5676
- result.text = text;
5677
- inputTokens = totalUsage?.inputTokens || 0;
5678
- outputTokens = totalUsage?.outputTokens || 0;
5679
- }
5627
+ agent,
5628
+ memoryItems
5629
+ ),
5630
+ stopWhen: [stepCountIs(maxStepCount || 5)]
5631
+ // make configurable
5632
+ });
5633
+ console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
5634
+ const {
5635
+ text,
5636
+ totalUsage
5637
+ } = output;
5638
+ result.text = text;
5639
+ inputTokens = totalUsage?.inputTokens || 0;
5640
+ outputTokens = totalUsage?.outputTokens || 0;
5680
5641
  if (statistics) {
5681
5642
  await Promise.all([
5682
5643
  updateStatistic({
@@ -5881,6 +5842,7 @@ ${extractedText}
5881
5842
  allExuluTools,
5882
5843
  toolConfigs,
5883
5844
  providerapikey,
5845
+ languageModel,
5884
5846
  contexts,
5885
5847
  rerankers,
5886
5848
  exuluConfig,
@@ -5888,10 +5850,6 @@ ${extractedText}
5888
5850
  req,
5889
5851
  maxStepCount
5890
5852
  }) => {
5891
- if (!this.model) {
5892
- console.error("[EXULU] Model is required for streaming.");
5893
- throw new Error("Model is required for streaming.");
5894
- }
5895
5853
  if (!this.config) {
5896
5854
  console.error("[EXULU] Config is required for streaming.");
5897
5855
  throw new Error("Config is required for generating.");
@@ -5917,13 +5875,7 @@ ${extractedText}
5917
5875
  });
5918
5876
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
5919
5877
  }
5920
- const model = this.model.create({
5921
- ...providerapikey ? { apiKey: providerapikey } : {},
5922
- user: user?.id,
5923
- role: user?.role?.id,
5924
- project,
5925
- agent: agent?.id
5926
- });
5878
+ const model = languageModel;
5927
5879
  messages = await validateUIMessages({
5928
5880
  // append the new message to the previous messages:
5929
5881
  messages: [...previousMessagesContent, message]
@@ -5960,7 +5912,7 @@ ${extractedText}
5960
5912
  // todo make this configurable?
5961
5913
  page: 1
5962
5914
  });
5963
- fs.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
5915
+ fs2.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
5964
5916
  if (result2?.chunks?.length) {
5965
5917
  memoryItems = result2.chunks;
5966
5918
  memoryContext = `
@@ -5983,12 +5935,12 @@ ${extractedText}
5983
5935
  let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
5984
5936
  system += "\n\n" + genericContext;
5985
5937
  const includesContextSearchTool = currentTools?.some(
5986
- (tool) => tool.name.toLowerCase().includes("context_search") || tool.id.includes("context_search") || tool.type === "context"
5938
+ (tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
5987
5939
  );
5988
5940
  const includesWebSearchTool = currentTools?.some(
5989
- (tool) => tool.name.toLowerCase().includes("web_search") || tool.id.includes("web_search") || tool.type === "web_search"
5941
+ (tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
5990
5942
  );
5991
- console.log("[EXULU] Current tools: " + currentTools?.map((tool) => tool.name).join("\n"));
5943
+ console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
5992
5944
  console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
5993
5945
  console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
5994
5946
  if (includesContextSearchTool) {
@@ -6088,7 +6040,7 @@ ${skillsList}
6088
6040
 
6089
6041
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
6090
6042
  'Inform the user that the action was not performed.`;
6091
- fs.writeFileSync("system-prompt.txt", system);
6043
+ fs2.writeFileSync("system-prompt.txt", system);
6092
6044
  console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
6093
6045
  console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
6094
6046
  const tools = await convertExuluToolsToAiSdkTools(
@@ -6175,7 +6127,8 @@ var getSession = async ({ sessionID }) => {
6175
6127
  var saveChat = async ({
6176
6128
  session,
6177
6129
  user,
6178
- messages
6130
+ messages,
6131
+ model
6179
6132
  }) => {
6180
6133
  const { db } = await postgresClient();
6181
6134
  for (const message of messages) {
@@ -6184,13 +6137,168 @@ var saveChat = async ({
6184
6137
  user,
6185
6138
  content: JSON.stringify(message),
6186
6139
  message_id: message.id,
6187
- title: message.role === "user" ? "User" : "Assistant"
6140
+ title: message.role === "user" ? "User" : "Assistant",
6141
+ ...model ? { model } : {}
6188
6142
  }).returning("id");
6189
6143
  mutation.onConflict("message_id").merge();
6190
6144
  await mutation;
6191
6145
  }
6192
6146
  };
6193
6147
 
6148
+ // src/exulu/suggestions.ts
6149
+ import {
6150
+ convertToModelMessages as convertToModelMessages2,
6151
+ generateText as generateText3,
6152
+ tool
6153
+ } from "ai";
6154
+ import { z as z2 } from "zod";
6155
+ var SUGGESTIONS_SYSTEM_PROMPT = "You generate short follow-up message suggestions for the user. You are NOT continuing the conversation as the assistant \u2014 you are predicting what the user might want to say next. Suggest up to 3 short follow-up questions or messages the user might want to send next. Each suggestion must be written from the user's perspective (first person) and be 12 words or fewer. You MUST submit your answer by calling the `submit_suggestions` tool exactly once. Do not emit any plain text \u2014 only the tool call.";
6156
+ var submitSuggestionsTool = tool({
6157
+ description: "Submit the final list of follow-up message suggestions for the user. Must be called exactly once. Each suggestion is written from the user's perspective (first person) and is 12 words or fewer.",
6158
+ inputSchema: z2.object({
6159
+ suggestions: z2.array(z2.string()).max(3)
6160
+ })
6161
+ });
6162
+ var MAX_CHARS_PER_MESSAGE = 1e4;
6163
+ var trimMessagesForSuggestions = (messages) => {
6164
+ const out = [];
6165
+ for (const m of messages) {
6166
+ const textBlobs = [];
6167
+ for (const p of m.parts ?? []) {
6168
+ if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
6169
+ textBlobs.push(p.text);
6170
+ }
6171
+ }
6172
+ if (textBlobs.length === 0) continue;
6173
+ let combined = textBlobs.join("\n\n");
6174
+ if (combined.length > MAX_CHARS_PER_MESSAGE) {
6175
+ combined = combined.slice(0, MAX_CHARS_PER_MESSAGE);
6176
+ }
6177
+ out.push({
6178
+ ...m,
6179
+ parts: [{ type: "text", text: combined }]
6180
+ });
6181
+ }
6182
+ return out;
6183
+ };
6184
+ var generateSuggestions = async ({
6185
+ languageModel,
6186
+ messages,
6187
+ agentInstructions
6188
+ }) => {
6189
+ const system = agentInstructions ? `${agentInstructions}
6190
+
6191
+ ---
6192
+
6193
+ ${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
6194
+ const trimmed = trimMessagesForSuggestions(messages);
6195
+ const { toolCalls, totalUsage } = await generateText3({
6196
+ temperature: 0,
6197
+ model: languageModel,
6198
+ system,
6199
+ messages: await convertToModelMessages2(trimmed, {
6200
+ ignoreIncompleteToolCalls: true
6201
+ }),
6202
+ tools: { submit_suggestions: submitSuggestionsTool },
6203
+ toolChoice: { type: "tool", toolName: "submit_suggestions" },
6204
+ maxRetries: 3
6205
+ });
6206
+ const call = toolCalls.find((c) => c.toolName === "submit_suggestions");
6207
+ const input = call?.input;
6208
+ const suggestions = Array.isArray(input?.suggestions) ? input.suggestions.slice(0, 3).map(String) : [];
6209
+ return {
6210
+ suggestions,
6211
+ usage: {
6212
+ inputTokens: totalUsage?.inputTokens ?? 0,
6213
+ outputTokens: totalUsage?.outputTokens ?? 0
6214
+ }
6215
+ };
6216
+ };
6217
+
6218
+ // src/exulu/transcribe.ts
6219
+ var TranscriptionError = class extends Error {
6220
+ constructor(upstreamStatus, message) {
6221
+ super(message);
6222
+ this.upstreamStatus = upstreamStatus;
6223
+ this.name = "TranscriptionError";
6224
+ }
6225
+ };
6226
+ async function transcribeAudio(args) {
6227
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
6228
+ const port = process.env.LITELLM_PORT ?? "4000";
6229
+ const masterKey = process.env.LITELLM_MASTER_KEY;
6230
+ const model = process.env.TRANSCRIPTION_MODEL;
6231
+ if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
6232
+ if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
6233
+ const form = new FormData();
6234
+ form.append(
6235
+ "file",
6236
+ new Blob([args.file.buffer], { type: args.file.mimetype }),
6237
+ args.file.originalname
6238
+ );
6239
+ form.append("model", model);
6240
+ if (args.language) form.append("language", args.language);
6241
+ const res = await fetch(`http://${host}:${port}/v1/audio/transcriptions`, {
6242
+ method: "POST",
6243
+ headers: { Authorization: `Bearer ${masterKey}` },
6244
+ body: form
6245
+ });
6246
+ if (!res.ok) {
6247
+ const body = await res.text().catch(() => "");
6248
+ throw new TranscriptionError(
6249
+ res.status,
6250
+ `LiteLLM transcription failed (status ${res.status}): ${body}`.trim()
6251
+ );
6252
+ }
6253
+ const json = await res.json();
6254
+ return { text: typeof json.text === "string" ? json.text : "" };
6255
+ }
6256
+
6257
+ // src/exulu/speech.ts
6258
+ var SpeechError = class extends Error {
6259
+ constructor(upstreamStatus, message) {
6260
+ super(message);
6261
+ this.upstreamStatus = upstreamStatus;
6262
+ this.name = "SpeechError";
6263
+ }
6264
+ };
6265
+ async function synthesizeSpeech(args) {
6266
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
6267
+ const port = process.env.LITELLM_PORT ?? "4000";
6268
+ const masterKey = process.env.LITELLM_MASTER_KEY;
6269
+ const model = process.env.TTS_MODEL;
6270
+ const voice = process.env.TTS_VOICE;
6271
+ if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
6272
+ if (!model) throw new Error("TTS_MODEL is not set");
6273
+ if (!voice) throw new Error("TTS_VOICE is not set");
6274
+ const body = {
6275
+ model,
6276
+ input: args.text,
6277
+ voice,
6278
+ response_format: "mp3"
6279
+ };
6280
+ const res = await fetch(`http://${host}:${port}/v1/audio/speech`, {
6281
+ method: "POST",
6282
+ headers: {
6283
+ Authorization: `Bearer ${masterKey}`,
6284
+ "Content-Type": "application/json"
6285
+ },
6286
+ body: JSON.stringify(body)
6287
+ });
6288
+ if (!res.ok) {
6289
+ const text = await res.text().catch(() => "");
6290
+ throw new SpeechError(
6291
+ res.status,
6292
+ `LiteLLM speech failed (status ${res.status}): ${text}`.trim()
6293
+ );
6294
+ }
6295
+ const arrayBuf = await res.arrayBuffer();
6296
+ return Buffer.from(arrayBuf);
6297
+ }
6298
+
6299
+ // src/exulu/routes.ts
6300
+ import multer from "multer";
6301
+
6194
6302
  // src/utils/check-provider-rate-limit.ts
6195
6303
  var checkProviderRateLimit = async (provider) => {
6196
6304
  if (provider.rateLimit) {
@@ -6321,7 +6429,7 @@ async function recordAgentTokenUsage(args) {
6321
6429
  import "express";
6322
6430
  import {
6323
6431
  streamText as streamText2,
6324
- generateText as generateText3,
6432
+ generateText as generateText4,
6325
6433
  stepCountIs as stepCountIs2,
6326
6434
  jsonSchema
6327
6435
  } from "ai";
@@ -6412,7 +6520,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
6412
6520
 
6413
6521
  // src/exulu/openai-gateway.ts
6414
6522
  import { randomUUID } from "crypto";
6415
- import CryptoJS4 from "crypto-js";
6523
+ import "crypto-js";
6416
6524
  import express from "express";
6417
6525
  function convertOpenAIToolsToAiSdkTools(tools) {
6418
6526
  return Object.fromEntries(
@@ -6698,41 +6806,33 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
6698
6806
  res.status(500).json({ error: { message: "Server configuration error", type: "server_error" } });
6699
6807
  return;
6700
6808
  }
6701
- if (!agent.providerapikey) {
6702
- res.status(400).json({
6703
- error: { message: "Agent has no API key configured", type: "invalid_request_error" }
6704
- });
6705
- return;
6706
- }
6707
- const variable = await db.from("variables").where({ name: agent.providerapikey }).first();
6708
- if (!variable) {
6709
- res.status(400).json({
6710
- error: { message: "API key variable not found", type: "invalid_request_error" }
6711
- });
6712
- return;
6713
- }
6714
- if (!variable.encrypted) {
6809
+ if (!agent.model) {
6715
6810
  res.status(400).json({
6716
- error: { message: "API key variable must be encrypted", type: "invalid_request_error" }
6811
+ error: { message: "Agent has no model configured", type: "invalid_request_error" }
6717
6812
  });
6718
6813
  return;
6719
6814
  }
6720
- const bytes = CryptoJS4.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6721
- const providerapikey = bytes.toString(CryptoJS4.enc.Utf8);
6722
- const provider = providers.find((p) => p.id === agent.provider);
6723
- if (!provider?.config?.model?.create) {
6724
- res.status(400).json({
6725
- error: { message: "No provider configured for this agent", type: "invalid_request_error" }
6815
+ let resolved;
6816
+ try {
6817
+ resolved = await resolveModel({
6818
+ modelId: agent.model,
6819
+ user,
6820
+ providers,
6821
+ agent: { id: agent.id },
6822
+ project: project ? { id: project.id } : void 0
6726
6823
  });
6727
- return;
6824
+ } catch (err) {
6825
+ if (err instanceof ResolveModelError) {
6826
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
6827
+ res.status(status).json({
6828
+ error: { message: err.message, type: "invalid_request_error", code: err.code }
6829
+ });
6830
+ return;
6831
+ }
6832
+ throw err;
6728
6833
  }
6729
- const languageModel = provider.config.model.create({
6730
- apiKey: providerapikey,
6731
- user: user.id,
6732
- role: user.role?.id,
6733
- project: project?.id,
6734
- agent: agent.id
6735
- });
6834
+ const providerapikey = resolved.apiKey;
6835
+ const languageModel = resolved.languageModel;
6736
6836
  const disabledTools = req.body.disabledTools ?? [];
6737
6837
  const enabledTools = await getEnabledTools(
6738
6838
  agent,
@@ -6821,7 +6921,7 @@ ${project.description}` : ""}` : "",
6821
6921
  const usage = await result.usage;
6822
6922
  await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
6823
6923
  } else {
6824
- const { text, usage } = await generateText3({
6924
+ const { text, usage } = await generateText4({
6825
6925
  model: languageModel,
6826
6926
  system: systemPrompt || void 0,
6827
6927
  messages: coreMessages,
@@ -6842,9 +6942,6 @@ ${project.description}` : ""}` : "",
6842
6942
  );
6843
6943
  };
6844
6944
 
6845
- // src/exulu/routes.ts
6846
- import { convertJsonSchemaToZod } from "zod-from-json-schema";
6847
-
6848
6945
  // src/utils/enabled-skills.ts
6849
6946
  var getEnabledSkills = async (agent, disabledSkills = []) => {
6850
6947
  let enabledSkills = [];
@@ -6862,7 +6959,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
6862
6959
  var getExuluVersionNumber = async () => {
6863
6960
  try {
6864
6961
  const path2 = process.cwd();
6865
- const packageJson = fs2.readFileSync(path2 + "/package.json", "utf8");
6962
+ const packageJson = fs3.readFileSync(path2 + "/package.json", "utf8");
6866
6963
  const packageData = JSON.parse(packageJson);
6867
6964
  const exuluVersion = packageData.dependencies["@exulu/backend"];
6868
6965
  console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
@@ -6885,6 +6982,7 @@ var {
6885
6982
  platformConfigurationsSchema,
6886
6983
  agentSessionsSchema,
6887
6984
  agentMessagesSchema,
6985
+ modelsSchema,
6888
6986
  rolesSchema,
6889
6987
  usersSchema,
6890
6988
  skillsSchema,
@@ -6949,6 +7047,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
6949
7047
  testCasesSchema(),
6950
7048
  agentSessionsSchema(),
6951
7049
  agentMessagesSchema(),
7050
+ modelsSchema(),
6952
7051
  variablesSchema(),
6953
7052
  workflowTemplatesSchema(),
6954
7053
  statisticsSchema(),
@@ -7209,12 +7308,13 @@ Mood: friendly and intelligent.
7209
7308
  },
7210
7309
  redisHost: process.env.REDIS_HOST,
7211
7310
  enabled: config?.workers?.enabled
7311
+ },
7312
+ liteLLM: {
7313
+ enabled: process.env.EXULU_USE_LITELLM === "true"
7212
7314
  }
7213
7315
  });
7214
7316
  });
7215
- providers.forEach((provider) => {
7216
- const slug = provider.slug;
7217
- if (!slug) return;
7317
+ const registerAgentRunRoute = (slug, provider) => {
7218
7318
  app.post(slug + "/:instance", async (req, res) => {
7219
7319
  console.log("[EXULU] POST " + slug + "/:instance", req.body);
7220
7320
  const headers = {
@@ -7310,39 +7410,33 @@ Mood: friendly and intelligent.
7310
7410
  providers,
7311
7411
  user
7312
7412
  );
7313
- if (req.body.outputSchema && !!headers.stream) {
7314
- throw new Error("Providing a outputSchema in the POST body is not allowed when using the streaming API, set 'stream' to false in the headers when defining a response schema.");
7315
- }
7316
- let outputSchema;
7317
- if (req.body.outputSchema) {
7318
- if (typeof req.body.outputSchema === "string") {
7319
- req.body.outputSchema = JSON.parse(req.body.outputSchema);
7320
- }
7321
- outputSchema = convertJsonSchemaToZod(req.body.outputSchema);
7322
- }
7323
- let providerapikey;
7324
- const variableName = agent.providerapikey;
7325
- if (variableName) {
7326
- console.log("[EXULU] provider api key variable name", variableName);
7327
- const variable = await db.from("variables").where({ name: variableName }).first();
7328
- if (!variable) {
7329
- res.status(400).json({
7330
- message: "Provider API key variable not found for " + agent.name + " (" + agent.id + ")."
7331
- });
7332
- return;
7333
- }
7334
- providerapikey = variable.value;
7335
- if (!variable.encrypted) {
7336
- res.status(400).json({
7337
- message: "Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
7338
- });
7413
+ const overrideModelId = req.headers["x-exulu-model-override"];
7414
+ const modelId = overrideModelId ?? agent.model;
7415
+ if (!modelId) {
7416
+ res.status(400).json({
7417
+ message: `Agent ${agent.name} (${agent.id}) has no model configured.`
7418
+ });
7419
+ return;
7420
+ }
7421
+ let resolved;
7422
+ try {
7423
+ resolved = await resolveModel({
7424
+ modelId,
7425
+ user,
7426
+ providers,
7427
+ agent: { id: agent.id }
7428
+ });
7429
+ } catch (err) {
7430
+ if (err instanceof ResolveModelError) {
7431
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
7432
+ res.status(status).json({ message: err.message, code: err.code });
7339
7433
  return;
7340
7434
  }
7341
- if (variable.encrypted) {
7342
- const bytes = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7343
- providerapikey = bytes.toString(CryptoJS5.enc.Utf8);
7344
- }
7435
+ throw err;
7345
7436
  }
7437
+ const providerapikey = resolved.apiKey;
7438
+ const resolvedLanguageModel = resolved.languageModel;
7439
+ const resolvedModelId = resolved.model.id;
7346
7440
  if (!!headers.stream) {
7347
7441
  const statistics = {
7348
7442
  label: agent.name,
@@ -7374,6 +7468,7 @@ ${customInstructions}` : agent.instructions;
7374
7468
  currentSkills: enabledSkills,
7375
7469
  approvedTools,
7376
7470
  allExuluTools: tools,
7471
+ languageModel: resolvedLanguageModel,
7377
7472
  providerapikey,
7378
7473
  toolConfigs: agent.tools,
7379
7474
  exuluConfig: config,
@@ -7422,7 +7517,8 @@ ${customInstructions}` : agent.instructions;
7422
7517
  await saveChat({
7423
7518
  session: headers.session,
7424
7519
  user: user.id,
7425
- messages
7520
+ messages,
7521
+ model: resolvedModelId
7426
7522
  });
7427
7523
  clearSessionCurrentTask(headers.session).catch(() => {
7428
7524
  });
@@ -7484,7 +7580,6 @@ ${customInstructions}` : agent.instructions;
7484
7580
  const response = await provider.generateSync({
7485
7581
  contexts,
7486
7582
  rerankers: rerankers || [],
7487
- outputSchema,
7488
7583
  agent,
7489
7584
  user,
7490
7585
  req,
@@ -7494,6 +7589,7 @@ ${customInstructions}` : agent.instructions;
7494
7589
  currentTools: enabledTools,
7495
7590
  currentSkills: enabledSkills,
7496
7591
  allExuluTools: tools,
7592
+ languageModel: resolvedLanguageModel,
7497
7593
  providerapikey,
7498
7594
  exuluConfig: config,
7499
7595
  toolConfigs: agent.tools,
@@ -7515,289 +7611,414 @@ ${customInstructions}` : agent.instructions;
7515
7611
  return;
7516
7612
  }
7517
7613
  });
7614
+ };
7615
+ providers.forEach((provider) => {
7616
+ const slug = provider.slug;
7617
+ if (!slug) return;
7618
+ registerAgentRunRoute(slug, provider);
7518
7619
  });
7519
- if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
7520
- await createUppyRoutes(app, config);
7521
- } else {
7522
- console.log(
7523
- "[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance."
7524
- );
7620
+ if (isLiteLLMEnabled() && providers.length > 0) {
7621
+ registerAgentRunRoute("/agents/litellm/run", providers[0]);
7525
7622
  }
7526
- app.get("/config", async (req, res) => {
7527
- res.status(200).json({
7528
- message: "Config fetched successfully.",
7529
- config: {
7530
- workers: {
7531
- enabled: config?.workers?.enabled || false
7532
- }
7533
- }
7623
+ app.post("/agents/suggestions/:agentId", async (req, res) => {
7624
+ const agentId = req.params.agentId;
7625
+ if (!agentId) {
7626
+ res.status(400).json({ detail: "Missing agentId" });
7627
+ return;
7628
+ }
7629
+ const agent = await exuluApp.get().agent(agentId);
7630
+ if (!agent) {
7631
+ res.status(404).json({ detail: `Agent ${agentId} not found.` });
7632
+ return;
7633
+ }
7634
+ if (!agent.suggestions_enabled) {
7635
+ res.status(400).json({ detail: "Suggestions are not enabled for this agent." });
7636
+ return;
7637
+ }
7638
+ const authenticationResult = await requestValidators.authenticate(req);
7639
+ if (!authenticationResult.user?.id && agent.rights_mode !== "public") {
7640
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
7641
+ return;
7642
+ }
7643
+ const user = authenticationResult.user;
7644
+ const scopeCheck = checkApiKeyScope(user, agentId);
7645
+ if (!scopeCheck.allowed) {
7646
+ res.status(scopeCheck.code).json({ detail: scopeCheck.reason });
7647
+ return;
7648
+ }
7649
+ const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
7650
+ if (!hasAccessToAgent) {
7651
+ res.status(401).json({ detail: "You don't have access to this agent." });
7652
+ return;
7653
+ }
7654
+ const callerId = resolveCallerId(req, user?.id);
7655
+ const rateLimitsEnabled = checkLicense()["rate-limits"] === true;
7656
+ const effectiveLimits = rateLimitsEnabled ? agent.rate_limits ?? null : null;
7657
+ const preCheck = await preCheckAgentRateLimit({
7658
+ agentId,
7659
+ callerId,
7660
+ limits: effectiveLimits
7534
7661
  });
7535
- });
7536
- app.use(
7537
- "/gateway/anthropic/:agent/:project",
7538
- express2.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }),
7539
- async (req, res) => {
7540
- try {
7541
- if (!req.body.tools) {
7542
- req.body.tools = [];
7543
- }
7544
- const { db } = await postgresClient();
7545
- const authenticationResult = await requestValidators.authenticate(req);
7546
- if (!authenticationResult.user?.id) {
7547
- console.log("[EXULU] failed authentication result", authenticationResult);
7548
- res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
7549
- return;
7550
- }
7551
- const user = authenticationResult.user;
7552
- let agentQuery = db("agents");
7553
- agentQuery.select("*");
7554
- agentQuery = applyAccessControl(agentsSchema(), agentQuery, authenticationResult.user);
7555
- agentQuery.where({ id: req.params.agent });
7556
- const agent = await agentQuery.first();
7557
- if (!agent) {
7558
- const arrayBuffer = createCustomAnthropicStreamingMessage(`
7559
- \x1B[41m -- Agent ${req.params.agent} not found or you do not have access to it. --
7560
- \x1B[0m`);
7561
- res.setHeader("Content-Type", "application/json");
7562
- res.end(Buffer.from(arrayBuffer));
7563
- return;
7564
- }
7565
- let project = null;
7566
- if (!req.params.project || req.params.project === "DEFAULT") {
7567
- project = null;
7568
- } else {
7569
- let projectQuery = db("projects");
7570
- projectQuery.select("*");
7571
- projectQuery = applyAccessControl(
7572
- projectsSchema(),
7573
- projectQuery,
7574
- authenticationResult.user
7575
- );
7576
- projectQuery.where({ id: req.params.project });
7577
- project = await projectQuery.first();
7578
- if (!project) {
7579
- const arrayBuffer = createCustomAnthropicStreamingMessage(
7580
- CLAUDE_MESSAGES.missing_project
7581
- );
7582
- res.setHeader("Content-Type", "application/json");
7583
- res.end(Buffer.from(arrayBuffer));
7584
- return;
7585
- }
7586
- }
7587
- console.log("[EXULU] anthropic proxy called for agent:", agent?.name);
7588
- if (!process.env.NEXTAUTH_SECRET) {
7589
- const arrayBuffer = createCustomAnthropicStreamingMessage(
7590
- CLAUDE_MESSAGES.missing_nextauth_secret
7591
- );
7592
- res.setHeader("Content-Type", "application/json");
7593
- res.end(Buffer.from(arrayBuffer));
7662
+ if (!preCheck.ok) {
7663
+ res.setHeader("Retry-After", String(preCheck.retryAfter));
7664
+ res.status(429).json({
7665
+ detail: `Rate limit exceeded for ${preCheck.metric} on agent ${agent.name}.`,
7666
+ metric: preCheck.metric,
7667
+ retryAfter: preCheck.retryAfter
7668
+ });
7669
+ return;
7670
+ }
7671
+ const messages = Array.isArray(req.body.messages) ? req.body.messages : [];
7672
+ if (messages.length === 0) {
7673
+ res.status(400).json({ detail: "Missing messages in request body." });
7674
+ return;
7675
+ }
7676
+ if (!agent.model) {
7677
+ res.status(400).json({
7678
+ detail: `Agent ${agent.name} (${agent.id}) has no model configured.`
7679
+ });
7680
+ return;
7681
+ }
7682
+ let resolved;
7683
+ try {
7684
+ resolved = await resolveModel({
7685
+ modelId: agent.model,
7686
+ user,
7687
+ providers,
7688
+ agent: { id: agent.id }
7689
+ });
7690
+ } catch (err) {
7691
+ if (err instanceof ResolveModelError) {
7692
+ const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
7693
+ res.status(status).json({ detail: err.message, code: err.code });
7694
+ return;
7695
+ }
7696
+ throw err;
7697
+ }
7698
+ try {
7699
+ const { suggestions, usage } = await generateSuggestions({
7700
+ languageModel: resolved.languageModel,
7701
+ messages,
7702
+ agentInstructions: agent.instructions
7703
+ });
7704
+ await Promise.all([
7705
+ updateStatistic({
7706
+ name: "count",
7707
+ label: agent.name,
7708
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7709
+ trigger: "agent",
7710
+ count: 1,
7711
+ user: user?.id,
7712
+ role: user?.role?.id
7713
+ }),
7714
+ ...usage.inputTokens ? [
7715
+ updateStatistic({
7716
+ name: "inputTokens",
7717
+ label: agent.name,
7718
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7719
+ trigger: "agent",
7720
+ count: usage.inputTokens,
7721
+ user: user?.id,
7722
+ role: user?.role?.id
7723
+ })
7724
+ ] : [],
7725
+ ...usage.outputTokens ? [
7726
+ updateStatistic({
7727
+ name: "outputTokens",
7728
+ label: agent.name,
7729
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7730
+ trigger: "agent",
7731
+ count: usage.outputTokens
7732
+ })
7733
+ ] : [],
7734
+ recordAgentTokenUsage({
7735
+ agentId,
7736
+ callerId,
7737
+ limits: effectiveLimits,
7738
+ inputTokens: usage.inputTokens,
7739
+ outputTokens: usage.outputTokens
7740
+ })
7741
+ ]);
7742
+ res.status(200).json({ suggestions });
7743
+ } catch (err) {
7744
+ console.error("[EXULU] suggestions generation failed", err);
7745
+ res.status(500).json({
7746
+ detail: err instanceof Error ? err.message : "Failed to generate suggestions."
7747
+ });
7748
+ }
7749
+ });
7750
+ const MAX_TRANSCRIBE_BYTES = 25 * 1024 * 1024;
7751
+ const transcribeUpload = multer({
7752
+ storage: multer.memoryStorage(),
7753
+ limits: { fileSize: MAX_TRANSCRIBE_BYTES }
7754
+ });
7755
+ app.post(
7756
+ "/transcribe",
7757
+ (req, res, next) => {
7758
+ transcribeUpload.single("file")(req, res, (err) => {
7759
+ if (!err) return next();
7760
+ const code = err?.code;
7761
+ if (code === "LIMIT_FILE_SIZE") {
7762
+ res.status(413).json({ detail: "Recording too large. Please record a shorter clip." });
7594
7763
  return;
7595
7764
  }
7596
- if (!agent.providerapikey) {
7597
- const arrayBuffer = createCustomAnthropicStreamingMessage(CLAUDE_MESSAGES.not_enabled);
7598
- res.setHeader("Content-Type", "application/json");
7599
- res.end(Buffer.from(arrayBuffer));
7765
+ res.status(400).json({ detail: err instanceof Error ? err.message : "Upload failed." });
7766
+ });
7767
+ },
7768
+ async (req, res) => {
7769
+ if (!isLiteLLMEnabled() || !process.env.TRANSCRIPTION_MODEL) {
7770
+ res.status(503).json({
7771
+ detail: "Speech-to-text is not enabled on this deployment. Set EXULU_USE_LITELLM=true and TRANSCRIPTION_MODEL in the environment."
7772
+ });
7773
+ return;
7774
+ }
7775
+ const authenticationResult = await requestValidators.authenticate(req);
7776
+ if (!authenticationResult.user?.id) {
7777
+ res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
7778
+ return;
7779
+ }
7780
+ const file = req.file;
7781
+ if (!file) {
7782
+ res.status(400).json({ detail: "No audio file provided in 'file' field." });
7783
+ return;
7784
+ }
7785
+ if (!file.mimetype.startsWith("audio/")) {
7786
+ res.status(400).json({
7787
+ detail: `Unsupported mimetype: ${file.mimetype}. Expected audio/*.`
7788
+ });
7789
+ return;
7790
+ }
7791
+ try {
7792
+ await Promise.race([
7793
+ waitForLiteLLMReady(),
7794
+ new Promise(
7795
+ (_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
7796
+ )
7797
+ ]);
7798
+ } catch {
7799
+ res.status(503).json({ detail: "Transcription service is not ready. Try again shortly." });
7800
+ return;
7801
+ }
7802
+ const language = typeof req.body?.language === "string" && /^[a-z]{2}$/.test(req.body.language) ? req.body.language : void 0;
7803
+ try {
7804
+ const { text } = await transcribeAudio({ file, language });
7805
+ res.status(200).json({ text });
7806
+ } catch (err) {
7807
+ if (err instanceof TranscriptionError) {
7808
+ const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
7809
+ res.status(code).json({ detail: err.message });
7600
7810
  return;
7601
7811
  }
7602
- const variableName = agent.providerapikey;
7603
- const variable = await db.from("variables").where({ name: variableName }).first();
7604
- if (!variable) {
7605
- const arrayBuffer = createCustomAnthropicStreamingMessage(
7606
- CLAUDE_MESSAGES.anthropic_token_variable_not_found
7607
- );
7608
- res.setHeader("Content-Type", "application/json");
7609
- res.end(Buffer.from(arrayBuffer));
7812
+ console.error("[EXULU] /transcribe failed", err);
7813
+ res.status(500).json({
7814
+ detail: err instanceof Error ? err.message : "Transcription failed."
7815
+ });
7816
+ }
7817
+ }
7818
+ );
7819
+ const MAX_TTS_INPUT_CHARS = 4e3;
7820
+ app.post(
7821
+ "/speech",
7822
+ bodyParser.json({ limit: "64kb" }),
7823
+ async (req, res) => {
7824
+ if (!isLiteLLMEnabled() || !process.env.TTS_MODEL || !process.env.TTS_VOICE) {
7825
+ res.status(503).json({
7826
+ detail: "Text-to-speech is not enabled on this deployment. Set EXULU_USE_LITELLM=true, TTS_MODEL, and TTS_VOICE in the environment."
7827
+ });
7828
+ return;
7829
+ }
7830
+ const authenticationResult = await requestValidators.authenticate(req);
7831
+ if (!authenticationResult.user?.id) {
7832
+ res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
7833
+ return;
7834
+ }
7835
+ const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
7836
+ if (!text) {
7837
+ res.status(400).json({ detail: "Missing 'text' in request body." });
7838
+ return;
7839
+ }
7840
+ if (text.length > MAX_TTS_INPUT_CHARS) {
7841
+ res.status(400).json({
7842
+ detail: `Text too long (${text.length} chars). Max ${MAX_TTS_INPUT_CHARS}.`
7843
+ });
7844
+ return;
7845
+ }
7846
+ try {
7847
+ await Promise.race([
7848
+ waitForLiteLLMReady(),
7849
+ new Promise(
7850
+ (_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
7851
+ )
7852
+ ]);
7853
+ } catch {
7854
+ res.status(503).json({ detail: "Speech service is not ready. Try again shortly." });
7855
+ return;
7856
+ }
7857
+ try {
7858
+ const audio = await synthesizeSpeech({ text });
7859
+ res.status(200);
7860
+ res.setHeader("Content-Type", "audio/mpeg");
7861
+ res.setHeader("Content-Length", String(audio.length));
7862
+ res.setHeader("Cache-Control", "no-store");
7863
+ res.send(audio);
7864
+ } catch (err) {
7865
+ if (err instanceof SpeechError) {
7866
+ const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
7867
+ res.status(code).json({ detail: err.message });
7610
7868
  return;
7611
7869
  }
7612
- let anthropicApiKey = variable.value;
7613
- if (!variable.encrypted) {
7614
- const arrayBuffer = createCustomAnthropicStreamingMessage(
7615
- CLAUDE_MESSAGES.anthropic_token_variable_not_encrypted
7616
- );
7617
- res.setHeader("Content-Type", "application/json");
7618
- res.end(Buffer.from(arrayBuffer));
7870
+ console.error("[EXULU] /speech failed", err);
7871
+ res.status(500).json({
7872
+ detail: err instanceof Error ? err.message : "Speech generation failed."
7873
+ });
7874
+ }
7875
+ }
7876
+ );
7877
+ app.use("/litellm/:project", async (req, res) => {
7878
+ if (!isLiteLLMEnabled()) {
7879
+ res.status(503).json({
7880
+ detail: "LiteLLM is not enabled on this deployment. Set EXULU_USE_LITELLM=true."
7881
+ });
7882
+ return;
7883
+ }
7884
+ const masterKey = process.env.LITELLM_MASTER_KEY;
7885
+ if (!masterKey) {
7886
+ res.status(503).json({ detail: "LITELLM_MASTER_KEY is not configured." });
7887
+ return;
7888
+ }
7889
+ const authenticationResult = await requestValidators.authenticate(req);
7890
+ if (!authenticationResult.user?.id) {
7891
+ console.log("[EXULU] /litellm failed authentication", authenticationResult);
7892
+ res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
7893
+ return;
7894
+ }
7895
+ const user = authenticationResult.user;
7896
+ if (!req.path.startsWith("/v1/")) {
7897
+ res.status(403).json({
7898
+ detail: `Path ${req.path} is not exposed through the Exulu LiteLLM proxy.`
7899
+ });
7900
+ return;
7901
+ }
7902
+ let project = null;
7903
+ if (req.params.project && req.params.project !== "DEFAULT") {
7904
+ const { db } = await postgresClient();
7905
+ let projectQuery = db("projects");
7906
+ projectQuery.select("*");
7907
+ projectQuery = applyAccessControl(
7908
+ projectsSchema(),
7909
+ projectQuery,
7910
+ authenticationResult.user
7911
+ );
7912
+ projectQuery.where({ id: req.params.project });
7913
+ project = await projectQuery.first();
7914
+ if (!project) {
7915
+ res.status(404).json({
7916
+ detail: "Project not found or you do not have access to it."
7917
+ });
7918
+ return;
7919
+ }
7920
+ }
7921
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
7922
+ const port = process.env.LITELLM_PORT ?? "4000";
7923
+ const upstreamUrl = `http://${host}:${port}${req.url}`;
7924
+ const upstreamHeaders = {};
7925
+ for (const [name, value] of Object.entries(req.headers)) {
7926
+ if (value === void 0) continue;
7927
+ const lower = name.toLowerCase();
7928
+ if (lower === "authorization" || lower === "host" || lower === "content-length" || lower === "connection" || lower === "transfer-encoding" || lower === "accept-encoding")
7929
+ continue;
7930
+ upstreamHeaders[name] = Array.isArray(value) ? value.join(", ") : value;
7931
+ }
7932
+ upstreamHeaders["authorization"] = `Bearer ${masterKey}`;
7933
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
7934
+ let body;
7935
+ if (methodHasBody && req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
7936
+ body = JSON.stringify(req.body);
7937
+ upstreamHeaders["content-type"] = "application/json";
7938
+ }
7939
+ const tags = buildTags({
7940
+ user_id: user.id,
7941
+ role_id: user.role?.id,
7942
+ project_id: project?.id,
7943
+ user_name: user.email,
7944
+ role_name: user.role?.name,
7945
+ project_name: project?.name
7946
+ });
7947
+ if (tags?.length) {
7948
+ upstreamHeaders["x-litellm-tags"] = tags.join(",");
7949
+ upstreamHeaders["x-litellm-spend-logs-metadata"] = JSON.stringify({
7950
+ user_id: user.id,
7951
+ role_id: user.role?.id,
7952
+ project_id: project?.id || "DEFAULT",
7953
+ user_name: user.email,
7954
+ role_name: user.role?.name,
7955
+ project_name: project?.name
7956
+ });
7957
+ }
7958
+ console.log("[EXULU] Built tags", tags);
7959
+ const taggedFetch = createTaggedFetch(tags);
7960
+ try {
7961
+ const upstream = await taggedFetch(upstreamUrl, {
7962
+ method: req.method,
7963
+ headers: upstreamHeaders,
7964
+ body
7965
+ });
7966
+ res.status(upstream.status);
7967
+ upstream.headers.forEach((value, name) => {
7968
+ const lower = name.toLowerCase();
7969
+ if (lower === "content-encoding" || lower === "content-length" || lower === "transfer-encoding" || lower === "connection")
7619
7970
  return;
7971
+ res.setHeader(name, value);
7972
+ });
7973
+ if (!upstream.body) {
7974
+ res.end();
7975
+ return;
7976
+ }
7977
+ const reader = upstream.body.getReader();
7978
+ try {
7979
+ while (true) {
7980
+ const { done, value } = await reader.read();
7981
+ if (done) break;
7982
+ if (value) res.write(value);
7620
7983
  }
7621
- if (variable.encrypted) {
7622
- const bytes = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7623
- anthropicApiKey = bytes.toString(CryptoJS5.enc.Utf8);
7624
- }
7625
- const headers = {
7626
- "x-api-key": anthropicApiKey,
7627
- "anthropic-version": "2023-06-01",
7628
- "content-type": req.headers["content-type"] || "application/json"
7629
- };
7630
- if (req.headers["accept"]) headers["accept"] = req.headers["accept"];
7631
- if (req.headers["user-agent"]) headers["user-agent"] = req.headers["user-agent"];
7632
- const client2 = new Anthropic({
7633
- apiKey: anthropicApiKey
7984
+ } finally {
7985
+ reader.releaseLock();
7986
+ }
7987
+ res.end();
7988
+ } catch (err) {
7989
+ console.error("[EXULU] /litellm proxy failed", err);
7990
+ if (!res.headersSent) {
7991
+ res.status(502).json({
7992
+ detail: err instanceof Error ? err.message : "LiteLLM proxy failed."
7634
7993
  });
7635
- const tokens = {};
7636
- const disabledTools = req.body.disabledTools ? req.body.disabledTools : [];
7637
- let enabledTools = await getEnabledTools(
7638
- agent,
7639
- tools,
7640
- contexts || [],
7641
- rerankers || [],
7642
- disabledTools,
7643
- providers,
7644
- user
7645
- );
7646
- const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
7647
- const agentInstructions = customInstructions ? `${agent?.instructions}
7648
-
7649
- ${customInstructions}` : agent?.instructions;
7650
- let system = req.body.system;
7651
- if (Array.isArray(req.body.system)) {
7652
- system = [
7653
- ...req.body.system,
7654
- ...agent ? [
7655
- {
7656
- type: "text",
7657
- text: `
7658
- You are an agent named: ${agent?.name}
7659
- Here are some additional instructions for you: ${agentInstructions}`
7660
- }
7661
- ] : [],
7662
- ...project ? [
7663
- {
7664
- type: "text",
7665
- text: `Additional information:
7666
-
7667
- The project you are working on is: ${project?.name}
7668
- Here is some additional information about the project: ${project?.description}`
7669
- }
7670
- ] : []
7671
- ];
7672
- } else {
7673
- system = `${req.body.system}
7674
-
7675
-
7676
- ${agent ? `You are an agent named: ${agent?.name}
7677
- Here are some additional instructions for you: ${agentInstructions}` : ""}
7678
-
7679
- ${project?.id ? `Additional information:
7680
-
7681
- The project you are working on is: ${project?.name}
7682
- The project description is: ${project?.description}` : ""}
7683
- `;
7684
- }
7685
- for await (const event of client2.messages.stream({
7686
- ...req.body,
7687
- system
7688
- })) {
7689
- if (event.message?.id) {
7690
- tokens[event.message.id] = {
7691
- input_tokens: event.message.usage.input_tokens,
7692
- cache_creation_input_tokens: event.message.usage.cache_creation_input_tokens,
7693
- cache_read_input_tokens: event.message.usage.cache_read_input_tokens,
7694
- output_tokens: event.message.usage.output_tokens
7695
- };
7696
- }
7697
- if (event.message?.type === "tool_use" && event.message?.name?.includes("exulu_")) {
7698
- const toolName = event.message?.name;
7699
- console.log("[EXULU] Using tool", toolName);
7700
- const inputs = event.message?.input;
7701
- const id = event.message?.id;
7702
- const tool = enabledTools.find(
7703
- (tool2) => tool2.id === toolName.replace("exulu_", "")
7704
- );
7705
- if (!tool || !tool.tool.execute) {
7706
- console.error("[EXULU] Tool not found or not enabled.", toolName);
7707
- continue;
7708
- }
7709
- const toolResult = await tool.tool.execute(inputs, {
7710
- toolCallId: id,
7711
- messages: [
7712
- {
7713
- ...event.message,
7714
- role: "tool"
7715
- }
7716
- ]
7717
- });
7718
- console.log("[EXULU] Tool result", toolResult);
7719
- const toolResultMessage = {
7720
- role: "user",
7721
- content: [
7722
- {
7723
- type: "tool_result",
7724
- tool_use_id: id,
7725
- content: toolResult
7726
- }
7727
- ]
7728
- };
7729
- res.write(`event: tool_result
7730
- data: ${JSON.stringify(toolResultMessage)}
7731
-
7732
- `);
7733
- } else {
7734
- const msg = `event: ${event.type}
7735
- data: ${JSON.stringify(event)}
7736
-
7737
- `;
7738
- res.write(msg);
7739
- }
7740
- }
7741
- let totalInputTokens = 0;
7742
- let totalOutputTokens = 0;
7743
- for (const token of Object.values(tokens)) {
7744
- totalInputTokens += token.input_tokens;
7745
- totalOutputTokens += token.output_tokens;
7746
- }
7747
- const statistics = {
7748
- label: agent.name,
7749
- trigger: "agent"
7750
- };
7751
- await Promise.all([
7752
- updateStatistic({
7753
- name: "count",
7754
- label: statistics.label,
7755
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7756
- trigger: statistics.trigger,
7757
- count: 1,
7758
- user: user.id,
7759
- role: user?.role?.id,
7760
- ...project ? { project: project.id } : {}
7761
- }),
7762
- ...totalInputTokens ? [
7763
- updateStatistic({
7764
- name: "inputTokens",
7765
- label: statistics.label,
7766
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7767
- trigger: statistics.trigger,
7768
- count: totalInputTokens,
7769
- user: user.id,
7770
- role: user?.role?.id,
7771
- ...project ? { project: project.id } : {}
7772
- })
7773
- ] : [],
7774
- ...totalOutputTokens ? [
7775
- updateStatistic({
7776
- name: "outputTokens",
7777
- label: statistics.label,
7778
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7779
- trigger: statistics.trigger,
7780
- count: totalInputTokens,
7781
- user: user.id,
7782
- role: user?.role?.id,
7783
- ...project ? { project: project.id } : {}
7784
- })
7785
- ] : []
7786
- ]);
7787
- res.write("event: done\ndata: [DONE]\n\n");
7994
+ } else {
7788
7995
  res.end();
7789
- } catch (error) {
7790
- console.error("[PROXY] Manual proxy error:", error);
7791
- if (!res.headersSent) {
7792
- if (error?.message === "Invalid token") {
7793
- res.status(500).json({ error: "Authentication error, please check your IMP token and try again." });
7794
- } else {
7795
- res.status(500).json({ error: error.message });
7796
- }
7797
- }
7798
7996
  }
7799
7997
  }
7800
- );
7998
+ });
7999
+ if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
8000
+ await createUppyRoutes(app, config);
8001
+ } else {
8002
+ console.log(
8003
+ "[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance."
8004
+ );
8005
+ }
8006
+ app.get("/config", async (req, res) => {
8007
+ res.status(200).json({
8008
+ message: "Config fetched successfully.",
8009
+ config: {
8010
+ workers: {
8011
+ enabled: config?.workers?.enabled || false
8012
+ }
8013
+ }
8014
+ });
8015
+ });
8016
+ app.use("/gateway/anthropic/:agent/:project", (req, res) => {
8017
+ console.warn("[EXULU] DEPRECATED: Received a request to /gateway/anthropic/:agent/:project, this is a deprecated path and will be removed in a future version. Please use /litellm/:project instead.");
8018
+ const suffix = req.url;
8019
+ req.url = `/litellm/${encodeURIComponent(req.params.project)}${suffix}`;
8020
+ app.handle(req, res);
8021
+ });
7801
8022
  function buildFileTree(files, stripPrefix) {
7802
8023
  const root = { name: "/", path: "/", key: "", type: "folder", children: [] };
7803
8024
  for (const file of files) {
@@ -8513,13 +8734,139 @@ data: ${JSON.stringify(event)}
8513
8734
  }
8514
8735
  }
8515
8736
  );
8516
- app.use(express2.static("public"));
8517
- await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config, rerankers);
8518
- return app;
8519
- };
8520
- function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
8521
- function lcs(a, b) {
8522
- const m = a.length;
8737
+ async function resolveTargetUser(req, res, opts) {
8738
+ const authResult = await requestValidators.authenticate(req);
8739
+ if (!authResult.user?.id) {
8740
+ res.status(authResult.code ?? 401).json({ detail: authResult.message });
8741
+ return null;
8742
+ }
8743
+ if (!authResult.user.super_admin) {
8744
+ res.status(403).json({ detail: "Super admin access required." });
8745
+ return null;
8746
+ }
8747
+ const targetId = Number.parseInt(req.params.id ?? "", 10);
8748
+ if (!Number.isFinite(targetId)) {
8749
+ res.status(400).json({ detail: "Invalid user id." });
8750
+ return null;
8751
+ }
8752
+ if (!opts.allowSelf && targetId === authResult.user.id) {
8753
+ res.status(403).json({ detail: "Super admins cannot delete their own account." });
8754
+ return null;
8755
+ }
8756
+ const { db } = await postgresClient();
8757
+ const targetUser = await db.from("users").where({ id: targetId }).first();
8758
+ if (!targetUser) {
8759
+ res.status(404).json({ detail: "User not found." });
8760
+ return null;
8761
+ }
8762
+ return { targetUser, db };
8763
+ }
8764
+ app.get("/users/:id/data-export", async (req, res) => {
8765
+ const resolved = await resolveTargetUser(req, res, { allowSelf: true });
8766
+ if (!resolved) return;
8767
+ const { targetUser, db } = resolved;
8768
+ try {
8769
+ const role = targetUser.role ? await db.from("roles").where({ id: targetUser.role }).first() : null;
8770
+ const userExport = { ...targetUser, role: role ?? targetUser.role };
8771
+ delete userExport.password;
8772
+ delete userExport.apikey;
8773
+ delete userExport.temporary_token;
8774
+ delete userExport.anthropic_token;
8775
+ const sessions = await db.from("agent_sessions").where({ user: targetUser.id });
8776
+ const sessionIds = sessions.map((s) => s.id);
8777
+ const messages = sessionIds.length ? await db.from("agent_messages").whereIn("session", sessionIds).orderBy("createdAt", "asc") : [];
8778
+ const messagesBySession = /* @__PURE__ */ new Map();
8779
+ for (const m of messages) {
8780
+ const list = messagesBySession.get(m.session) ?? [];
8781
+ list.push(m);
8782
+ messagesBySession.set(m.session, list);
8783
+ }
8784
+ const sessionsWithMessages = sessions.map((s) => ({
8785
+ ...s,
8786
+ messages: messagesBySession.get(s.id) ?? []
8787
+ }));
8788
+ const [feedback, promptFavorites, tracking] = await Promise.all([
8789
+ db.from("feedback").where({ user: targetUser.id }).catch(() => []),
8790
+ db.from("prompt_favorites").where({ user_id: targetUser.id }).catch(() => []),
8791
+ db.from("tracking").where({ user: targetUser.id }).catch(() => [])
8792
+ ]);
8793
+ const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
8794
+ const readme = [
8795
+ `Exulu user data export`,
8796
+ ``,
8797
+ `User id: ${targetUser.id}`,
8798
+ `Exported at: ${exportedAt}`,
8799
+ ``,
8800
+ `This archive contains the personal data Exulu holds for the user`,
8801
+ `referenced above, provided in fulfilment of DSGVO Art. 15`,
8802
+ `(Recht auf Auskunft / GDPR right of access).`,
8803
+ ``,
8804
+ `Files:`,
8805
+ ` - user_data.json account row (password/api-key hashes stripped)`,
8806
+ ` - sessions.json chat sessions with messages inlined`,
8807
+ ` - feedback.json feedback the user submitted`,
8808
+ ` - prompt_favorites.json prompt-library favourites`,
8809
+ ` - tracking.json tracking events linked to the user`,
8810
+ ``
8811
+ ].join("\n");
8812
+ const zip = new JSZip2();
8813
+ zip.file("README.txt", readme);
8814
+ zip.file("user_data.json", JSON.stringify(userExport, null, 2));
8815
+ zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
8816
+ zip.file("feedback.json", JSON.stringify(feedback, null, 2));
8817
+ zip.file(
8818
+ "prompt_favorites.json",
8819
+ JSON.stringify(promptFavorites, null, 2)
8820
+ );
8821
+ zip.file("tracking.json", JSON.stringify(tracking, null, 2));
8822
+ const buffer = await zip.generateAsync({ type: "nodebuffer" });
8823
+ const filename = `user-${targetUser.id}-export-${exportedAt.replace(/[:.]/g, "-")}.zip`;
8824
+ res.setHeader("Content-Type", "application/zip");
8825
+ res.setHeader(
8826
+ "Content-Disposition",
8827
+ `attachment; filename="${filename}"`
8828
+ );
8829
+ res.send(buffer);
8830
+ } catch (err) {
8831
+ console.error("[GDPR] Failed to build data export", err);
8832
+ res.status(500).json({ detail: "Failed to build data export." });
8833
+ }
8834
+ });
8835
+ app.delete("/users/:id", async (req, res) => {
8836
+ const resolved = await resolveTargetUser(req, res, { allowSelf: false });
8837
+ if (!resolved) return;
8838
+ const { targetUser, db } = resolved;
8839
+ try {
8840
+ await db.transaction(async (trx) => {
8841
+ await trx("agent_messages").where({ user: targetUser.id }).delete();
8842
+ await trx("agent_sessions").where({ user: targetUser.id }).delete();
8843
+ await trx("feedback").where({ user: targetUser.id }).delete();
8844
+ await trx("prompt_favorites").where({ user_id: targetUser.id }).delete();
8845
+ await trx("tracking").where({ user: targetUser.id }).delete();
8846
+ await trx("rbac").where({ user_id: targetUser.id }).delete();
8847
+ await trx("users").where({ id: targetUser.id }).delete();
8848
+ });
8849
+ } catch (err) {
8850
+ console.error("[GDPR] Failed to delete user", err);
8851
+ res.status(500).json({ detail: "Failed to delete user." });
8852
+ return;
8853
+ }
8854
+ try {
8855
+ const prefix = `user_${targetUser.id}/`;
8856
+ const files = await listS3ObjectsByPrefix(prefix, config);
8857
+ await Promise.all(files.map((f) => deleteS3Object(f.key, config)));
8858
+ } catch (err) {
8859
+ console.error("[GDPR] DB delete succeeded but S3 cleanup failed", err);
8860
+ }
8861
+ res.status(204).send();
8862
+ });
8863
+ app.use(express2.static("public"));
8864
+ await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config, rerankers);
8865
+ return app;
8866
+ };
8867
+ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
8868
+ function lcs(a, b) {
8869
+ const m = a.length;
8523
8870
  const n = b.length;
8524
8871
  const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
8525
8872
  for (let i = 1; i <= m; i++) {
@@ -8577,20 +8924,6 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
8577
8924
  }
8578
8925
  return lines.join("\n");
8579
8926
  }
8580
- var createCustomAnthropicStreamingMessage = (message) => {
8581
- const responseData = {
8582
- type: "message",
8583
- content: [
8584
- {
8585
- type: "text",
8586
- text: message
8587
- }
8588
- ]
8589
- };
8590
- const jsonString = JSON.stringify(responseData);
8591
- const arrayBuffer = new TextEncoder().encode(jsonString).buffer;
8592
- return arrayBuffer;
8593
- };
8594
8927
 
8595
8928
  // src/mcp/index.ts
8596
8929
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -8599,8 +8932,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
8599
8932
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
8600
8933
  import "express";
8601
8934
  import "@opentelemetry/api";
8602
- import CryptoJS6 from "crypto-js";
8603
- import { z as z2 } from "zod";
8935
+ import { z as z3 } from "zod";
8604
8936
  var SESSION_ID_HEADER = "mcp-session-id";
8605
8937
  var ExuluMCP = class {
8606
8938
  server = {};
@@ -8638,61 +8970,53 @@ var ExuluMCP = class {
8638
8970
  allProviders,
8639
8971
  user
8640
8972
  );
8641
- const provider = allProviders.find((a) => a.id === agent.provider);
8642
- if (!provider) {
8973
+ if (!agent.model) {
8643
8974
  throw new Error(
8644
- "Agent provider not found for agent " + agent.name + " (" + agent.id + ")."
8975
+ "Agent has no model configured for agent " + agent.name + " (" + agent.id + ")."
8645
8976
  );
8646
8977
  }
8647
- const agentTool = await provider.tool(agent.id, allProviders, allContexts, allRerankers);
8648
- if (agentTool) {
8649
- enabledTools = [...enabledTools, agentTool];
8650
- }
8651
- const variableName = agent.providerapikey;
8652
- const { db } = await postgresClient();
8653
- let providerapikey;
8654
- if (variableName) {
8655
- const variable = await db.from("variables").where({ name: variableName }).first();
8656
- if (!variable) {
8657
- throw new Error(
8658
- "Provider API key variable not found for " + agent.name + " (" + agent.id + ")."
8659
- );
8660
- }
8661
- providerapikey = variable.value;
8662
- if (!variable.encrypted) {
8663
- throw new Error(
8664
- "Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
8665
- );
8666
- }
8667
- if (variable.encrypted) {
8668
- const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
8669
- providerapikey = bytes.toString(CryptoJS6.enc.Utf8);
8978
+ const resolved = await resolveModel({
8979
+ modelId: agent.model,
8980
+ user,
8981
+ providers: allProviders,
8982
+ agent: { id: agent.id }
8983
+ });
8984
+ const providerapikey = resolved.apiKey;
8985
+ if (!isLiteLLMEnabled()) {
8986
+ const agentTool = await resolved.exuluProvider.tool(
8987
+ agent.id,
8988
+ allProviders,
8989
+ allContexts,
8990
+ allRerankers
8991
+ );
8992
+ if (agentTool) {
8993
+ enabledTools = [...enabledTools, agentTool];
8670
8994
  }
8671
8995
  }
8672
8996
  console.log(
8673
8997
  "[EXULU] Enabled tools",
8674
8998
  enabledTools?.map((x) => x.name + " (" + x.id + ")")
8675
8999
  );
8676
- for (const tool of enabledTools || []) {
8677
- if (server.tools[tool.id]) {
9000
+ for (const tool2 of enabledTools || []) {
9001
+ if (server.tools[tool2.id]) {
8678
9002
  continue;
8679
9003
  }
8680
9004
  server.mcp.registerTool(
8681
- sanitizeToolName(tool.name + "_agent_" + tool.id),
9005
+ sanitizeToolName(tool2.name + "_agent_" + tool2.id),
8682
9006
  {
8683
- title: tool.name + " agent",
8684
- description: tool.description,
9007
+ title: tool2.name + " agent",
9008
+ description: tool2.description,
8685
9009
  inputSchema: {
8686
- inputs: tool.inputSchema || z2.object({})
9010
+ inputs: tool2.inputSchema || z3.object({})
8687
9011
  }
8688
9012
  },
8689
9013
  async ({ inputs }, args) => {
8690
- console.log("[EXULU] MCP tool name", tool.name);
9014
+ console.log("[EXULU] MCP tool name", tool2.name);
8691
9015
  console.log("[EXULU] MCP tool inputs", inputs);
8692
9016
  console.log("[EXULU] MCP tool args", args);
8693
9017
  const configValues = agent.tools;
8694
9018
  const tools = await convertExuluToolsToAiSdkTools(
8695
- [tool],
9019
+ [tool2],
8696
9020
  [],
8697
9021
  [],
8698
9022
  allTools,
@@ -8706,13 +9030,13 @@ var ExuluMCP = class {
8706
9030
  void 0,
8707
9031
  void 0
8708
9032
  );
8709
- const convertedTool = tools[sanitizeToolName(tool.name)];
9033
+ const convertedTool = tools[sanitizeToolName(tool2.name)];
8710
9034
  if (!convertedTool?.execute) {
8711
9035
  console.error("[EXULU] Tool not found in converted tools array.", tools);
8712
9036
  throw new Error("Tool not found in converted tools array.");
8713
9037
  }
8714
9038
  const iterator = await convertedTool.execute(inputs, {
8715
- toolCallId: tool.id + "_" + randomUUID3(),
9039
+ toolCallId: tool2.id + "_" + randomUUID3(),
8716
9040
  messages: []
8717
9041
  });
8718
9042
  let result;
@@ -8726,7 +9050,7 @@ var ExuluMCP = class {
8726
9050
  };
8727
9051
  }
8728
9052
  );
8729
- server.tools[tool.id] = tool.name;
9053
+ server.tools[tool2.id] = tool2.name;
8730
9054
  }
8731
9055
  const getListOfPromptTemplatesName = "getListOfPromptTemplates";
8732
9056
  if (!server.tools[getListOfPromptTemplatesName]) {
@@ -8736,13 +9060,13 @@ var ExuluMCP = class {
8736
9060
  title: "Get List of Prompt Templates",
8737
9061
  description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
8738
9062
  inputSchema: {
8739
- inputs: z2.object({})
9063
+ inputs: z3.object({})
8740
9064
  }
8741
9065
  },
8742
9066
  async ({ inputs }, args) => {
8743
9067
  console.log("[EXULU] Getting list of prompt templates for agent", agent.id);
8744
- const { db: db2 } = await postgresClient();
8745
- const prompts = await db2.from("prompt_library").select("id", "name", "description").whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).orderBy("updatedAt", "desc");
9068
+ const { db } = await postgresClient();
9069
+ const prompts = await db.from("prompt_library").select("id", "name", "description").whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).orderBy("updatedAt", "desc");
8746
9070
  console.log("[EXULU] Found", prompts.length, "prompt templates");
8747
9071
  return {
8748
9072
  content: [
@@ -8782,15 +9106,15 @@ var ExuluMCP = class {
8782
9106
  title: "Get Prompt Template Details",
8783
9107
  description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
8784
9108
  inputSchema: {
8785
- inputs: z2.object({
8786
- id: z2.string().describe("The ID of the prompt template to retrieve")
9109
+ inputs: z3.object({
9110
+ id: z3.string().describe("The ID of the prompt template to retrieve")
8787
9111
  })
8788
9112
  }
8789
9113
  },
8790
9114
  async ({ inputs }, args) => {
8791
9115
  console.log("[EXULU] Getting prompt template details for ID", inputs.id);
8792
- const { db: db2 } = await postgresClient();
8793
- const prompt = await db2.from("prompt_library").select(
9116
+ const { db } = await postgresClient();
9117
+ const prompt = await db.from("prompt_library").select(
8794
9118
  "id",
8795
9119
  "name",
8796
9120
  "description",
@@ -8803,7 +9127,7 @@ var ExuluMCP = class {
8803
9127
  if (!prompt) {
8804
9128
  throw new Error(`Prompt template with ID ${inputs.id} not found`);
8805
9129
  }
8806
- const isAssignedToAgent = await db2.from("prompt_library").select("id").where({ id: inputs.id }).whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).first();
9130
+ const isAssignedToAgent = await db.from("prompt_library").select("id").where({ id: inputs.id }).whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).first();
8807
9131
  console.log("[EXULU] Prompt template found:", prompt.name);
8808
9132
  return {
8809
9133
  content: [
@@ -9886,7 +10210,7 @@ var ExuluEval = class {
9886
10210
  };
9887
10211
 
9888
10212
  // src/templates/evals/index.ts
9889
- import { z as z3 } from "zod";
10213
+ import { z as z4 } from "zod";
9890
10214
  var llmAsJudgeEval = () => {
9891
10215
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
9892
10216
  return new ExuluEval({
@@ -9919,21 +10243,28 @@ var llmAsJudgeEval = () => {
9919
10243
  }
9920
10244
  prompt = prompt.replace("{actual_output}", lastMessage);
9921
10245
  prompt = prompt.replace("{expected_output}", testCase.expected_output);
9922
- if (!agent.providerapikey) {
10246
+ if (!agent.model) {
9923
10247
  throw new Error(
9924
- `Provider API key for agent ${agent.name} is required, variable name is ${agent.providerapikey} but it is not set.`
10248
+ `Agent ${agent.name} has no model configured (required for llm-as-judge eval).`
9925
10249
  );
9926
10250
  }
9927
- const providerapikey = await ExuluVariables.get(agent.providerapikey);
10251
+ const resolved = await resolveModel({
10252
+ modelId: agent.model,
10253
+ providers: exuluApp.get().providers,
10254
+ agent: { id: agent.id },
10255
+ rbacBypass: true
10256
+ });
10257
+ const providerapikey = resolved.apiKey;
9928
10258
  console.log("[EXULU] prompt", prompt);
9929
10259
  const response = await provider.generateSync({
9930
10260
  agent,
9931
10261
  contexts: [],
9932
10262
  rerankers: [],
9933
10263
  prompt,
9934
- outputSchema: z3.object({
9935
- score: z3.number().min(0).max(100).describe("The score between 0 and 100.")
10264
+ outputSchema: z4.object({
10265
+ score: z4.number().min(0).max(100).describe("The score between 0 and 100.")
9936
10266
  }),
10267
+ languageModel: resolved.languageModel,
9937
10268
  providerapikey
9938
10269
  });
9939
10270
  console.log("[EXULU] response", response);
@@ -10160,12 +10491,12 @@ Usage:
10160
10491
  - If no todos exist yet, an empty list will be returned`;
10161
10492
 
10162
10493
  // src/templates/tools/todo/todo.ts
10163
- import z4 from "zod";
10164
- var TodoSchema = z4.object({
10165
- content: z4.string().describe("Brief description of the task"),
10166
- status: z4.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
10167
- priority: z4.string().describe("Priority level of the task: high, medium, low"),
10168
- id: z4.string().describe("Unique identifier for the todo item")
10494
+ import z5 from "zod";
10495
+ var TodoSchema = z5.object({
10496
+ content: z5.string().describe("Brief description of the task"),
10497
+ status: z5.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
10498
+ priority: z5.string().describe("Priority level of the task: high, medium, low"),
10499
+ id: z5.string().describe("Unique identifier for the todo item")
10169
10500
  });
10170
10501
  var TodoWriteTool = new ExuluTool({
10171
10502
  id: "todo_write",
@@ -10181,8 +10512,8 @@ var TodoWriteTool = new ExuluTool({
10181
10512
  default: todowrite_default
10182
10513
  }
10183
10514
  ],
10184
- inputSchema: z4.object({
10185
- todos: z4.array(TodoSchema).describe("The updated todo list")
10515
+ inputSchema: z5.object({
10516
+ todos: z5.array(TodoSchema).describe("The updated todo list")
10186
10517
  }),
10187
10518
  execute: async (inputs) => {
10188
10519
  const { sessionID, todos, user } = inputs;
@@ -10217,7 +10548,7 @@ var TodoReadTool = new ExuluTool({
10217
10548
  id: "todo_read",
10218
10549
  name: "Todo Read",
10219
10550
  description: "Use this tool to read your todo list",
10220
- inputSchema: z4.object({}),
10551
+ inputSchema: z5.object({}),
10221
10552
  type: "function",
10222
10553
  category: "todo",
10223
10554
  config: [
@@ -10259,7 +10590,7 @@ var todoTools = [TodoWriteTool, TodoReadTool];
10259
10590
  var questionread_default = 'Use this tool to read questions you\'ve asked and check if they\'ve been answered by the user. This tool helps you track the status of questions and retrieve the user\'s selected answers.\n\n## When to Use This Tool\n\nUse this tool proactively in these situations:\n- After asking a question to check if the user has responded\n- To retrieve the user\'s answer before proceeding with implementation\n- To review all questions and answers in the current session\n- When you need to reference a previous answer\n\n## How It Works\n\n- This tool takes no parameters (leave the input blank or empty)\n- Returns an array of all questions in the session\n- Each question includes:\n - `id`: Unique identifier for the question\n - `question`: The question text\n - `answerOptions`: Array of answer options with their IDs and text\n - `status`: Either "pending" (not answered) or "answered"\n - `selectedAnswerId`: The ID of the chosen answer (only present if answered)\n\n## Usage Pattern\n\nTypically you\'ll:\n1. Use Question Ask to pose a question\n2. Wait for the user to respond\n3. Use Question Read to check the answer\n4. Find the selected answer by matching the `selectedAnswerId` with an option in `answerOptions`\n5. Proceed with implementation based on the user\'s choice\n\n## Example Response\n\n```json\n[\n {\n "id": "question123",\n "question": "Which authentication method would you like to implement?",\n "answerOptions": [\n { "id": "ans1", "text": "JWT tokens" },\n { "id": "ans2", "text": "OAuth 2.0" },\n { "id": "ans3", "text": "Session-based auth" },\n { "id": "ans4", "text": "None of the above..." }\n ],\n "status": "answered",\n "selectedAnswerId": "ans1"\n }\n]\n```\n\nIn this example, the user selected "JWT tokens" (id: ans1).\n\n## Important Notes\n\n- If no questions exist in the session, an empty array will be returned\n- Questions remain in the session even after being answered for reference\n- Use the `selectedAnswerId` to find which answer option the user chose by matching it against the `id` field in `answerOptions`\n';
10260
10591
 
10261
10592
  // src/templates/tools/question/question.ts
10262
- import z6 from "zod";
10593
+ import z7 from "zod";
10263
10594
 
10264
10595
  // src/templates/tools/question/questionask.txt
10265
10596
  var questionask_default = `Use this tool to ask the user a question with multiple choice answer options during your session. This helps you gather user input, clarify requirements, and make informed decisions based on user preferences.
@@ -10346,18 +10677,18 @@ After asking a question, use the Question Read tool to check if the user has ans
10346
10677
  `;
10347
10678
 
10348
10679
  // src/templates/tools/question/question-ask.ts
10349
- import z5 from "zod";
10680
+ import z6 from "zod";
10350
10681
  import { randomUUID as randomUUID4 } from "crypto";
10351
- var AnswerOptionSchema = z5.object({
10352
- id: z5.string().describe("Unique identifier for the answer option"),
10353
- text: z5.string().describe("The text of the answer option")
10682
+ var AnswerOptionSchema = z6.object({
10683
+ id: z6.string().describe("Unique identifier for the answer option"),
10684
+ text: z6.string().describe("The text of the answer option")
10354
10685
  });
10355
- var _QuestionSchema = z5.object({
10356
- id: z5.string().describe("Unique identifier for the question"),
10357
- question: z5.string().describe("The question to ask the user"),
10358
- answerOptions: z5.array(AnswerOptionSchema).describe("Array of possible answer options"),
10359
- selectedAnswerId: z5.string().optional().describe("The ID of the answer option selected by the user"),
10360
- status: z5.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
10686
+ var _QuestionSchema = z6.object({
10687
+ id: z6.string().describe("Unique identifier for the question"),
10688
+ question: z6.string().describe("The question to ask the user"),
10689
+ answerOptions: z6.array(AnswerOptionSchema).describe("Array of possible answer options"),
10690
+ selectedAnswerId: z6.string().optional().describe("The ID of the answer option selected by the user"),
10691
+ status: z6.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
10361
10692
  });
10362
10693
  var QuestionAskTool = new ExuluTool({
10363
10694
  id: "question_ask",
@@ -10374,9 +10705,9 @@ var QuestionAskTool = new ExuluTool({
10374
10705
  default: questionask_default
10375
10706
  }
10376
10707
  ],
10377
- inputSchema: z5.object({
10378
- question: z5.string().describe("The question to ask the user"),
10379
- answerOptions: z5.array(z5.string()).describe("Array of possible answer options (strings)")
10708
+ inputSchema: z6.object({
10709
+ question: z6.string().describe("The question to ask the user"),
10710
+ answerOptions: z6.array(z6.string()).describe("Array of possible answer options (strings)")
10380
10711
  }),
10381
10712
  execute: async (inputs) => {
10382
10713
  const { sessionID, question, answerOptions, user } = inputs;
@@ -10449,7 +10780,7 @@ var QuestionReadTool = new ExuluTool({
10449
10780
  name: "Question Read",
10450
10781
  needsApproval: false,
10451
10782
  description: "Use this tool to read questions and their answers",
10452
- inputSchema: z6.object({}),
10783
+ inputSchema: z7.object({}),
10453
10784
  type: "function",
10454
10785
  category: "question",
10455
10786
  config: [
@@ -10479,15 +10810,15 @@ async function getQuestions(sessionID) {
10479
10810
  var questionTools = [QuestionAskTool, QuestionReadTool];
10480
10811
 
10481
10812
  // src/templates/tools/perplexity.ts
10482
- import z7 from "zod";
10813
+ import z8 from "zod";
10483
10814
  import Perplexity from "@perplexity-ai/perplexity_ai";
10484
10815
  var internetSearchTool = new ExuluTool({
10485
10816
  id: "internet_search",
10486
10817
  name: "Internet Search",
10487
10818
  description: "Search the internet for information.",
10488
- inputSchema: z7.object({
10489
- query: z7.string().describe("The query to the tool."),
10490
- search_recency_filter: z7.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
10819
+ inputSchema: z8.object({
10820
+ query: z8.string().describe("The query to the tool."),
10821
+ search_recency_filter: z8.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
10491
10822
  }),
10492
10823
  category: "internet_search",
10493
10824
  type: "web_search",
@@ -10565,121 +10896,324 @@ var internetSearchTool = new ExuluTool({
10565
10896
  } catch (error) {
10566
10897
  if (error instanceof Perplexity.RateLimitError && attempt < maxRetries - 1) {
10567
10898
  const delay = Math.pow(2, attempt) * 1e3 + Math.random() * 1e3;
10568
- await new Promise((resolve3) => setTimeout(resolve3, delay));
10899
+ await new Promise((resolve4) => setTimeout(resolve4, delay));
10569
10900
  continue;
10570
10901
  }
10571
10902
  throw error;
10572
10903
  }
10573
10904
  }
10574
10905
  return {
10575
- result: "Max retries exceeded for perplexity research for query."
10906
+ result: "Max retries exceeded for perplexity research for query."
10907
+ };
10908
+ }
10909
+ });
10910
+ var perplexityTools = [internetSearchTool];
10911
+
10912
+ // src/templates/tools/email.ts
10913
+ import * as nodemailer from "nodemailer";
10914
+ import { z as z9 } from "zod";
10915
+ var transporter = null;
10916
+ function getTransporter(config) {
10917
+ if (!transporter) {
10918
+ transporter = nodemailer.createTransport(config);
10919
+ }
10920
+ return transporter;
10921
+ }
10922
+ async function sendEmail(recipient, subject, html, text, config) {
10923
+ const transport = getTransporter(config);
10924
+ html = html.trim();
10925
+ text = text.trim();
10926
+ await transport.sendMail({
10927
+ from: config.from,
10928
+ to: recipient,
10929
+ subject,
10930
+ text,
10931
+ html
10932
+ });
10933
+ }
10934
+ var emailTool = new ExuluTool({
10935
+ id: "email",
10936
+ name: "Email",
10937
+ description: "Send an email.",
10938
+ inputSchema: z9.object({
10939
+ recipient: z9.string().describe("The recipient of the email."),
10940
+ subject: z9.string().describe("The subject of the email."),
10941
+ html: z9.string().describe("The HTML body of the email."),
10942
+ text: z9.string().describe("The text body of the email.")
10943
+ }),
10944
+ type: "function",
10945
+ config: [{
10946
+ name: "smtp_host",
10947
+ description: "The SMTP host to send the email from.",
10948
+ type: "variable",
10949
+ default: void 0
10950
+ }, {
10951
+ name: "smtp_port",
10952
+ description: "The SMTP port to send the email from.",
10953
+ type: "variable",
10954
+ default: void 0
10955
+ }, {
10956
+ name: "smtp_user",
10957
+ description: "The SMTP user to send the email from.",
10958
+ type: "variable",
10959
+ default: void 0
10960
+ }, {
10961
+ name: "smtp_password",
10962
+ description: "The SMTP password to send the email from.",
10963
+ type: "variable",
10964
+ default: void 0
10965
+ }, {
10966
+ name: "smtp_from",
10967
+ description: "The SMTP from address to send the email from.",
10968
+ type: "variable",
10969
+ default: void 0
10970
+ }, {
10971
+ name: "allowed_recipient_domains",
10972
+ description: "A comma seperated list of allowed recipient domains to send emails to.",
10973
+ type: "string",
10974
+ default: void 0
10975
+ }],
10976
+ execute: async ({ recipient, subject, html, text, toolVariablesConfig }) => {
10977
+ const EMAIL_CONFIG = {
10978
+ host: toolVariablesConfig.smtp_host || process.env.SMTP_HOST || "",
10979
+ port: parseInt(toolVariablesConfig.smtp_port || process.env.SMTP_PORT || "587", 10),
10980
+ secure: toolVariablesConfig.smtp_secure === "true" || process.env.SMTP_SECURE === "true",
10981
+ // true for 465, false for other ports
10982
+ auth: {
10983
+ user: toolVariablesConfig.smtp_user || process.env.SMTP_USER || "",
10984
+ pass: toolVariablesConfig.smtp_password || process.env.SMTP_PASSWORD || ""
10985
+ },
10986
+ from: toolVariablesConfig.smtp_from || process.env.SMTP_FROM || "",
10987
+ // Allow self-signed certificates if SMTP_REJECT_UNAUTHORIZED is set to 'false'
10988
+ tls: {
10989
+ rejectUnauthorized: false
10990
+ }
10991
+ };
10992
+ if (toolVariablesConfig.allowed_recipient_domains) {
10993
+ const allowedRecipientDomains = toolVariablesConfig.allowed_recipient_domains.split(",");
10994
+ if (!allowedRecipientDomains.some((domain) => recipient.endsWith(`@${domain}`))) {
10995
+ return {
10996
+ result: "Recipient domain not allowed to send emails to."
10997
+ };
10998
+ }
10999
+ }
11000
+ await sendEmail(recipient, subject, html, text, EMAIL_CONFIG);
11001
+ return {
11002
+ result: "Email sent successfully to " + recipient + " with subject " + subject + "."
11003
+ };
11004
+ }
11005
+ });
11006
+
11007
+ // src/validators/postgres-name.ts
11008
+ var isValidPostgresName = (id) => {
11009
+ const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
11010
+ const isValid = regex.test(id);
11011
+ const length = id.length;
11012
+ return isValid && length <= 80 && length > 2;
11013
+ };
11014
+
11015
+ // src/utils/python-setup.ts
11016
+ import { exec as exec2 } from "child_process";
11017
+ import { promisify as promisify2 } from "util";
11018
+ import { resolve, join as join2, dirname } from "path";
11019
+ import { existsSync as existsSync2, readFileSync } from "fs";
11020
+ import { fileURLToPath } from "url";
11021
+ var execAsync2 = promisify2(exec2);
11022
+ function getPackageRoot() {
11023
+ const currentFile = fileURLToPath(import.meta.url);
11024
+ let currentDir = dirname(currentFile);
11025
+ let attempts = 0;
11026
+ const maxAttempts = 10;
11027
+ while (attempts < maxAttempts) {
11028
+ const packageJsonPath = join2(currentDir, "package.json");
11029
+ if (existsSync2(packageJsonPath)) {
11030
+ try {
11031
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
11032
+ if (packageJson.name === "@exulu/backend") {
11033
+ return currentDir;
11034
+ }
11035
+ } catch {
11036
+ }
11037
+ }
11038
+ const parentDir = resolve(currentDir, "..");
11039
+ if (parentDir === currentDir) {
11040
+ break;
11041
+ }
11042
+ currentDir = parentDir;
11043
+ attempts++;
11044
+ }
11045
+ const fallback = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
11046
+ return fallback;
11047
+ }
11048
+ function getSetupScriptPath(packageRoot) {
11049
+ return resolve(packageRoot, "ee/python/setup.sh");
11050
+ }
11051
+ function getVenvPath(packageRoot) {
11052
+ return resolve(packageRoot, "ee/python/.venv");
11053
+ }
11054
+ function isPythonEnvironmentSetup(packageRoot) {
11055
+ const root = packageRoot ?? getPackageRoot();
11056
+ const venvPath = getVenvPath(root);
11057
+ const pythonPath = join2(venvPath, "bin", "python");
11058
+ return existsSync2(venvPath) && existsSync2(pythonPath);
11059
+ }
11060
+ async function setupPythonEnvironment(options = {}) {
11061
+ const {
11062
+ packageRoot = getPackageRoot(),
11063
+ force = false,
11064
+ verbose = false,
11065
+ timeout = 6e5
11066
+ // 10 minutes
11067
+ } = options;
11068
+ if (!force && isPythonEnvironmentSetup(packageRoot)) {
11069
+ if (verbose) {
11070
+ console.log("\u2713 Python environment already set up");
11071
+ }
11072
+ return {
11073
+ success: true,
11074
+ message: "Python environment already exists",
11075
+ alreadyExists: true
11076
+ };
11077
+ }
11078
+ const setupScriptPath = getSetupScriptPath(packageRoot);
11079
+ if (!existsSync2(setupScriptPath)) {
11080
+ return {
11081
+ success: false,
11082
+ message: `Setup script not found at: ${setupScriptPath}`,
11083
+ alreadyExists: false
11084
+ };
11085
+ }
11086
+ try {
11087
+ if (verbose) {
11088
+ console.log("Setting up Python environment...");
11089
+ }
11090
+ const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
11091
+ cwd: packageRoot,
11092
+ timeout,
11093
+ env: {
11094
+ ...process.env,
11095
+ // Ensure script can write to the directory
11096
+ PYTHONDONTWRITEBYTECODE: "1"
11097
+ },
11098
+ maxBuffer: 10 * 1024 * 1024
11099
+ // 10MB buffer
11100
+ });
11101
+ const output = stdout + stderr;
11102
+ const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
11103
+ const pythonVersion = versionMatch ? versionMatch[1] : void 0;
11104
+ if (verbose) {
11105
+ console.log(output);
11106
+ }
11107
+ return {
11108
+ success: true,
11109
+ message: "Python environment set up successfully",
11110
+ alreadyExists: false,
11111
+ pythonVersion,
11112
+ output
11113
+ };
11114
+ } catch (error) {
11115
+ const errorOutput = error.stdout + error.stderr;
11116
+ return {
11117
+ success: false,
11118
+ message: `Setup failed: ${error.message}`,
11119
+ alreadyExists: false,
11120
+ output: errorOutput
11121
+ };
11122
+ }
11123
+ }
11124
+ function getPythonSetupInstructions() {
11125
+ return `
11126
+ Python environment not set up. Please run one of the following commands:
11127
+
11128
+ Option 1 (Automatic):
11129
+ import { setupPythonEnvironment } from '@exulu/backend';
11130
+ await setupPythonEnvironment();
11131
+
11132
+ Option 2 (Manual - for package consumers):
11133
+ npx @exulu/backend setup-python
11134
+
11135
+ Option 3 (Manual - for contributors):
11136
+ npm run python:setup
11137
+
11138
+ These commands will automatically create a Python virtual environment (.venv)
11139
+ in the @exulu/backend package and install all required dependencies.
11140
+
11141
+ Requirements:
11142
+ - Python 3.10 or higher must be installed
11143
+ - pip must be available
11144
+ - venv module must be available (for creating virtual environments)
11145
+
11146
+ If Python dependencies are not installed, install them first, then run one of the commands above:
11147
+ - macOS: brew install python@3.12
11148
+ - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
11149
+ - Alpine Linux: apk add python3 py3-pip python3-dev
11150
+ - Windows: Download from https://www.python.org/downloads/
11151
+
11152
+ Note: In Docker containers, ensure you install all three components:
11153
+ Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
11154
+ Alpine: apk add python3 py3-pip python3-dev
11155
+ `.trim();
11156
+ }
11157
+ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
11158
+ const root = packageRoot ?? getPackageRoot();
11159
+ const venvPath = getVenvPath(root);
11160
+ const pythonPath = join2(venvPath, "bin", "python");
11161
+ if (!existsSync2(venvPath)) {
11162
+ return {
11163
+ valid: false,
11164
+ message: getPythonSetupInstructions()
10576
11165
  };
10577
11166
  }
10578
- });
10579
- var perplexityTools = [internetSearchTool];
10580
-
10581
- // src/templates/tools/email.ts
10582
- import * as nodemailer from "nodemailer";
10583
- import { z as z8 } from "zod";
10584
- var transporter = null;
10585
- function getTransporter(config) {
10586
- if (!transporter) {
10587
- transporter = nodemailer.createTransport(config);
10588
- }
10589
- return transporter;
10590
- }
10591
- async function sendEmail(recipient, subject, html, text, config) {
10592
- const transport = getTransporter(config);
10593
- html = html.trim();
10594
- text = text.trim();
10595
- await transport.sendMail({
10596
- from: config.from,
10597
- to: recipient,
10598
- subject,
10599
- text,
10600
- html
10601
- });
10602
- }
10603
- var emailTool = new ExuluTool({
10604
- id: "email",
10605
- name: "Email",
10606
- description: "Send an email.",
10607
- inputSchema: z8.object({
10608
- recipient: z8.string().describe("The recipient of the email."),
10609
- subject: z8.string().describe("The subject of the email."),
10610
- html: z8.string().describe("The HTML body of the email."),
10611
- text: z8.string().describe("The text body of the email.")
10612
- }),
10613
- type: "function",
10614
- config: [{
10615
- name: "smtp_host",
10616
- description: "The SMTP host to send the email from.",
10617
- type: "variable",
10618
- default: void 0
10619
- }, {
10620
- name: "smtp_port",
10621
- description: "The SMTP port to send the email from.",
10622
- type: "variable",
10623
- default: void 0
10624
- }, {
10625
- name: "smtp_user",
10626
- description: "The SMTP user to send the email from.",
10627
- type: "variable",
10628
- default: void 0
10629
- }, {
10630
- name: "smtp_password",
10631
- description: "The SMTP password to send the email from.",
10632
- type: "variable",
10633
- default: void 0
10634
- }, {
10635
- name: "smtp_from",
10636
- description: "The SMTP from address to send the email from.",
10637
- type: "variable",
10638
- default: void 0
10639
- }, {
10640
- name: "allowed_recipient_domains",
10641
- description: "A comma seperated list of allowed recipient domains to send emails to.",
10642
- type: "string",
10643
- default: void 0
10644
- }],
10645
- execute: async ({ recipient, subject, html, text, toolVariablesConfig }) => {
10646
- const EMAIL_CONFIG = {
10647
- host: toolVariablesConfig.smtp_host || process.env.SMTP_HOST || "",
10648
- port: parseInt(toolVariablesConfig.smtp_port || process.env.SMTP_PORT || "587", 10),
10649
- secure: toolVariablesConfig.smtp_secure === "true" || process.env.SMTP_SECURE === "true",
10650
- // true for 465, false for other ports
10651
- auth: {
10652
- user: toolVariablesConfig.smtp_user || process.env.SMTP_USER || "",
10653
- pass: toolVariablesConfig.smtp_password || process.env.SMTP_PASSWORD || ""
10654
- },
10655
- from: toolVariablesConfig.smtp_from || process.env.SMTP_FROM || "",
10656
- // Allow self-signed certificates if SMTP_REJECT_UNAUTHORIZED is set to 'false'
10657
- tls: {
10658
- rejectUnauthorized: false
10659
- }
11167
+ if (!existsSync2(pythonPath)) {
11168
+ return {
11169
+ valid: false,
11170
+ message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
10660
11171
  };
10661
- if (toolVariablesConfig.allowed_recipient_domains) {
10662
- const allowedRecipientDomains = toolVariablesConfig.allowed_recipient_domains.split(",");
10663
- if (!allowedRecipientDomains.some((domain) => recipient.endsWith(`@${domain}`))) {
10664
- return {
10665
- result: "Recipient domain not allowed to send emails to."
10666
- };
10667
- }
10668
- }
10669
- await sendEmail(recipient, subject, html, text, EMAIL_CONFIG);
11172
+ }
11173
+ try {
11174
+ await execAsync2(`"${pythonPath}" --version`, { cwd: root });
11175
+ } catch {
10670
11176
  return {
10671
- result: "Email sent successfully to " + recipient + " with subject " + subject + "."
11177
+ valid: false,
11178
+ message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
10672
11179
  };
10673
11180
  }
10674
- });
11181
+ if (checkPackages) {
11182
+ const criticalPackages = ["docling", "transformers"];
11183
+ const missingPackages = [];
11184
+ for (const pkg of criticalPackages) {
11185
+ try {
11186
+ await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
11187
+ cwd: root,
11188
+ timeout: 1e4
11189
+ // 10 second timeout per import check
11190
+ });
11191
+ } catch {
11192
+ missingPackages.push(pkg);
11193
+ }
11194
+ }
11195
+ if (missingPackages.length > 0) {
11196
+ return {
11197
+ valid: false,
11198
+ message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
10675
11199
 
10676
- // src/validators/postgres-name.ts
10677
- var isValidPostgresName = (id) => {
10678
- const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10679
- const isValid = regex.test(id);
10680
- const length = id.length;
10681
- return isValid && length <= 80 && length > 2;
10682
- };
11200
+ This usually happens when:
11201
+ 1. The .venv folder was copied but dependencies were not installed
11202
+ 2. The package was installed via npm but setup script was not run
11203
+
11204
+ Please run:
11205
+ await setupPythonEnvironment({ force: true })
11206
+
11207
+ Or manually run the setup script:
11208
+ bash ` + getSetupScriptPath(root)
11209
+ };
11210
+ }
11211
+ }
11212
+ return {
11213
+ valid: true,
11214
+ message: "Python environment is valid"
11215
+ };
11216
+ }
10683
11217
 
10684
11218
  // src/exulu/app/index.ts
10685
11219
  var isDev = process.env.NODE_ENV !== "production";
@@ -10795,9 +11329,9 @@ var ExuluApp = class {
10795
11329
  id: provider.id ?? "",
10796
11330
  type: "agent"
10797
11331
  })),
10798
- ...this._tools.map((tool) => ({
10799
- name: tool.name ?? "",
10800
- id: tool.id ?? "",
11332
+ ...this._tools.map((tool2) => ({
11333
+ name: tool2.name ?? "",
11334
+ id: tool2.id ?? "",
10801
11335
  type: "tool"
10802
11336
  })),
10803
11337
  ...this._rerankers.map((reranker) => ({
@@ -10839,6 +11373,21 @@ var ExuluApp = class {
10839
11373
  await reportSystemDependencies({
10840
11374
  requireSystemDependencies: config.requireSystemDependencies !== false
10841
11375
  });
11376
+ if (process.env.TRANSCRIPTION_MODEL && !isLiteLLMEnabled()) {
11377
+ console.warn(
11378
+ "[EXULU] TRANSCRIPTION_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /transcribe endpoint will return 503 until LiteLLM is enabled."
11379
+ );
11380
+ }
11381
+ if (process.env.TTS_MODEL && !isLiteLLMEnabled()) {
11382
+ console.warn(
11383
+ "[EXULU] TTS_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /speech endpoint will return 503 until LiteLLM is enabled."
11384
+ );
11385
+ }
11386
+ if (process.env.TTS_MODEL && !process.env.TTS_VOICE) {
11387
+ console.warn(
11388
+ "[EXULU] TTS_MODEL is set but TTS_VOICE is not. LiteLLM's router requires a voice for /v1/audio/speech; the /speech endpoint will return 503 until TTS_VOICE is set (e.g. 'alloy' for OpenAI tts-1)."
11389
+ );
11390
+ }
10842
11391
  console.log("[EXULU] App initialized.");
10843
11392
  exuluApp.set(this);
10844
11393
  return this;
@@ -10850,6 +11399,18 @@ var ExuluApp = class {
10850
11399
  await this.server.express.init();
10851
11400
  console.log("[EXULU] Express app initialized.");
10852
11401
  }
11402
+ if (isLiteLLMEnabled()) {
11403
+ const packageRoot = getPackageRoot();
11404
+ setLiteLLMPackageRoot(packageRoot);
11405
+ try {
11406
+ await startLiteLLMSupervisor();
11407
+ } catch (err) {
11408
+ console.error(
11409
+ "[EXULU] LiteLLM supervisor failed to start:",
11410
+ err.message
11411
+ );
11412
+ }
11413
+ }
10853
11414
  return this._expressApp;
10854
11415
  }
10855
11416
  };
@@ -12062,7 +12623,7 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
12062
12623
  };
12063
12624
 
12064
12625
  // src/exulu/embedder.ts
12065
- import CryptoJS7 from "crypto-js";
12626
+ import CryptoJS6 from "crypto-js";
12066
12627
  var ExuluEmbedder = class {
12067
12628
  id;
12068
12629
  name;
@@ -12133,8 +12694,8 @@ var ExuluEmbedder = class {
12133
12694
  );
12134
12695
  }
12135
12696
  try {
12136
- const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12137
- const decrypted = bytes.toString(CryptoJS7.enc.Utf8);
12697
+ const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
12698
+ const decrypted = bytes.toString(CryptoJS6.enc.Utf8);
12138
12699
  if (!decrypted) {
12139
12700
  throw new Error("Decryption returned empty string - invalid key or corrupted data");
12140
12701
  }
@@ -12610,7 +13171,7 @@ var SentenceChunker = class _SentenceChunker extends BaseChunker {
12610
13171
  }
12611
13172
  };
12612
13173
 
12613
- // src/postgres/init-db.ts
13174
+ // src/postgres/init-exulu-db.ts
12614
13175
  var {
12615
13176
  agentsSchema: agentsSchema2,
12616
13177
  feedbackSchema: feedbackSchema2,
@@ -12620,6 +13181,7 @@ var {
12620
13181
  agentSessionsSchema: agentSessionsSchema2,
12621
13182
  platformConfigurationsSchema: platformConfigurationsSchema2,
12622
13183
  agentMessagesSchema: agentMessagesSchema2,
13184
+ modelsSchema: modelsSchema2,
12623
13185
  rolesSchema: rolesSchema2,
12624
13186
  usersSchema: usersSchema2,
12625
13187
  skillsSchema: skillsSchema2,
@@ -12659,6 +13221,7 @@ var up = async function(knex) {
12659
13221
  const schemas = [
12660
13222
  agentSessionsSchema2(),
12661
13223
  agentMessagesSchema2(),
13224
+ modelsSchema2(),
12662
13225
  rolesSchema2(),
12663
13226
  testCasesSchema2(),
12664
13227
  evalSetsSchema2(),
@@ -12702,6 +13265,47 @@ var up = async function(knex) {
12702
13265
  console.log(`[EXULU] Creating ${schema.name.plural} table.`, schema.fields);
12703
13266
  await createTable(schema);
12704
13267
  }
13268
+ const hasOldProviderCol = await knex.schema.hasColumn("agents", "provider");
13269
+ const hasOldKeyCol = await knex.schema.hasColumn("agents", "providerapikey");
13270
+ if (hasOldProviderCol || hasOldKeyCol) {
13271
+ console.log("[EXULU] Migrating agents.provider/providerapikey -> models table.");
13272
+ await knex.transaction(async (trx) => {
13273
+ const pairs = await trx("agents").distinct("provider", "providerapikey").whereNotNull("provider");
13274
+ const pairToModelId = /* @__PURE__ */ new Map();
13275
+ for (const { provider, providerapikey } of pairs) {
13276
+ const inserted = await trx("models").insert({
13277
+ name: `${provider}${providerapikey ? ` (${providerapikey})` : ""}`,
13278
+ provider,
13279
+ authvariable: providerapikey,
13280
+ active: true,
13281
+ rights_mode: "public",
13282
+ created_by: 1
13283
+ }).returning("id");
13284
+ const id = inserted[0]?.id;
13285
+ if (!id) {
13286
+ throw new Error("[EXULU] Migration: failed to insert models row");
13287
+ }
13288
+ pairToModelId.set(`${provider}::${providerapikey ?? ""}`, id);
13289
+ }
13290
+ for (const [key2, modelId] of pairToModelId) {
13291
+ const [provider, providerapikey] = key2.split("::");
13292
+ const where = {
13293
+ provider,
13294
+ providerapikey: providerapikey ? providerapikey : null
13295
+ };
13296
+ await trx("agents").where(where).update({ model: modelId });
13297
+ }
13298
+ if (hasOldProviderCol) {
13299
+ await trx.schema.alterTable("agents", (t) => t.dropColumn("provider"));
13300
+ }
13301
+ if (hasOldKeyCol) {
13302
+ await trx.schema.alterTable("agents", (t) => t.dropColumn("providerapikey"));
13303
+ }
13304
+ console.log(
13305
+ `[EXULU] Migrated ${pairToModelId.size} unique provider+key pairs into models.`
13306
+ );
13307
+ });
13308
+ }
12705
13309
  if (!await knex.schema.hasTable("verification_token")) {
12706
13310
  console.log("[EXULU] Creating verification_token table.");
12707
13311
  await knex.schema.createTable("verification_token", (table) => {
@@ -12799,39 +13403,310 @@ var execute = async ({ contexts }) => {
12799
13403
  } else {
12800
13404
  adminRoleId = existingAdminRole.id;
12801
13405
  }
12802
- if (!existingDefaultRole) {
12803
- console.log("[EXULU] Creating default role.");
12804
- await db.from("roles").insert({
12805
- name: "default",
12806
- agents: "write",
12807
- api: "read",
12808
- workflows: "read",
12809
- variables: "read",
12810
- users: "read",
12811
- evals: "read"
12812
- }).returning("id");
13406
+ if (!existingDefaultRole) {
13407
+ console.log("[EXULU] Creating default role.");
13408
+ await db.from("roles").insert({
13409
+ name: "default",
13410
+ agents: "write",
13411
+ api: "read",
13412
+ workflows: "read",
13413
+ variables: "read",
13414
+ users: "read",
13415
+ evals: "read"
13416
+ }).returning("id");
13417
+ }
13418
+ const existingUser = await db.from("users").where({ email: "admin@exulu.com" }).first();
13419
+ if (!existingUser) {
13420
+ const password = await encryptString("admin");
13421
+ console.log("[EXULU] Creating default admin user.");
13422
+ await db.from("users").insert({
13423
+ name: "exulu",
13424
+ email: "admin@exulu.com",
13425
+ super_admin: true,
13426
+ createdAt: /* @__PURE__ */ new Date(),
13427
+ emailVerified: /* @__PURE__ */ new Date(),
13428
+ updatedAt: /* @__PURE__ */ new Date(),
13429
+ password,
13430
+ type: "user",
13431
+ role: adminRoleId
13432
+ });
13433
+ }
13434
+ const { key: key2 } = await generateApiKey("exulu", "api@exulu.com");
13435
+ console.log("[EXULU] Database initialized.");
13436
+ console.log("[EXULU] Default api key: ", `${key2}`);
13437
+ console.log("[EXULU] Default password if using password auth: ", `admin`);
13438
+ console.log("[EXULU] Default email if using password auth: ", `admin@exulu.com`);
13439
+ return;
13440
+ };
13441
+
13442
+ // src/exulu/litellm/db-init.ts
13443
+ import { existsSync as existsSync4 } from "fs";
13444
+ import { resolve as resolve2 } from "path";
13445
+ import { spawnSync } from "child_process";
13446
+ import { Client } from "pg";
13447
+
13448
+ // src/exulu/litellm/db-setup-check.ts
13449
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
13450
+ var readLiteLLMDatabaseUrl = (configPath) => {
13451
+ if (!existsSync3(configPath)) return void 0;
13452
+ const text = readFileSync2(configPath, "utf8");
13453
+ const match = text.match(
13454
+ /^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
13455
+ );
13456
+ if (!match) return void 0;
13457
+ const value = match[1]?.trim();
13458
+ if (!value) return void 0;
13459
+ if (value.startsWith("os.environ/")) {
13460
+ const envName = value.slice("os.environ/".length).trim();
13461
+ return process.env[envName];
13462
+ }
13463
+ return value;
13464
+ };
13465
+ var parsePostgresUrl = (url) => {
13466
+ try {
13467
+ const u = new URL(url);
13468
+ if (u.protocol !== "postgres:" && u.protocol !== "postgresql:") return void 0;
13469
+ return {
13470
+ host: u.hostname,
13471
+ port: u.port ? parseInt(u.port, 10) : 5432,
13472
+ database: u.pathname.replace(/^\//, "")
13473
+ };
13474
+ } catch {
13475
+ return void 0;
13476
+ }
13477
+ };
13478
+ var getExuluPostgresTarget = () => {
13479
+ const host = process.env.POSTGRES_DB_HOST;
13480
+ const database = process.env.POSTGRES_DB_NAME ?? "exulu";
13481
+ if (!host) return void 0;
13482
+ return {
13483
+ host,
13484
+ port: parseInt(process.env.POSTGRES_DB_PORT ?? "5432", 10),
13485
+ database
13486
+ };
13487
+ };
13488
+ var isSameDatabase = (a, b) => a.host === b.host && a.port === b.port && a.database === b.database;
13489
+ var checkLiteLLMDatabaseSafety = (configPath) => {
13490
+ const litellmUrl = readLiteLLMDatabaseUrl(configPath);
13491
+ if (!litellmUrl) return { ok: true, reason: "no-litellm-db-mode" };
13492
+ const litellmTarget = parsePostgresUrl(litellmUrl);
13493
+ if (!litellmTarget) return { ok: false, reason: "unparseable-url", rawUrl: litellmUrl };
13494
+ const exuluTarget = getExuluPostgresTarget();
13495
+ if (exuluTarget && isSameDatabase(litellmTarget, exuluTarget)) {
13496
+ return { ok: false, reason: "shared-with-exulu", litellmTarget, exuluTarget };
13497
+ }
13498
+ return { ok: true, reason: "isolated", litellmTarget };
13499
+ };
13500
+
13501
+ // src/exulu/litellm/db-init.ts
13502
+ var WARNING_BANNER = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
13503
+ var warn = (lines) => {
13504
+ console.warn(`
13505
+ ${WARNING_BANNER}`);
13506
+ console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
13507
+ for (const line of lines) console.warn(` ${line}`);
13508
+ console.warn(`${WARNING_BANNER}
13509
+ `);
13510
+ };
13511
+ var log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
13512
+ var initLiteLLMDatabase = async (packageRoot) => {
13513
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(packageRoot, "./config.litellm.yaml");
13514
+ const safety = checkLiteLLMDatabaseSafety(configPath);
13515
+ if (safety.ok && safety.reason === "no-litellm-db-mode") return;
13516
+ if (!safety.ok && safety.reason === "unparseable-url") {
13517
+ warn([
13518
+ `LiteLLM's database_url is not a valid postgres URL:`,
13519
+ ` ${safety.rawUrl}`,
13520
+ `Expected postgres://user:pass@host:port/database. Skipping setup.`
13521
+ ]);
13522
+ return;
13523
+ }
13524
+ if (!safety.ok && safety.reason === "shared-with-exulu") {
13525
+ const { exuluTarget } = safety;
13526
+ warn([
13527
+ `LiteLLM's database_url points to the SAME database Exulu uses:`,
13528
+ ` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
13529
+ ``,
13530
+ `If LiteLLM's schema sync were to run against this database, it`,
13531
+ `would DROP every table that is not in LiteLLM's Prisma schema,`,
13532
+ `destroying all of Exulu's data. Setup has been SKIPPED.`,
13533
+ ``,
13534
+ `Fix: create a dedicated Postgres database for LiteLLM (e.g.`,
13535
+ `\`createdb litellm\`) and change database_url in ${configPath}`,
13536
+ `to point at it. Then restart Exulu.`
13537
+ ]);
13538
+ return;
13539
+ }
13540
+ const litellmUrl = readLiteLLMDatabaseUrl(configPath);
13541
+ if (!litellmUrl) {
13542
+ return;
13543
+ }
13544
+ const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
13545
+ log(
13546
+ `LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
13547
+ );
13548
+ const ensureDatabaseExists = async () => {
13549
+ const probe = new Client({ connectionString: litellmUrl });
13550
+ try {
13551
+ await probe.connect();
13552
+ await probe.end();
13553
+ return true;
13554
+ } catch (err) {
13555
+ const code = err?.code;
13556
+ if (code !== "3D000") {
13557
+ warn([
13558
+ `Could not connect to LiteLLM's target database:`,
13559
+ ` ${err instanceof Error ? err.message : String(err)}`,
13560
+ ``,
13561
+ `Skipping LiteLLM database setup. LiteLLM features that depend on`,
13562
+ `the database will fail at runtime until this is resolved.`
13563
+ ]);
13564
+ return false;
13565
+ }
13566
+ const url = new URL(litellmUrl);
13567
+ const targetDbName = url.pathname.replace(/^\//, "");
13568
+ if (!targetDbName) {
13569
+ warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
13570
+ return false;
13571
+ }
13572
+ url.pathname = "/postgres";
13573
+ log(`Target database "${targetDbName}" does not exist; creating it\u2026`);
13574
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
13575
+ warn([
13576
+ `Refusing to auto-create database "${targetDbName}" \u2014 name`,
13577
+ `contains characters that would require quoting. Create it`,
13578
+ `manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
13579
+ ]);
13580
+ return false;
13581
+ }
13582
+ const admin = new Client({ connectionString: url.toString() });
13583
+ try {
13584
+ await admin.connect();
13585
+ await admin.query(`CREATE DATABASE "${targetDbName}"`);
13586
+ log(`\u2713 Created database "${targetDbName}".`);
13587
+ return true;
13588
+ } catch (createErr) {
13589
+ warn([
13590
+ `Failed to auto-create database "${targetDbName}":`,
13591
+ ` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
13592
+ ``,
13593
+ `The connecting user likely lacks CREATEDB privilege. Create the`,
13594
+ `database manually:`,
13595
+ ` createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`,
13596
+ `Then restart Exulu.`
13597
+ ]);
13598
+ return false;
13599
+ } finally {
13600
+ try {
13601
+ await admin.end();
13602
+ } catch {
13603
+ }
13604
+ }
13605
+ }
13606
+ };
13607
+ if (!await ensureDatabaseExists()) return;
13608
+ log("Checking that the target database is safe to push into\u2026");
13609
+ const client2 = new Client({ connectionString: litellmUrl });
13610
+ let foreignTables = [];
13611
+ try {
13612
+ await client2.connect();
13613
+ const res = await client2.query(
13614
+ `SELECT table_name
13615
+ FROM information_schema.tables
13616
+ WHERE table_schema = 'public'
13617
+ AND table_type = 'BASE TABLE'
13618
+ AND table_name NOT LIKE 'LiteLLM%'
13619
+ AND table_name <> '_prisma_migrations'
13620
+ ORDER BY table_name;`
13621
+ );
13622
+ foreignTables = res.rows.map((r) => r.table_name);
13623
+ } catch (err) {
13624
+ warn([
13625
+ `Could not query LiteLLM's target database to verify it is safe`,
13626
+ `to push into:`,
13627
+ ` ${err instanceof Error ? err.message : String(err)}`,
13628
+ ``,
13629
+ `Skipping LiteLLM database setup. LiteLLM features that depend on the`,
13630
+ `database will fail at runtime until this is resolved.`
13631
+ ]);
13632
+ return;
13633
+ } finally {
13634
+ try {
13635
+ await client2.end();
13636
+ } catch {
13637
+ }
13638
+ }
13639
+ if (foreignTables.length > 0) {
13640
+ warn([
13641
+ `LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
13642
+ `part of LiteLLM's schema:`,
13643
+ ...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
13644
+ ...foreignTables.length > 10 ? [` \u2026 and ${foreignTables.length - 10} more`] : [],
13645
+ ``,
13646
+ `Refusing to run prisma db push to avoid data loss. Move LiteLLM to a`,
13647
+ `dedicated database, or remove these tables first if they are obsolete.`
13648
+ ]);
13649
+ return;
13650
+ }
13651
+ const venvBin = resolve2(packageRoot, "ee/python/.venv/bin");
13652
+ const prismaCli = resolve2(venvBin, "prisma");
13653
+ const litellmProxyDir = resolve2(
13654
+ packageRoot,
13655
+ "ee/python/.venv/lib/python3.12/site-packages/litellm/proxy"
13656
+ );
13657
+ const schemaPath = resolve2(litellmProxyDir, "schema.prisma");
13658
+ if (!existsSync4(prismaCli)) {
13659
+ warn([
13660
+ `Prisma CLI not found at ${prismaCli}.`,
13661
+ `Run \`npm run python:setup\` to create the venv and install prisma.`,
13662
+ `Skipping LiteLLM database setup.`
13663
+ ]);
13664
+ return;
12813
13665
  }
12814
- const existingUser = await db.from("users").where({ email: "admin@exulu.com" }).first();
12815
- if (!existingUser) {
12816
- const password = await encryptString("admin");
12817
- console.log("[EXULU] Creating default admin user.");
12818
- await db.from("users").insert({
12819
- name: "exulu",
12820
- email: "admin@exulu.com",
12821
- super_admin: true,
12822
- createdAt: /* @__PURE__ */ new Date(),
12823
- emailVerified: /* @__PURE__ */ new Date(),
12824
- updatedAt: /* @__PURE__ */ new Date(),
12825
- password,
12826
- type: "user",
12827
- role: adminRoleId
12828
- });
13666
+ if (!existsSync4(schemaPath)) {
13667
+ warn([
13668
+ `LiteLLM Prisma schema not found at ${schemaPath}.`,
13669
+ `Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
13670
+ ]);
13671
+ return;
12829
13672
  }
12830
- const { key: key2 } = await generateApiKey("exulu", "api@exulu.com");
12831
- console.log("[EXULU] Database initialized.");
12832
- console.log("[EXULU] Default api key: ", `${key2}`);
12833
- console.log("[EXULU] Default password if using password auth: ", `admin`);
12834
- console.log("[EXULU] Default email if using password auth: ", `admin@exulu.com`);
13673
+ log("Running `prisma db push` against LiteLLM's schema\u2026");
13674
+ const result = spawnSync(prismaCli, ["db", "push", "--skip-generate"], {
13675
+ cwd: litellmProxyDir,
13676
+ env: {
13677
+ ...process.env,
13678
+ DATABASE_URL: litellmUrl,
13679
+ PATH: `${venvBin}:${process.env.PATH ?? ""}`
13680
+ },
13681
+ // Capture rather than inherit so the prisma noise doesn't bury other
13682
+ // Exulu boot logs; we'll print stdout/stderr only on failure.
13683
+ stdio: ["ignore", "pipe", "pipe"],
13684
+ encoding: "utf8"
13685
+ });
13686
+ if (result.error) {
13687
+ warn([
13688
+ `Failed to launch prisma: ${result.error.message}`,
13689
+ `Skipping LiteLLM database setup.`
13690
+ ]);
13691
+ return;
13692
+ }
13693
+ if (result.status !== 0) {
13694
+ warn([
13695
+ `prisma db push exited with status ${result.status}.`,
13696
+ `stdout:`,
13697
+ ...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
13698
+ `stderr:`,
13699
+ ...(result.stderr || "(empty)").split("\n").map((l) => ` ${l}`)
13700
+ ]);
13701
+ return;
13702
+ }
13703
+ log("\u2713 LiteLLM database ready.");
13704
+ };
13705
+
13706
+ // src/postgres/init-litellm-db.ts
13707
+ var initLitellmDb = async () => {
13708
+ await initLiteLLMDatabase(getPackageRoot());
13709
+ console.log("[EXULU] LiteLLM database initialized.");
12835
13710
  return;
12836
13711
  };
12837
13712
 
@@ -12878,7 +13753,7 @@ var create = ({
12878
13753
  };
12879
13754
 
12880
13755
  // src/index.ts
12881
- import CryptoJS8 from "crypto-js";
13756
+ import CryptoJS7 from "crypto-js";
12882
13757
 
12883
13758
  // ee/chunking/markdown.ts
12884
13759
  var extractPageTag = (text) => {
@@ -13358,214 +14233,11 @@ var MarkdownChunker = class {
13358
14233
  }
13359
14234
  };
13360
14235
 
13361
- // src/utils/python-setup.ts
13362
- import { exec as exec2 } from "child_process";
13363
- import { promisify as promisify2 } from "util";
13364
- import { resolve, join as join2, dirname } from "path";
13365
- import { existsSync as existsSync2, readFileSync } from "fs";
13366
- import { fileURLToPath } from "url";
13367
- var execAsync2 = promisify2(exec2);
13368
- function getPackageRoot() {
13369
- const currentFile = fileURLToPath(import.meta.url);
13370
- let currentDir = dirname(currentFile);
13371
- let attempts = 0;
13372
- const maxAttempts = 10;
13373
- while (attempts < maxAttempts) {
13374
- const packageJsonPath = join2(currentDir, "package.json");
13375
- if (existsSync2(packageJsonPath)) {
13376
- try {
13377
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
13378
- if (packageJson.name === "@exulu/backend") {
13379
- return currentDir;
13380
- }
13381
- } catch {
13382
- }
13383
- }
13384
- const parentDir = resolve(currentDir, "..");
13385
- if (parentDir === currentDir) {
13386
- break;
13387
- }
13388
- currentDir = parentDir;
13389
- attempts++;
13390
- }
13391
- const fallback = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
13392
- return fallback;
13393
- }
13394
- function getSetupScriptPath(packageRoot) {
13395
- return resolve(packageRoot, "ee/python/setup.sh");
13396
- }
13397
- function getVenvPath(packageRoot) {
13398
- return resolve(packageRoot, "ee/python/.venv");
13399
- }
13400
- function isPythonEnvironmentSetup(packageRoot) {
13401
- const root = packageRoot ?? getPackageRoot();
13402
- const venvPath = getVenvPath(root);
13403
- const pythonPath = join2(venvPath, "bin", "python");
13404
- return existsSync2(venvPath) && existsSync2(pythonPath);
13405
- }
13406
- async function setupPythonEnvironment(options = {}) {
13407
- const {
13408
- packageRoot = getPackageRoot(),
13409
- force = false,
13410
- verbose = false,
13411
- timeout = 6e5
13412
- // 10 minutes
13413
- } = options;
13414
- if (!force && isPythonEnvironmentSetup(packageRoot)) {
13415
- if (verbose) {
13416
- console.log("\u2713 Python environment already set up");
13417
- }
13418
- return {
13419
- success: true,
13420
- message: "Python environment already exists",
13421
- alreadyExists: true
13422
- };
13423
- }
13424
- const setupScriptPath = getSetupScriptPath(packageRoot);
13425
- if (!existsSync2(setupScriptPath)) {
13426
- return {
13427
- success: false,
13428
- message: `Setup script not found at: ${setupScriptPath}`,
13429
- alreadyExists: false
13430
- };
13431
- }
13432
- try {
13433
- if (verbose) {
13434
- console.log("Setting up Python environment...");
13435
- }
13436
- const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
13437
- cwd: packageRoot,
13438
- timeout,
13439
- env: {
13440
- ...process.env,
13441
- // Ensure script can write to the directory
13442
- PYTHONDONTWRITEBYTECODE: "1"
13443
- },
13444
- maxBuffer: 10 * 1024 * 1024
13445
- // 10MB buffer
13446
- });
13447
- const output = stdout + stderr;
13448
- const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
13449
- const pythonVersion = versionMatch ? versionMatch[1] : void 0;
13450
- if (verbose) {
13451
- console.log(output);
13452
- }
13453
- return {
13454
- success: true,
13455
- message: "Python environment set up successfully",
13456
- alreadyExists: false,
13457
- pythonVersion,
13458
- output
13459
- };
13460
- } catch (error) {
13461
- const errorOutput = error.stdout + error.stderr;
13462
- return {
13463
- success: false,
13464
- message: `Setup failed: ${error.message}`,
13465
- alreadyExists: false,
13466
- output: errorOutput
13467
- };
13468
- }
13469
- }
13470
- function getPythonSetupInstructions() {
13471
- return `
13472
- Python environment not set up. Please run one of the following commands:
13473
-
13474
- Option 1 (Automatic):
13475
- import { setupPythonEnvironment } from '@exulu/backend';
13476
- await setupPythonEnvironment();
13477
-
13478
- Option 2 (Manual - for package consumers):
13479
- npx @exulu/backend setup-python
13480
-
13481
- Option 3 (Manual - for contributors):
13482
- npm run python:setup
13483
-
13484
- These commands will automatically create a Python virtual environment (.venv)
13485
- in the @exulu/backend package and install all required dependencies.
13486
-
13487
- Requirements:
13488
- - Python 3.10 or higher must be installed
13489
- - pip must be available
13490
- - venv module must be available (for creating virtual environments)
13491
-
13492
- If Python dependencies are not installed, install them first, then run one of the commands above:
13493
- - macOS: brew install python@3.12
13494
- - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
13495
- - Alpine Linux: apk add python3 py3-pip python3-dev
13496
- - Windows: Download from https://www.python.org/downloads/
13497
-
13498
- Note: In Docker containers, ensure you install all three components:
13499
- Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
13500
- Alpine: apk add python3 py3-pip python3-dev
13501
- `.trim();
13502
- }
13503
- async function validatePythonEnvironment(packageRoot, checkPackages = true) {
13504
- const root = packageRoot ?? getPackageRoot();
13505
- const venvPath = getVenvPath(root);
13506
- const pythonPath = join2(venvPath, "bin", "python");
13507
- if (!existsSync2(venvPath)) {
13508
- return {
13509
- valid: false,
13510
- message: getPythonSetupInstructions()
13511
- };
13512
- }
13513
- if (!existsSync2(pythonPath)) {
13514
- return {
13515
- valid: false,
13516
- message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
13517
- };
13518
- }
13519
- try {
13520
- await execAsync2(`"${pythonPath}" --version`, { cwd: root });
13521
- } catch {
13522
- return {
13523
- valid: false,
13524
- message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
13525
- };
13526
- }
13527
- if (checkPackages) {
13528
- const criticalPackages = ["docling", "transformers"];
13529
- const missingPackages = [];
13530
- for (const pkg of criticalPackages) {
13531
- try {
13532
- await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
13533
- cwd: root,
13534
- timeout: 1e4
13535
- // 10 second timeout per import check
13536
- });
13537
- } catch {
13538
- missingPackages.push(pkg);
13539
- }
13540
- }
13541
- if (missingPackages.length > 0) {
13542
- return {
13543
- valid: false,
13544
- message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
13545
-
13546
- This usually happens when:
13547
- 1. The .venv folder was copied but dependencies were not installed
13548
- 2. The package was installed via npm but setup script was not run
13549
-
13550
- Please run:
13551
- await setupPythonEnvironment({ force: true })
13552
-
13553
- Or manually run the setup script:
13554
- bash ` + getSetupScriptPath(root)
13555
- };
13556
- }
13557
- }
13558
- return {
13559
- valid: true,
13560
- message: "Python environment is valid"
13561
- };
13562
- }
13563
-
13564
14236
  // ee/python/documents/processing/doc_processor.ts
13565
- import * as fs3 from "fs";
14237
+ import * as fs4 from "fs";
13566
14238
  import * as path from "path";
13567
- import { generateText as generateText4, Output as Output2 } from "ai";
13568
- import { z as z9 } from "zod";
14239
+ import { generateText as generateText5, Output as Output2 } from "ai";
14240
+ import { z as z10 } from "zod";
13569
14241
  import pLimit from "p-limit";
13570
14242
  import { randomUUID as randomUUID5 } from "crypto";
13571
14243
  import * as mammoth from "mammoth";
@@ -13576,8 +14248,8 @@ import { parseOfficeAsync as parseOfficeAsync2 } from "officeparser";
13576
14248
  // src/utils/python-executor.ts
13577
14249
  import { exec as exec3 } from "child_process";
13578
14250
  import { promisify as promisify3 } from "util";
13579
- import { resolve as resolve2, join as join3, dirname as dirname2 } from "path";
13580
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
14251
+ import { resolve as resolve3, join as join3, dirname as dirname2 } from "path";
14252
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
13581
14253
  import { fileURLToPath as fileURLToPath2 } from "url";
13582
14254
  var execAsync3 = promisify3(exec3);
13583
14255
  function getPackageRoot2() {
@@ -13587,23 +14259,23 @@ function getPackageRoot2() {
13587
14259
  const maxAttempts = 10;
13588
14260
  while (attempts < maxAttempts) {
13589
14261
  const packageJsonPath = join3(currentDir, "package.json");
13590
- if (existsSync3(packageJsonPath)) {
14262
+ if (existsSync5(packageJsonPath)) {
13591
14263
  try {
13592
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
14264
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
13593
14265
  if (packageJson.name === "@exulu/backend") {
13594
14266
  return currentDir;
13595
14267
  }
13596
14268
  } catch {
13597
14269
  }
13598
14270
  }
13599
- const parentDir = resolve2(currentDir, "..");
14271
+ const parentDir = resolve3(currentDir, "..");
13600
14272
  if (parentDir === currentDir) {
13601
14273
  break;
13602
14274
  }
13603
14275
  currentDir = parentDir;
13604
14276
  attempts++;
13605
14277
  }
13606
- return resolve2(dirname2(fileURLToPath2(import.meta.url)), "../..");
14278
+ return resolve3(dirname2(fileURLToPath2(import.meta.url)), "../..");
13607
14279
  }
13608
14280
  var PythonEnvironmentError = class extends Error {
13609
14281
  constructor(message) {
@@ -13624,7 +14296,7 @@ var PythonExecutionError = class extends Error {
13624
14296
  }
13625
14297
  };
13626
14298
  function getVenvPath2(packageRoot) {
13627
- return resolve2(packageRoot, "ee/python/.venv");
14299
+ return resolve3(packageRoot, "ee/python/.venv");
13628
14300
  }
13629
14301
  function getPythonExecutable(packageRoot) {
13630
14302
  const venvPath = getVenvPath2(packageRoot);
@@ -13650,8 +14322,8 @@ async function executePythonScript(config) {
13650
14322
  if (validateEnvironment) {
13651
14323
  await validatePythonEnvironmentForExecution(packageRoot);
13652
14324
  }
13653
- const resolvedScriptPath = resolve2(packageRoot, scriptPath);
13654
- if (!existsSync3(resolvedScriptPath)) {
14325
+ const resolvedScriptPath = resolve3(packageRoot, scriptPath);
14326
+ if (!existsSync5(resolvedScriptPath)) {
13655
14327
  throw new PythonExecutionError(
13656
14328
  `Python script not found: ${resolvedScriptPath}`,
13657
14329
  "",
@@ -13746,9 +14418,9 @@ async function processWord(file) {
13746
14418
  }
13747
14419
  async function processImage(buffer, paths, config, verbose = false) {
13748
14420
  try {
13749
- await fs3.promises.mkdir(paths.images, { recursive: true });
14421
+ await fs4.promises.mkdir(paths.images, { recursive: true });
13750
14422
  const imagePath = path.join(paths.images, "1.png");
13751
- await fs3.promises.writeFile(imagePath, buffer);
14423
+ await fs4.promises.writeFile(imagePath, buffer);
13752
14424
  console.log(`[EXULU] Image saved to: ${imagePath}`);
13753
14425
  let json = [{
13754
14426
  page: 1,
@@ -13765,7 +14437,7 @@ async function processImage(buffer, paths, config, verbose = false) {
13765
14437
  verbose,
13766
14438
  config.vlm.concurrency
13767
14439
  );
13768
- await fs3.promises.writeFile(
14440
+ await fs4.promises.writeFile(
13769
14441
  paths.json,
13770
14442
  JSON.stringify(json, null, 2),
13771
14443
  "utf-8"
@@ -13776,14 +14448,14 @@ async function processImage(buffer, paths, config, verbose = false) {
13776
14448
  } else {
13777
14449
  console.log("[EXULU] No VLM configured, image saved without content extraction");
13778
14450
  console.log("[EXULU] Note: Enable VLM in config to extract text/content from images");
13779
- await fs3.promises.writeFile(
14451
+ await fs4.promises.writeFile(
13780
14452
  paths.json,
13781
14453
  JSON.stringify(json, null, 2),
13782
14454
  "utf-8"
13783
14455
  );
13784
14456
  }
13785
14457
  const markdown = json.map((p) => p.vlm_corrected_text ?? p.content).join("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
13786
- await fs3.promises.writeFile(paths.markdown, markdown, "utf-8");
14458
+ await fs4.promises.writeFile(paths.markdown, markdown, "utf-8");
13787
14459
  return {
13788
14460
  markdown,
13789
14461
  json
@@ -13836,7 +14508,7 @@ function reconstructHeadings(correctedText, headingsHierarchy) {
13836
14508
  return result;
13837
14509
  }
13838
14510
  async function validatePageWithVLM(page, imagePath, model) {
13839
- const imageBuffer = await fs3.promises.readFile(imagePath);
14511
+ const imageBuffer = await fs4.promises.readFile(imagePath);
13840
14512
  const imageBase64 = imageBuffer.toString("base64");
13841
14513
  const mimeType = "image/png";
13842
14514
  const prompt = `You are a document validation assistant. Your task is to analyze a page image and correct the output of an OCR/parsing pipeline. The content may include tables, technical diagrams, schematics, and structured text.
@@ -13914,18 +14586,18 @@ If the page contains a flow-chart, schematic, technical drawing or control board
13914
14586
 
13915
14587
  ### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
13916
14588
  `;
13917
- const result = await generateText4({
14589
+ const result = await generateText5({
13918
14590
  model,
13919
14591
  output: Output2.object({
13920
- schema: z9.object({
13921
- needs_correction: z9.boolean(),
13922
- corrected_text: z9.string().nullable(),
13923
- current_page_table: z9.object({
13924
- headers: z9.array(z9.string()),
13925
- is_continuation: z9.boolean()
14592
+ schema: z10.object({
14593
+ needs_correction: z10.boolean(),
14594
+ corrected_text: z10.string().nullable(),
14595
+ current_page_table: z10.object({
14596
+ headers: z10.array(z10.string()),
14597
+ is_continuation: z10.boolean()
13926
14598
  }).nullable(),
13927
- confidence: z9.enum(["high", "medium", "low"]),
13928
- reasoning: z9.string()
14599
+ confidence: z10.enum(["high", "medium", "low"]),
14600
+ reasoning: z10.string()
13929
14601
  })
13930
14602
  }),
13931
14603
  messages: [
@@ -14005,7 +14677,7 @@ async function validateWithVLM(document, model, verbose = false, concurrency = 1
14005
14677
  let correctedCount = 0;
14006
14678
  const validationTasks = document.map(
14007
14679
  (page) => limit(async () => {
14008
- await new Promise((resolve3) => setImmediate(resolve3));
14680
+ await new Promise((resolve4) => setImmediate(resolve4));
14009
14681
  const imagePath = page.image;
14010
14682
  if (!imagePath) {
14011
14683
  console.warn(`[EXULU] Page ${page.page}: No image found, skipping validation`);
@@ -14179,7 +14851,7 @@ ${setupResult.output || ""}`);
14179
14851
  if (!result.success) {
14180
14852
  throw new Error(`Document processing failed: ${result.stderr}`);
14181
14853
  }
14182
- const jsonContent = await fs3.promises.readFile(paths.json, "utf-8");
14854
+ const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
14183
14855
  json = JSON.parse(jsonContent);
14184
14856
  } else if (config?.processor.name === "officeparser") {
14185
14857
  const text = await parseOfficeAsync2(buffer, {
@@ -14196,7 +14868,7 @@ ${setupResult.output || ""}`);
14196
14868
  if (!MISTRAL_API_KEY) {
14197
14869
  throw new Error('[EXULU] MISTRAL_API_KEY is not set, please set it in the environment variable via process.env or via an Exulu variable named "MISTRAL_API_KEY".');
14198
14870
  }
14199
- await new Promise((resolve3) => setTimeout(resolve3, Math.floor(Math.random() * 4e3) + 1e3));
14871
+ await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 4e3) + 1e3));
14200
14872
  const base64Pdf = buffer.toString("base64");
14201
14873
  const client2 = new Mistral({ apiKey: MISTRAL_API_KEY });
14202
14874
  const ocrResponse = await withRetry(async () => {
@@ -14212,9 +14884,9 @@ ${setupResult.output || ""}`);
14212
14884
  }, 10);
14213
14885
  const parser = new LiteParse();
14214
14886
  const screenshots = await parser.screenshot(paths.source, void 0);
14215
- await fs3.promises.mkdir(paths.images, { recursive: true });
14887
+ await fs4.promises.mkdir(paths.images, { recursive: true });
14216
14888
  for (const screenshot of screenshots) {
14217
- await fs3.promises.writeFile(
14889
+ await fs4.promises.writeFile(
14218
14890
  path.join(
14219
14891
  paths.images,
14220
14892
  `${screenshot.pageNum}.png`
@@ -14229,15 +14901,15 @@ ${setupResult.output || ""}`);
14229
14901
  image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
14230
14902
  headings: []
14231
14903
  }));
14232
- fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
14904
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
14233
14905
  } else if (config?.processor.name === "liteparse") {
14234
14906
  const parser = new LiteParse();
14235
14907
  const result = await parser.parse(paths.source);
14236
14908
  const screenshots = await parser.screenshot(paths.source, void 0);
14237
14909
  console.log(`[EXULU] Liteparse screenshots: ${JSON.stringify(screenshots)}`);
14238
- await fs3.promises.mkdir(paths.images, { recursive: true });
14910
+ await fs4.promises.mkdir(paths.images, { recursive: true });
14239
14911
  for (const screenshot of screenshots) {
14240
- await fs3.promises.writeFile(path.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
14912
+ await fs4.promises.writeFile(path.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
14241
14913
  screenshot.imagePath = path.join(paths.images, `${screenshot.pageNum}.png`);
14242
14914
  }
14243
14915
  json = result.pages.map((page) => ({
@@ -14245,7 +14917,7 @@ ${setupResult.output || ""}`);
14245
14917
  content: page.text,
14246
14918
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
14247
14919
  }));
14248
- fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
14920
+ fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
14249
14921
  }
14250
14922
  console.log(`[EXULU]
14251
14923
  \u2713 Document processing completed successfully`);
@@ -14276,13 +14948,13 @@ ${setupResult.output || ""}`);
14276
14948
  console.log(`[EXULU] Corrected: ${page.vlm_corrected_text.substring(0, 150)}...`);
14277
14949
  });
14278
14950
  }
14279
- await fs3.promises.writeFile(
14951
+ await fs4.promises.writeFile(
14280
14952
  paths.json,
14281
14953
  JSON.stringify(json, null, 2),
14282
14954
  "utf-8"
14283
14955
  );
14284
14956
  }
14285
- const markdownStream = fs3.createWriteStream(paths.markdown, { encoding: "utf-8" });
14957
+ const markdownStream = fs4.createWriteStream(paths.markdown, { encoding: "utf-8" });
14286
14958
  for (let i = 0; i < json.length; i++) {
14287
14959
  const p = json[i];
14288
14960
  if (!p) continue;
@@ -14292,13 +14964,13 @@ ${setupResult.output || ""}`);
14292
14964
  markdownStream.write("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
14293
14965
  }
14294
14966
  }
14295
- await new Promise((resolve3, reject) => {
14296
- markdownStream.end(() => resolve3());
14967
+ await new Promise((resolve4, reject) => {
14968
+ markdownStream.end(() => resolve4());
14297
14969
  markdownStream.on("error", reject);
14298
14970
  });
14299
14971
  console.log(`[EXULU] Validated output saved to: ${paths.json}`);
14300
14972
  console.log(`[EXULU] Validated markdown saved to: ${paths.markdown}`);
14301
- const markdown = await fs3.promises.readFile(paths.markdown, "utf-8");
14973
+ const markdown = await fs4.promises.readFile(paths.markdown, "utf-8");
14302
14974
  const processedJson = json.map((e) => {
14303
14975
  const finalContent = e.vlm_corrected_text ?? e.content;
14304
14976
  return {
@@ -14329,7 +15001,7 @@ var loadFile = async (file, name, tempDir) => {
14329
15001
  let buffer;
14330
15002
  if (Buffer.isBuffer(file)) {
14331
15003
  filePath = path.join(tempDir, `${UUID}.${fileType}`);
14332
- await fs3.promises.writeFile(filePath, file);
15004
+ await fs4.promises.writeFile(filePath, file);
14333
15005
  buffer = file;
14334
15006
  } else {
14335
15007
  filePath = filePath.trim();
@@ -14337,11 +15009,11 @@ var loadFile = async (file, name, tempDir) => {
14337
15009
  const response = await fetch(filePath);
14338
15010
  const array = await response.arrayBuffer();
14339
15011
  const tempFilePath = path.join(tempDir, `${UUID}.${fileType}`);
14340
- await fs3.promises.writeFile(tempFilePath, Buffer.from(array));
15012
+ await fs4.promises.writeFile(tempFilePath, Buffer.from(array));
14341
15013
  buffer = Buffer.from(array);
14342
15014
  filePath = tempFilePath;
14343
15015
  } else {
14344
- buffer = await fs3.promises.readFile(file);
15016
+ buffer = await fs4.promises.readFile(file);
14345
15017
  }
14346
15018
  }
14347
15019
  return { filePath, fileType, buffer };
@@ -14359,9 +15031,9 @@ async function documentProcessor({
14359
15031
  const tempDir = path.join(process.cwd(), "temp", uuid);
14360
15032
  const localFilesAndFoldersToDelete = [tempDir];
14361
15033
  console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
14362
- await fs3.promises.mkdir(tempDir, { recursive: true });
15034
+ await fs4.promises.mkdir(tempDir, { recursive: true });
14363
15035
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
14364
- await fs3.promises.writeFile(path.join(tempDir, "created_at.txt"), timestamp);
15036
+ await fs4.promises.writeFile(path.join(tempDir, "created_at.txt"), timestamp);
14365
15037
  try {
14366
15038
  const {
14367
15039
  filePath,
@@ -14402,7 +15074,7 @@ async function documentProcessor({
14402
15074
  if (config?.debugging?.deleteTempFiles !== false) {
14403
15075
  for (const file2 of localFilesAndFoldersToDelete) {
14404
15076
  try {
14405
- await fs3.promises.rm(file2, { recursive: true });
15077
+ await fs4.promises.rm(file2, { recursive: true });
14406
15078
  console.log(`[EXULU] Deleted file or folder: ${file2}`);
14407
15079
  } catch (error) {
14408
15080
  console.error(`[EXULU] Error deleting file or folder: ${file2}`, error);
@@ -14463,8 +15135,8 @@ var ExuluVariables = {
14463
15135
  throw new Error(`Variable ${name} not found.`);
14464
15136
  }
14465
15137
  if (variable.encrypted) {
14466
- const bytes = CryptoJS8.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
14467
- variable.value = bytes.toString(CryptoJS8.enc.Utf8);
15138
+ const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
15139
+ variable.value = bytes.toString(CryptoJS7.enc.Utf8);
14468
15140
  }
14469
15141
  return variable.value;
14470
15142
  }
@@ -14489,11 +15161,17 @@ var ExuluOtel = {
14489
15161
  }
14490
15162
  };
14491
15163
  var ExuluDatabase = {
14492
- init: async ({ contexts }) => {
15164
+ init: async ({ contexts, litellm }) => {
14493
15165
  await execute({ contexts });
15166
+ if (litellm !== false) {
15167
+ await initLitellmDb();
15168
+ }
14494
15169
  },
14495
- update: async ({ contexts }) => {
15170
+ update: async ({ contexts, litellm }) => {
14496
15171
  await execute({ contexts });
15172
+ if (litellm !== false) {
15173
+ await initLitellmDb();
15174
+ }
14497
15175
  },
14498
15176
  api: {
14499
15177
  key: {