@exulu/backend 1.27.2 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,14 +40,15 @@ __export(index_exports, {
40
40
  ExuluDefaultAgents: () => ExuluDefaultAgents,
41
41
  ExuluDefaultContexts: () => ExuluDefaultContexts,
42
42
  ExuluEmbedder: () => ExuluEmbedder,
43
- ExuluEval: () => ExuluEval,
43
+ ExuluEval: () => ExuluEval2,
44
44
  ExuluJobs: () => ExuluJobs,
45
45
  ExuluOtel: () => ExuluOtel,
46
46
  ExuluQueues: () => queues,
47
47
  ExuluTool: () => ExuluTool2,
48
48
  ExuluUtils: () => ExuluUtils,
49
49
  ExuluVariables: () => ExuluVariables,
50
- db: () => db2
50
+ db: () => db2,
51
+ logMetadata: () => logMetadata
51
52
  });
52
53
  module.exports = __toCommonJS(index_exports);
53
54
  var import_config = require("dotenv/config");
@@ -150,7 +151,17 @@ async function ensureDatabaseExists() {
150
151
  database: "postgres",
151
152
  // Connect to default database
152
153
  password: process.env.POSTGRES_DB_PASSWORD,
153
- ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
154
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
155
+ connectionTimeoutMillis: 1e4
156
+ },
157
+ pool: {
158
+ min: 2,
159
+ max: 4,
160
+ acquireTimeoutMillis: 3e4,
161
+ createTimeoutMillis: 3e4,
162
+ idleTimeoutMillis: 3e4,
163
+ reapIntervalMillis: 1e3,
164
+ createRetryIntervalMillis: 200
154
165
  }
155
166
  });
156
167
  try {
@@ -195,7 +206,17 @@ async function postgresClient() {
195
206
  user: process.env.POSTGRES_DB_USER,
196
207
  database: dbName,
197
208
  password: process.env.POSTGRES_DB_PASSWORD,
198
- ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
209
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
210
+ connectionTimeoutMillis: 1e4
211
+ },
212
+ pool: {
213
+ min: 2,
214
+ max: 20,
215
+ acquireTimeoutMillis: 3e4,
216
+ createTimeoutMillis: 3e4,
217
+ idleTimeoutMillis: 3e4,
218
+ reapIntervalMillis: 1e3,
219
+ createRetryIntervalMillis: 200
199
220
  }
200
221
  });
201
222
  try {
@@ -243,7 +264,9 @@ var bullmqDecorator = async ({
243
264
  workflow,
244
265
  item,
245
266
  context,
246
- retries
267
+ retries,
268
+ backoff,
269
+ timeoutInSeconds
247
270
  }) => {
248
271
  const types = [
249
272
  embedder,
@@ -267,30 +290,35 @@ var bullmqDecorator = async ({
267
290
  if (embedder) {
268
291
  type = "embedder";
269
292
  }
293
+ const jobData = {
294
+ label,
295
+ type: `${type}`,
296
+ timeoutInSeconds: timeoutInSeconds || 180,
297
+ // 3 minutes default
298
+ inputs,
299
+ ...user && { user },
300
+ ...role && { role },
301
+ ...trigger && { trigger },
302
+ ...workflow && { workflow },
303
+ ...embedder && { embedder },
304
+ ...processor && { processor },
305
+ ...evaluation && { evaluation },
306
+ ...item && { item },
307
+ ...context && { context }
308
+ };
270
309
  const redisId = (0, import_uuid.v4)();
271
310
  const job = await queue.add(
272
311
  `${embedder || workflow || processor || evaluation}`,
273
- {
274
- label,
275
- type: `${type}`,
276
- inputs,
277
- ...user && { user },
278
- ...role && { role },
279
- ...trigger && { trigger },
280
- ...workflow && { workflow },
281
- ...embedder && { embedder },
282
- ...processor && { processor },
283
- ...evaluation && { evaluation },
284
- ...item && { item },
285
- ...context && { context }
286
- },
312
+ jobData,
287
313
  {
288
314
  jobId: redisId,
289
315
  // Setting it to 3 as a sensible default, as
290
316
  // many AI services are quite unstable.
291
317
  attempts: retries || 3,
292
318
  // todo make this configurable?
293
- backoff: {
319
+ removeOnComplete: 5e3,
320
+ removeOnFail: 1e4,
321
+ backoff: backoff || {
294
322
  type: "exponential",
295
323
  delay: 2e3
296
324
  }
@@ -735,6 +763,17 @@ var requestValidators = {
735
763
  // src/registry/utils/graphql.ts
736
764
  var import_bcryptjs3 = __toESM(require("bcryptjs"), 1);
737
765
 
766
+ // types/enums/jobs.ts
767
+ var JOB_STATUS_ENUM = {
768
+ completed: "completed",
769
+ failed: "failed",
770
+ delayed: "delayed",
771
+ active: "active",
772
+ waiting: "waiting",
773
+ paused: "paused",
774
+ stuck: "stuck"
775
+ };
776
+
738
777
  // src/postgres/core-schema.ts
739
778
  var agentMessagesSchema = {
740
779
  type: "agent_messages",
@@ -1169,6 +1208,45 @@ var evalSetsSchema = {
1169
1208
  }
1170
1209
  ]
1171
1210
  };
1211
+ var jobResultsSchema = {
1212
+ type: "job_results",
1213
+ name: {
1214
+ plural: "job_results",
1215
+ singular: "job_result"
1216
+ },
1217
+ fields: [
1218
+ {
1219
+ name: "job_id",
1220
+ type: "text"
1221
+ },
1222
+ {
1223
+ name: "state",
1224
+ type: "text"
1225
+ },
1226
+ {
1227
+ name: "error",
1228
+ type: "json"
1229
+ },
1230
+ {
1231
+ name: "label",
1232
+ type: "text",
1233
+ index: true
1234
+ },
1235
+ {
1236
+ name: "tries",
1237
+ type: "number",
1238
+ default: 0
1239
+ },
1240
+ {
1241
+ name: "result",
1242
+ type: "json"
1243
+ },
1244
+ {
1245
+ name: "metadata",
1246
+ type: "json"
1247
+ }
1248
+ ]
1249
+ };
1172
1250
  var evalRunsSchema = {
1173
1251
  type: "eval_runs",
1174
1252
  name: {
@@ -1177,6 +1255,15 @@ var evalRunsSchema = {
1177
1255
  },
1178
1256
  RBAC: true,
1179
1257
  fields: [
1258
+ {
1259
+ name: "name",
1260
+ type: "text"
1261
+ },
1262
+ {
1263
+ name: "timeout_in_seconds",
1264
+ type: "number",
1265
+ default: 180
1266
+ },
1180
1267
  {
1181
1268
  name: "eval_set_id",
1182
1269
  type: "uuid",
@@ -1188,7 +1275,7 @@ var evalRunsSchema = {
1188
1275
  required: true
1189
1276
  },
1190
1277
  {
1191
- name: "eval_function_ids",
1278
+ name: "eval_functions",
1192
1279
  type: "json",
1193
1280
  required: true
1194
1281
  },
@@ -1199,7 +1286,7 @@ var evalRunsSchema = {
1199
1286
  {
1200
1287
  name: "scoring_method",
1201
1288
  type: "enum",
1202
- enumValues: ["mean", "sum", "average"],
1289
+ enumValues: ["median", "sum", "average"],
1203
1290
  required: true
1204
1291
  },
1205
1292
  {
@@ -1323,7 +1410,8 @@ var coreSchemas = {
1323
1410
  variablesSchema: () => addCoreFields(variablesSchema),
1324
1411
  rbacSchema: () => addCoreFields(rbacSchema),
1325
1412
  workflowTemplatesSchema: () => addCoreFields(workflowTemplatesSchema),
1326
- platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema)
1413
+ platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema),
1414
+ jobResultsSchema: () => addCoreFields(jobResultsSchema)
1327
1415
  };
1328
1416
  }
1329
1417
  };
@@ -1386,11 +1474,11 @@ var bullmq = {
1386
1474
  if (!data.inputs) {
1387
1475
  throw new Error(`Missing property "inputs" in data for job ${id}.`);
1388
1476
  }
1389
- if (data.type !== "embedder" && data.type !== "workflow") {
1390
- throw new Error(`Property "type" in data for job ${id} must be of value "embedder" or "workflow".`);
1477
+ if (data.type !== "embedder" && data.type !== "workflow" && data.type !== "processor" && data.type !== "eval_run" && data.type !== "eval_function") {
1478
+ throw new Error(`Property "type" in data for job ${id} must be of value "embedder", "workflow", "processor", "eval_run" or "eval_function".`);
1391
1479
  }
1392
- if (!data.workflow && !data.embedder) {
1393
- throw new Error(`Either a workflow or embedder must be set for job ${id}.`);
1480
+ if (!data.workflow && !data.embedder && !data.processor && !data.eval_run_id && !data.eval_functions?.length) {
1481
+ throw new Error(`Either a workflow, embedder, processor, eval_run or eval_functions must be set for job ${id}.`);
1394
1482
  }
1395
1483
  }
1396
1484
  };
@@ -1594,6 +1682,7 @@ var generateApiKey = async (name, email) => {
1594
1682
  };
1595
1683
 
1596
1684
  // src/registry/utils/graphql.ts
1685
+ var import_uuid2 = require("uuid");
1597
1686
  var GraphQLDate = new import_graphql2.GraphQLScalarType({
1598
1687
  name: "Date",
1599
1688
  description: "Date custom scalar type",
@@ -1689,7 +1778,6 @@ ${enumValues}
1689
1778
  fields.push(" maxContextLength: Int");
1690
1779
  fields.push(" provider: String");
1691
1780
  fields.push(" slug: String");
1692
- fields.push(" evals: [AgentEvalFunction]");
1693
1781
  }
1694
1782
  const rbacField = table.RBAC ? " RBAC: RBACData" : "";
1695
1783
  const typeDef = `
@@ -1901,11 +1989,6 @@ function createMutations(table, agents, contexts, tools, config) {
1901
1989
  if (!record) {
1902
1990
  throw new Error("Record not found");
1903
1991
  }
1904
- if (tableNamePlural === "jobs") {
1905
- if (!user.super_admin && record.created_by !== user.id) {
1906
- throw new Error("You are not authorized to edit this record");
1907
- }
1908
- }
1909
1992
  if (record.rights_mode === "public") {
1910
1993
  return true;
1911
1994
  }
@@ -2328,10 +2411,6 @@ function createMutations(table, agents, contexts, tools, config) {
2328
2411
  }
2329
2412
  var applyAccessControl = (table, user, query) => {
2330
2413
  const tableNamePlural = table.name.plural.toLowerCase();
2331
- if (!user.super_admin && table.name.plural === "jobs") {
2332
- query = query.where("created_by", user.id);
2333
- return query;
2334
- }
2335
2414
  if (table.name.plural !== "agent_sessions" && user.super_admin === true) {
2336
2415
  return query;
2337
2416
  }
@@ -2466,14 +2545,6 @@ var addAgentFields = async (requestedFields, agents, result, tools, user) => {
2466
2545
  if (requestedFields.includes("provider")) {
2467
2546
  result.provider = backend?.provider || "";
2468
2547
  }
2469
- if (requestedFields.includes("evals")) {
2470
- result.evals = backend?.evals?.map((evalFunc) => ({
2471
- id: evalFunc.id,
2472
- name: evalFunc.name,
2473
- description: evalFunc.description,
2474
- config: evalFunc.config || []
2475
- })) || [];
2476
- }
2477
2548
  if (!requestedFields.includes("backend")) {
2478
2549
  delete result.backend;
2479
2550
  }
@@ -3176,7 +3247,7 @@ var contextToTableDefinition = (context) => {
3176
3247
  });
3177
3248
  return addCoreFields(definition);
3178
3249
  };
3179
- function createSDL(tables, contexts, agents, tools, config) {
3250
+ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3180
3251
  const contextSchemas = contexts.map((context) => contextToTableDefinition(context));
3181
3252
  tables.forEach((table) => {
3182
3253
  if (!table.fields.some((field) => field.name === "createdAt")) {
@@ -3371,15 +3442,39 @@ type PageInfo {
3371
3442
  typeDefs += `
3372
3443
  providers: ProviderPaginationResult
3373
3444
  `;
3445
+ typeDefs += `
3446
+ queue(queue: QueueEnum!): QueueResult
3447
+ `;
3448
+ typeDefs += `
3449
+ evals: EvalPaginationResult
3450
+ `;
3374
3451
  typeDefs += `
3375
3452
  contexts: ContextPaginationResult
3376
3453
  `;
3377
3454
  typeDefs += `
3378
3455
  contextById(id: ID!): Context
3379
3456
  `;
3457
+ mutationDefs += `
3458
+ runEval(id: ID!, test_case_ids: [ID!]): RunEvalReturnPayload
3459
+ `;
3460
+ mutationDefs += `
3461
+ drainQueue(queue: QueueEnum!): JobActionReturnPayload
3462
+ `;
3463
+ mutationDefs += `
3464
+ pauseQueue(queue: QueueEnum!): JobActionReturnPayload
3465
+ `;
3466
+ mutationDefs += `
3467
+ resumeQueue(queue: QueueEnum!): JobActionReturnPayload
3468
+ `;
3469
+ mutationDefs += `
3470
+ deleteJob(queue: QueueEnum!, id: String!): JobActionReturnPayload
3471
+ `;
3380
3472
  typeDefs += `
3381
3473
  tools: ToolPaginationResult
3382
3474
  `;
3475
+ typeDefs += `
3476
+ jobs(queue: QueueEnum!, statusses: [JobStateEnum!]): JobPaginationResult
3477
+ `;
3383
3478
  resolvers.Query["providers"] = async (_, args, context, info) => {
3384
3479
  const requestedFields = getRequestedFields(info);
3385
3480
  return {
@@ -3392,6 +3487,227 @@ type PageInfo {
3392
3487
  })
3393
3488
  };
3394
3489
  };
3490
+ resolvers.Query["queue"] = async (_, args, context, info) => {
3491
+ if (!args.queue) {
3492
+ throw new Error("Queue name is required");
3493
+ }
3494
+ const queue = queues.list.get(args.queue);
3495
+ if (!queue) {
3496
+ throw new Error("Queue not found");
3497
+ }
3498
+ const config2 = await queue.use();
3499
+ return {
3500
+ name: config2.queue.name,
3501
+ concurrency: config2.concurrency,
3502
+ ratelimit: config2.ratelimit,
3503
+ isMaxed: await config2.queue.isMaxed(),
3504
+ isPaused: await config2.queue.isPaused(),
3505
+ jobs: {
3506
+ paused: await config2.queue.isPaused(),
3507
+ completed: await config2.queue.getJobCountByTypes("completed"),
3508
+ failed: await config2.queue.getJobCountByTypes("failed"),
3509
+ waiting: await config2.queue.getJobCountByTypes("waiting"),
3510
+ active: await config2.queue.getJobCountByTypes("active"),
3511
+ delayed: await config2.queue.getJobCountByTypes("delayed")
3512
+ }
3513
+ };
3514
+ };
3515
+ resolvers.Mutation["runEval"] = async (_, args, context, info) => {
3516
+ console.log("[EXULU] /evals/run/:id", args.id);
3517
+ const user = context.user;
3518
+ const eval_run_id = args.id;
3519
+ if (!user.super_admin && (!user.role || user.role.evals !== "write")) {
3520
+ throw new Error("You don't have permission to run evals. Required: super_admin or evals write access.");
3521
+ }
3522
+ const { db: db3 } = await postgresClient();
3523
+ const evalRun = await db3.from("eval_runs").where({ id: eval_run_id }).first();
3524
+ if (!evalRun) {
3525
+ throw new Error("Eval run not found in database.");
3526
+ }
3527
+ const hasAccessToEvalRun = await checkRecordAccess(evalRun, "write", user);
3528
+ if (!hasAccessToEvalRun) {
3529
+ throw new Error("You don't have access to this eval run.");
3530
+ }
3531
+ let testCaseIds = evalRun.test_case_ids ? typeof evalRun.test_case_ids === "string" ? JSON.parse(evalRun.test_case_ids) : evalRun.test_case_ids : [];
3532
+ const eval_functions = evalRun.eval_functions ? typeof evalRun.eval_functions === "string" ? JSON.parse(evalRun.eval_functions) : evalRun.eval_functions : [];
3533
+ if (!testCaseIds || testCaseIds.length === 0) {
3534
+ throw new Error("No test cases selected for this eval run.");
3535
+ }
3536
+ if (!eval_functions || eval_functions.length === 0) {
3537
+ throw new Error("No eval functions selected for this eval run.");
3538
+ }
3539
+ if (args.test_case_ids) {
3540
+ testCaseIds = testCaseIds.filter((testCase) => args.test_case_ids.includes(testCase));
3541
+ }
3542
+ console.log("[EXULU] test cases ids filtered", testCaseIds);
3543
+ const testCases = await db3.from("test_cases").whereIn("id", testCaseIds);
3544
+ if (testCases.length === 0) {
3545
+ throw new Error("No test cases found for eval run.");
3546
+ }
3547
+ const agentInstance = await loadAgent(evalRun.agent_id);
3548
+ if (!agentInstance) {
3549
+ throw new Error("Agent instance not found for eval run.");
3550
+ }
3551
+ const evalQueue = await queues.register("eval_runs", 1, 1).use();
3552
+ const jobIds = [];
3553
+ for (const testCase of testCases) {
3554
+ const jobData = {
3555
+ label: `Eval Run ${eval_run_id} - Test Case ${testCase.id}`,
3556
+ trigger: "api",
3557
+ timeoutInSeconds: evalRun.timeout_in_seconds || 180,
3558
+ // default to 3 minutes
3559
+ type: "eval_run",
3560
+ eval_run_id,
3561
+ eval_run_name: evalRun.name,
3562
+ test_case_id: testCase.id,
3563
+ test_case_name: testCase.name,
3564
+ eval_functions,
3565
+ // Array of eval function IDs - worker will create child jobs for these
3566
+ agent_id: evalRun.agent_id,
3567
+ inputs: testCase.inputs,
3568
+ expected_output: testCase.expected_output,
3569
+ expected_tools: testCase.expected_tools,
3570
+ expected_knowledge_sources: testCase.expected_knowledge_sources,
3571
+ expected_agent_tools: testCase.expected_agent_tools,
3572
+ config: evalRun.config,
3573
+ scoring_method: evalRun.scoring_method,
3574
+ pass_threshold: evalRun.pass_threshold,
3575
+ user: user.id,
3576
+ role: user.role?.id
3577
+ };
3578
+ const redisId = (0, import_uuid2.v4)();
3579
+ const job = await evalQueue.queue.add("eval_run", jobData, {
3580
+ jobId: redisId,
3581
+ // Setting it to 3 as a sensible default, as
3582
+ // many AI services are quite unstable.
3583
+ attempts: evalQueue.retries || 1,
3584
+ removeOnComplete: 5e3,
3585
+ removeOnFail: 1e4,
3586
+ backoff: evalQueue.backoff || {
3587
+ type: "exponential",
3588
+ delay: 2e3
3589
+ }
3590
+ });
3591
+ jobIds.push(job.id);
3592
+ }
3593
+ const response = {
3594
+ jobs: jobIds,
3595
+ count: jobIds.length
3596
+ };
3597
+ const requestedFields = getRequestedFields(info);
3598
+ const mapped = {};
3599
+ requestedFields.forEach((field) => {
3600
+ mapped[field] = response[field];
3601
+ });
3602
+ return mapped;
3603
+ };
3604
+ resolvers.Mutation["drainQueue"] = async (_, args, context, info) => {
3605
+ if (!args.queue) {
3606
+ throw new Error("Queue name is required");
3607
+ }
3608
+ const queue = queues.list.get(args.queue);
3609
+ if (!queue) {
3610
+ throw new Error("Queue not found");
3611
+ }
3612
+ const config2 = await queue.use();
3613
+ await config2.queue.drain();
3614
+ return { success: true };
3615
+ };
3616
+ resolvers.Mutation["pauseQueue"] = async (_, args, context, info) => {
3617
+ if (!args.queue) {
3618
+ throw new Error("Queue name is required");
3619
+ }
3620
+ const queue = queues.list.get(args.queue);
3621
+ if (!queue) {
3622
+ throw new Error("Queue not found");
3623
+ }
3624
+ const config2 = await queue.use();
3625
+ await config2.queue.pause();
3626
+ return { success: true };
3627
+ };
3628
+ resolvers.Mutation["resumeQueue"] = async (_, args, context, info) => {
3629
+ if (!args.queue) {
3630
+ throw new Error("Queue name is required");
3631
+ }
3632
+ const queue = queues.list.get(args.queue);
3633
+ if (!queue) {
3634
+ throw new Error("Queue not found");
3635
+ }
3636
+ const config2 = await queue.use();
3637
+ await config2.queue.resume();
3638
+ return { success: true };
3639
+ };
3640
+ resolvers.Mutation["deleteJob"] = async (_, args, context, info) => {
3641
+ if (!args.id) {
3642
+ throw new Error("Job ID is required");
3643
+ }
3644
+ if (!args.queue) {
3645
+ throw new Error("Queue name is required");
3646
+ }
3647
+ const queue = queues.list.get(args.queue);
3648
+ if (!queue) {
3649
+ throw new Error("Queue not found");
3650
+ }
3651
+ const config2 = await queue.use();
3652
+ await config2.queue.remove(args.id);
3653
+ return { success: true };
3654
+ };
3655
+ resolvers.Query["evals"] = async (_, args, context, info) => {
3656
+ const requestedFields = getRequestedFields(info);
3657
+ return {
3658
+ items: evals.map((_eval) => {
3659
+ const object = {};
3660
+ requestedFields.forEach((field) => {
3661
+ object[field] = _eval[field];
3662
+ });
3663
+ return object;
3664
+ })
3665
+ };
3666
+ };
3667
+ resolvers.Query["jobs"] = async (_, args, context, info) => {
3668
+ if (!args.queue) {
3669
+ throw new Error("Queue name is required");
3670
+ }
3671
+ const { client: client2 } = await redisClient();
3672
+ if (!client2) {
3673
+ throw new Error("Redis client not created properly");
3674
+ }
3675
+ const {
3676
+ jobs,
3677
+ count
3678
+ } = await getJobsByQueueAndName(
3679
+ args.queue,
3680
+ args.statusses,
3681
+ args.page || 1,
3682
+ args.limit || 100
3683
+ );
3684
+ console.log("[EXULU] jobs", jobs.map((job) => job.name));
3685
+ const requestedFields = getRequestedFields(info);
3686
+ return {
3687
+ items: await Promise.all(jobs.map(async (job) => {
3688
+ const object = {};
3689
+ for (const field of requestedFields) {
3690
+ if (field === "data") {
3691
+ object[field] = job[field];
3692
+ } else if (field === "timestamp") {
3693
+ object[field] = new Date(job[field]).toISOString();
3694
+ } else if (field === "state") {
3695
+ object[field] = await job.getState();
3696
+ } else {
3697
+ object[field] = job[field];
3698
+ }
3699
+ }
3700
+ return object;
3701
+ })),
3702
+ pageInfo: {
3703
+ pageCount: Math.ceil(count / (args.limit || 100)),
3704
+ itemCount: count,
3705
+ currentPage: args.page || 1,
3706
+ hasPreviousPage: args.page && args.page > 1 ? true : false,
3707
+ hasNextPage: args.page && args.page < Math.ceil(jobs.length / (args.limit || 100)) ? true : false
3708
+ }
3709
+ };
3710
+ };
3395
3711
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3396
3712
  const data = contexts.map((context2) => ({
3397
3713
  id: context2.id,
@@ -3497,7 +3813,32 @@ type PageInfo {
3497
3813
  };
3498
3814
  modelDefs += `
3499
3815
  type ProviderPaginationResult {
3500
- items: [Provider]!
3816
+ items: [Provider]!
3817
+ }
3818
+ `;
3819
+ modelDefs += `
3820
+ type QueueResult {
3821
+ name: String!
3822
+ concurrency: Int!
3823
+ ratelimit: Int!
3824
+ isMaxed: Boolean!
3825
+ isPaused: Boolean!
3826
+ jobs: QueueJobsCounts
3827
+ }
3828
+ `;
3829
+ modelDefs += `
3830
+ type QueueJobsCounts {
3831
+ paused: Int!
3832
+ completed: Int!
3833
+ failed: Int!
3834
+ waiting: Int!
3835
+ active: Int!
3836
+ delayed: Int!
3837
+ }
3838
+ `;
3839
+ modelDefs += `
3840
+ type EvalPaginationResult {
3841
+ items: [Eval]!
3501
3842
  }
3502
3843
  `;
3503
3844
  modelDefs += `
@@ -3510,6 +3851,12 @@ type PageInfo {
3510
3851
  items: [Tool]!
3511
3852
  }
3512
3853
  `;
3854
+ modelDefs += `
3855
+ type JobPaginationResult {
3856
+ items: [Job]!
3857
+ pageInfo: PageInfo!
3858
+ }
3859
+ `;
3513
3860
  typeDefs += "}\n";
3514
3861
  mutationDefs += "}\n";
3515
3862
  const genericTypes = `
@@ -3567,6 +3914,19 @@ type Provider {
3567
3914
  type: EnumProviderType!
3568
3915
  }
3569
3916
 
3917
+ type Eval {
3918
+ id: ID!
3919
+ name: String!
3920
+ description: String!
3921
+ llm: Boolean!
3922
+ config: [EvalConfig!]
3923
+ }
3924
+
3925
+ type EvalConfig {
3926
+ name: String!
3927
+ description: String!
3928
+ }
3929
+
3570
3930
  type Context {
3571
3931
  id: ID!
3572
3932
  name: String!
@@ -3578,6 +3938,15 @@ type Context {
3578
3938
  configuration: JSON
3579
3939
  }
3580
3940
 
3941
+ type RunEvalReturnPayload {
3942
+ jobs: [String!]!
3943
+ count: Int!
3944
+ }
3945
+
3946
+ type JobActionReturnPayload {
3947
+ success: Boolean!
3948
+ }
3949
+
3581
3950
  type ContextField {
3582
3951
  name: String!
3583
3952
  type: String!
@@ -3593,10 +3962,35 @@ type Tool {
3593
3962
  config: JSON
3594
3963
  }
3595
3964
 
3965
+ type Job {
3966
+ id: String!
3967
+ name: String!
3968
+ returnvalue: JSON
3969
+ stacktrace: [String]
3970
+ failedReason: String
3971
+ state: String!
3972
+ data: JSON
3973
+ timestamp: Date
3974
+ }
3975
+
3596
3976
  enum EnumProviderType {
3597
3977
  agent
3598
3978
  }
3599
3979
 
3980
+ enum QueueEnum {
3981
+ ${queues.list.keys().toArray().join("\n")}
3982
+ }
3983
+
3984
+ enum JobStateEnum {
3985
+ ${JOB_STATUS_ENUM.active}
3986
+ ${JOB_STATUS_ENUM.waiting}
3987
+ ${JOB_STATUS_ENUM.delayed}
3988
+ ${JOB_STATUS_ENUM.failed}
3989
+ ${JOB_STATUS_ENUM.completed}
3990
+ ${JOB_STATUS_ENUM.paused}
3991
+ ${JOB_STATUS_ENUM.stuck}
3992
+ }
3993
+
3600
3994
  type StatisticsResult {
3601
3995
  group: String!
3602
3996
  count: Int!
@@ -3626,6 +4020,25 @@ var validateCreateOrRemoveSuperAdminPermission = async (tableNamePlural, input,
3626
4020
  }
3627
4021
  }
3628
4022
  };
4023
+ async function getJobsByQueueAndName(queueName, statusses, page, limit) {
4024
+ const queue = queues.list.get(queueName);
4025
+ if (!queue) {
4026
+ throw new Error(`Queue ${queueName} not found`);
4027
+ }
4028
+ const config = await queue.use();
4029
+ const startIndex = (page || 1) - 1;
4030
+ const endIndex = startIndex + (limit || 100);
4031
+ const jobs = await config.queue.getJobs(statusses || [], startIndex, endIndex, false);
4032
+ const counts = await config.queue.getJobCounts(...statusses || []);
4033
+ let total = 0;
4034
+ if (counts) {
4035
+ total = Object.keys(counts).reduce((acc, key) => acc + (counts[key] || 0), 0);
4036
+ }
4037
+ return {
4038
+ jobs,
4039
+ count: total
4040
+ };
4041
+ }
3629
4042
 
3630
4043
  // src/registry/classes.ts
3631
4044
  var import_client_s32 = require("@aws-sdk/client-s3");
@@ -3640,6 +4053,9 @@ var import_node_crypto = require("crypto");
3640
4053
  var expiresIn = 60 * 60 * 24 * 1;
3641
4054
  var s3Client;
3642
4055
  function getS3Client(config) {
4056
+ if (!config.fileUploads) {
4057
+ throw new Error("File uploads are not configured");
4058
+ }
3643
4059
  s3Client ??= new import_client_s3.S3Client({
3644
4060
  region: config.fileUploads.s3region,
3645
4061
  ...config.fileUploads.s3endpoint && {
@@ -3654,6 +4070,9 @@ function getS3Client(config) {
3654
4070
  return s3Client;
3655
4071
  }
3656
4072
  var getPresignedUrl = async (key, config) => {
4073
+ if (!config.fileUploads) {
4074
+ throw new Error("File uploads are not configured");
4075
+ }
3657
4076
  const url = await (0, import_s3_request_presigner.getSignedUrl)(
3658
4077
  getS3Client(config),
3659
4078
  new import_client_s3.GetObjectCommand({
@@ -3665,6 +4084,9 @@ var getPresignedUrl = async (key, config) => {
3665
4084
  return url;
3666
4085
  };
3667
4086
  var addPrefixToKey = (keyPath, config) => {
4087
+ if (!config.fileUploads) {
4088
+ throw new Error("File uploads are not configured");
4089
+ }
3668
4090
  if (!config.fileUploads.s3prefix) {
3669
4091
  return keyPath;
3670
4092
  }
@@ -3675,6 +4097,10 @@ var addPrefixToKey = (keyPath, config) => {
3675
4097
  return `${prefix}/${keyPath}`;
3676
4098
  };
3677
4099
  var uploadFile = async (user, file, key, config, options = {}) => {
4100
+ console.log("[EXULU] Uploading file to S3", key);
4101
+ if (!config.fileUploads) {
4102
+ throw new Error("File uploads are not configured");
4103
+ }
3678
4104
  const client2 = getS3Client(config);
3679
4105
  let folder = `${user}/`;
3680
4106
  const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
@@ -3690,7 +4116,13 @@ var uploadFile = async (user, file, key, config, options = {}) => {
3690
4116
  return key;
3691
4117
  };
3692
4118
  var createUppyRoutes = async (app, config) => {
4119
+ if (!config.fileUploads) {
4120
+ throw new Error("File uploads are not configured");
4121
+ }
3693
4122
  const extractUserPrefix = (key) => {
4123
+ if (!config.fileUploads) {
4124
+ throw new Error("File uploads are not configured");
4125
+ }
3694
4126
  if (!config.fileUploads.s3prefix) {
3695
4127
  return key.split("/")[0];
3696
4128
  }
@@ -3718,6 +4150,9 @@ var createUppyRoutes = async (app, config) => {
3718
4150
  };
3719
4151
  let stsClient;
3720
4152
  function getSTSClient() {
4153
+ if (!config.fileUploads) {
4154
+ throw new Error("File uploads are not configured");
4155
+ }
3721
4156
  stsClient ??= new import_client_sts.STSClient({
3722
4157
  region: config.fileUploads.s3region,
3723
4158
  ...config.fileUploads.s3endpoint && { endpoint: config.fileUploads.s3endpoint },
@@ -3729,6 +4164,9 @@ var createUppyRoutes = async (app, config) => {
3729
4164
  return stsClient;
3730
4165
  }
3731
4166
  app.delete("/s3/delete", async (req, res, next) => {
4167
+ if (!config.fileUploads) {
4168
+ throw new Error("File uploads are not configured");
4169
+ }
3732
4170
  const apikey = req.headers["exulu-api-key"] || null;
3733
4171
  const internalkey = req.headers["internal-key"] || null;
3734
4172
  const { db: db3 } = await postgresClient();
@@ -3819,6 +4257,9 @@ var createUppyRoutes = async (app, config) => {
3819
4257
  }
3820
4258
  });
3821
4259
  app.post("/s3/object", async (req, res, next) => {
4260
+ if (!config.fileUploads) {
4261
+ throw new Error("File uploads are not configured");
4262
+ }
3822
4263
  const apikey = req.headers["exulu-api-key"] || null;
3823
4264
  const internalkey = req.headers["internal-key"] || null;
3824
4265
  const { db: db3 } = await postgresClient();
@@ -3849,6 +4290,9 @@ var createUppyRoutes = async (app, config) => {
3849
4290
  res.end();
3850
4291
  });
3851
4292
  app.get("/s3/list", async (req, res, next) => {
4293
+ if (!config.fileUploads) {
4294
+ throw new Error("File uploads are not configured");
4295
+ }
3852
4296
  const apikey = req.headers["exulu-api-key"] || null;
3853
4297
  const internalkey = req.headers["internal-key"] || null;
3854
4298
  const { db: db3 } = await postgresClient();
@@ -3885,6 +4329,9 @@ var createUppyRoutes = async (app, config) => {
3885
4329
  res.end();
3886
4330
  });
3887
4331
  app.get("/s3/sts", (req, res, next) => {
4332
+ if (!config.fileUploads) {
4333
+ throw new Error("File uploads are not configured");
4334
+ }
3888
4335
  getSTSClient().send(new import_client_sts.GetFederationTokenCommand({
3889
4336
  Name: "Exulu",
3890
4337
  // The duration, in seconds, of the role session. The value specified
@@ -3897,8 +4344,8 @@ var createUppyRoutes = async (app, config) => {
3897
4344
  res.setHeader("Cache-Control", `public,max-age=${expiresIn}`);
3898
4345
  res.json({
3899
4346
  credentials: response.Credentials,
3900
- bucket: config.fileUploads.s3Bucket,
3901
- region: config.fileUploads.s3region
4347
+ bucket: config.fileUploads?.s3Bucket,
4348
+ region: config.fileUploads?.s3region
3902
4349
  });
3903
4350
  }, next);
3904
4351
  });
@@ -3917,6 +4364,9 @@ var createUppyRoutes = async (app, config) => {
3917
4364
  };
3918
4365
  const generateS3Key2 = (filename) => `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
3919
4366
  const signOnServer = async (req, res, next) => {
4367
+ if (!config.fileUploads) {
4368
+ throw new Error("File uploads are not configured");
4369
+ }
3920
4370
  const apikey = req.headers["exulu-api-key"] || null;
3921
4371
  const { db: db3 } = await postgresClient();
3922
4372
  let authtoken = null;
@@ -3962,6 +4412,9 @@ var createUppyRoutes = async (app, config) => {
3962
4412
  return await signOnServer(req, res, next);
3963
4413
  });
3964
4414
  app.post("/s3/multipart", async (req, res, next) => {
4415
+ if (!config.fileUploads) {
4416
+ throw new Error("File uploads are not configured");
4417
+ }
3965
4418
  const apikey = req.headers["exulu-api-key"] || null;
3966
4419
  const { db: db3 } = await postgresClient();
3967
4420
  let authtoken = null;
@@ -4017,6 +4470,9 @@ var createUppyRoutes = async (app, config) => {
4017
4470
  return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 1e4;
4018
4471
  }
4019
4472
  app.get("/s3/multipart/:uploadId/:partNumber", (req, res, next) => {
4473
+ if (!config.fileUploads) {
4474
+ throw new Error("File uploads are not configured");
4475
+ }
4020
4476
  const { uploadId, partNumber } = req.params;
4021
4477
  const { key } = req.query;
4022
4478
  if (!validatePartNumber(partNumber)) {
@@ -4046,6 +4502,9 @@ var createUppyRoutes = async (app, config) => {
4046
4502
  }
4047
4503
  const parts = [];
4048
4504
  function listPartsPage(startAt) {
4505
+ if (!config.fileUploads) {
4506
+ throw new Error("File uploads are not configured");
4507
+ }
4049
4508
  client2.send(new import_client_s3.ListPartsCommand({
4050
4509
  Bucket: config.fileUploads.s3Bucket,
4051
4510
  Key: key,
@@ -4070,6 +4529,9 @@ var createUppyRoutes = async (app, config) => {
4070
4529
  return part && typeof part === "object" && Number(part.PartNumber) && typeof part.ETag === "string";
4071
4530
  }
4072
4531
  app.post("/s3/multipart/:uploadId/complete", (req, res, next) => {
4532
+ if (!config.fileUploads) {
4533
+ throw new Error("File uploads are not configured");
4534
+ }
4073
4535
  const client2 = getS3Client(config);
4074
4536
  const { uploadId } = req.params;
4075
4537
  const { key } = req.query;
@@ -4100,6 +4562,9 @@ var createUppyRoutes = async (app, config) => {
4100
4562
  });
4101
4563
  });
4102
4564
  app.delete("/s3/multipart/:uploadId", (req, res, next) => {
4565
+ if (!config.fileUploads) {
4566
+ throw new Error("File uploads are not configured");
4567
+ }
4103
4568
  const client2 = getS3Client(config);
4104
4569
  const { uploadId } = req.params;
4105
4570
  const { key } = req.query;
@@ -4288,27 +4753,34 @@ function errorHandler(error) {
4288
4753
  }
4289
4754
  return JSON.stringify(error);
4290
4755
  }
4291
- var ExuluEval = class {
4756
+ var ExuluEval2 = class {
4292
4757
  id;
4293
4758
  name;
4294
4759
  description;
4760
+ llm;
4295
4761
  execute;
4296
4762
  config;
4297
4763
  queue;
4298
- constructor({ id, name, description, execute: execute2, config, queue }) {
4764
+ constructor({ id, name, description, execute: execute2, config, queue, llm }) {
4299
4765
  this.id = id;
4300
4766
  this.name = name;
4301
4767
  this.description = description;
4302
4768
  this.execute = execute2;
4303
4769
  this.config = config;
4770
+ this.llm = llm;
4304
4771
  this.queue = queue;
4305
4772
  }
4306
- async run(messages, metadata, config) {
4307
- const score = await this.execute({ messages, metadata, config });
4308
- if (score < 0 || score > 100) {
4309
- throw new Error(`Eval function ${this.name} must return a score between 0 and 100, got ${score}`);
4773
+ async run(agent, backend, testCase, messages, config) {
4774
+ try {
4775
+ const score = await this.execute({ agent, backend, testCase, messages, config });
4776
+ if (score < 0 || score > 100) {
4777
+ throw new Error(`Eval function ${this.name} must return a score between 0 and 100, got ${score}`);
4778
+ }
4779
+ return score;
4780
+ } catch (error) {
4781
+ console.error(`[EXULU] error running eval function ${this.name}.`, error);
4782
+ throw new Error(`Error running eval function ${this.name}: ${error instanceof Error ? error.message : String(error)}`);
4310
4783
  }
4311
- return score;
4312
4784
  }
4313
4785
  };
4314
4786
  var ExuluAgent2 = class {
@@ -4323,13 +4795,13 @@ var ExuluAgent2 = class {
4323
4795
  type;
4324
4796
  streaming = false;
4325
4797
  maxContextLength;
4798
+ queue;
4326
4799
  rateLimit;
4327
4800
  config;
4328
- evals;
4329
4801
  // private memory: Memory | undefined; // TODO do own implementation
4330
4802
  model;
4331
4803
  capabilities;
4332
- constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, evals, provider }) {
4804
+ constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, provider, queue }) {
4333
4805
  this.id = id;
4334
4806
  this.name = name;
4335
4807
  this.description = description;
@@ -4338,7 +4810,7 @@ var ExuluAgent2 = class {
4338
4810
  this.config = config;
4339
4811
  this.type = type;
4340
4812
  this.maxContextLength = maxContextLength;
4341
- this.evals = evals;
4813
+ this.queue = queue;
4342
4814
  this.capabilities = capabilities || {
4343
4815
  text: false,
4344
4816
  images: [],
@@ -4437,7 +4909,7 @@ var ExuluAgent2 = class {
4437
4909
  prompt,
4438
4910
  user,
4439
4911
  session,
4440
- message,
4912
+ inputMessages,
4441
4913
  currentTools,
4442
4914
  allExuluTools,
4443
4915
  statistics,
@@ -4455,10 +4927,10 @@ var ExuluAgent2 = class {
4455
4927
  if (!this.config) {
4456
4928
  throw new Error("Config is required for generating.");
4457
4929
  }
4458
- if (prompt && message) {
4930
+ if (prompt && inputMessages?.length) {
4459
4931
  throw new Error("Message and prompt cannot be provided at the same time.");
4460
4932
  }
4461
- if (!prompt && !message) {
4933
+ if (!prompt && !inputMessages?.length) {
4462
4934
  throw new Error("Prompt or message is required for generating.");
4463
4935
  }
4464
4936
  if (outputSchema && !prompt) {
@@ -4468,18 +4940,18 @@ var ExuluAgent2 = class {
4468
4940
  apiKey: providerapikey
4469
4941
  });
4470
4942
  console.log("[EXULU] Model for agent: " + this.name, " created for generating sync.");
4471
- let messages = [];
4472
- if (message && session && user) {
4943
+ let messages = inputMessages || [];
4944
+ if (messages && session && user) {
4473
4945
  const previousMessages = await getAgentMessages({
4474
4946
  session,
4475
4947
  user: user.id,
4476
4948
  limit: 50,
4477
4949
  page: 1
4478
4950
  });
4479
- const previousMessagesContent = previousMessages.map((message2) => JSON.parse(message2.content));
4951
+ const previousMessagesContent = previousMessages.map((message) => JSON.parse(message.content));
4480
4952
  messages = await (0, import_ai.validateUIMessages)({
4481
4953
  // append the new message to the previous messages:
4482
- messages: [...previousMessagesContent, message]
4954
+ messages: [...previousMessagesContent, ...messages]
4483
4955
  });
4484
4956
  }
4485
4957
  console.log("[EXULU] Message count for agent: " + this.name, "loaded for generating sync.", messages.length);
@@ -4615,13 +5087,12 @@ var ExuluAgent2 = class {
4615
5087
  return "";
4616
5088
  };
4617
5089
  generateStream = async ({
4618
- express: express3,
4619
5090
  user,
4620
5091
  session,
4621
5092
  message,
5093
+ previousMessages,
4622
5094
  currentTools,
4623
5095
  allExuluTools,
4624
- statistics,
4625
5096
  toolConfigs,
4626
5097
  providerapikey,
4627
5098
  contexts,
@@ -4629,27 +5100,34 @@ var ExuluAgent2 = class {
4629
5100
  instructions
4630
5101
  }) => {
4631
5102
  if (!this.model) {
5103
+ console.error("[EXULU] Model is required for streaming.");
4632
5104
  throw new Error("Model is required for streaming.");
4633
5105
  }
4634
5106
  if (!this.config) {
5107
+ console.error("[EXULU] Config is required for streaming.");
4635
5108
  throw new Error("Config is required for generating.");
4636
5109
  }
4637
5110
  if (!message) {
5111
+ console.error("[EXULU] Message is required for streaming.");
4638
5112
  throw new Error("Message is required for streaming.");
4639
5113
  }
4640
5114
  const model = this.model.create({
4641
5115
  apiKey: providerapikey
4642
5116
  });
4643
5117
  let messages = [];
4644
- const previousMessages = await getAgentMessages({
4645
- session,
4646
- user: user.id,
4647
- limit: 50,
4648
- page: 1
4649
- });
4650
- const previousMessagesContent = previousMessages.map(
4651
- (message2) => JSON.parse(message2.content)
4652
- );
5118
+ let previousMessagesContent = previousMessages || [];
5119
+ if (session) {
5120
+ console.log("[EXULU] loading previous messages from session: " + session);
5121
+ const previousMessages2 = await getAgentMessages({
5122
+ session,
5123
+ user: user.id,
5124
+ limit: 50,
5125
+ page: 1
5126
+ });
5127
+ previousMessagesContent = previousMessages2.map(
5128
+ (message2) => JSON.parse(message2.content)
5129
+ );
5130
+ }
4653
5131
  messages = await (0, import_ai.validateUIMessages)({
4654
5132
  // append the new message to the previous messages:
4655
5133
  messages: [...previousMessagesContent, message]
@@ -4682,80 +5160,17 @@ var ExuluAgent2 = class {
4682
5160
  user,
4683
5161
  exuluConfig
4684
5162
  ),
4685
- onError: (error) => console.error("[EXULU] chat stream error.", error)
5163
+ onError: (error) => {
5164
+ console.error("[EXULU] chat stream error.", error);
5165
+ throw new Error(`Chat stream error: ${error instanceof Error ? error.message : String(error)}`);
5166
+ }
4686
5167
  // stopWhen: [stepCountIs(1)],
4687
5168
  });
4688
- result.consumeStream();
4689
- result.pipeUIMessageStreamToResponse(express3.res, {
4690
- messageMetadata: ({ part }) => {
4691
- if (part.type === "finish") {
4692
- return {
4693
- totalTokens: part.totalUsage.totalTokens,
4694
- reasoningTokens: part.totalUsage.reasoningTokens,
4695
- inputTokens: part.totalUsage.inputTokens,
4696
- outputTokens: part.totalUsage.outputTokens,
4697
- cachedInputTokens: part.totalUsage.cachedInputTokens
4698
- };
4699
- }
4700
- },
5169
+ return {
5170
+ stream: result,
4701
5171
  originalMessages: messages,
4702
- sendReasoning: true,
4703
- sendSources: true,
4704
- onError: (error) => {
4705
- console.error("[EXULU] chat response error.", error);
4706
- return errorHandler(error);
4707
- },
4708
- generateMessageId: (0, import_ai.createIdGenerator)({
4709
- prefix: "msg_",
4710
- size: 16
4711
- }),
4712
- onFinish: async ({ messages: messages2, isContinuation, isAborted, responseMessage }) => {
4713
- if (session) {
4714
- await saveChat({
4715
- session,
4716
- user: user.id,
4717
- messages: messages2.filter((x) => !previousMessagesContent.find((y) => y.id === x.id))
4718
- });
4719
- }
4720
- const metadata = messages2[messages2.length - 1]?.metadata;
4721
- console.log("[EXULU] Finished streaming", metadata);
4722
- console.log("[EXULU] Statistics", statistics);
4723
- if (statistics) {
4724
- await Promise.all([
4725
- updateStatistic({
4726
- name: "count",
4727
- label: statistics.label,
4728
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4729
- trigger: statistics.trigger,
4730
- count: 1,
4731
- user: user.id,
4732
- role: user?.role?.id
4733
- }),
4734
- ...metadata?.inputTokens ? [
4735
- updateStatistic({
4736
- name: "inputTokens",
4737
- label: statistics.label,
4738
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4739
- trigger: statistics.trigger,
4740
- count: metadata?.inputTokens,
4741
- user: user.id,
4742
- role: user?.role?.id
4743
- })
4744
- ] : [],
4745
- ...metadata?.outputTokens ? [
4746
- updateStatistic({
4747
- name: "outputTokens",
4748
- label: statistics.label,
4749
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4750
- trigger: statistics.trigger,
4751
- count: metadata?.outputTokens
4752
- })
4753
- ] : []
4754
- ]);
4755
- }
4756
- }
4757
- });
4758
- return;
5172
+ previousMessages: previousMessagesContent
5173
+ };
4759
5174
  };
4760
5175
  };
4761
5176
  var getAgentMessages = async ({ session, user, limit, page }) => {
@@ -4946,16 +5361,21 @@ var ExuluContext = class {
4946
5361
  if (queue?.queue.name) {
4947
5362
  console.log("[EXULU] processor is in queue mode, scheduling job.");
4948
5363
  const job = await bullmqDecorator({
5364
+ timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 180,
4949
5365
  label: `${this.name} ${field.name} data processor`,
4950
5366
  processor: `${this.id}-${field.name}`,
4951
5367
  context: this.id,
4952
5368
  inputs: item,
4953
5369
  item: item.id,
4954
5370
  queue: queue.queue,
5371
+ backoff: queue.backoff || {
5372
+ type: "exponential",
5373
+ delay: 2e3
5374
+ },
5375
+ retries: queue.retries || 2,
4955
5376
  user,
4956
5377
  role,
4957
- trigger,
4958
- retries: 2
5378
+ trigger
4959
5379
  });
4960
5380
  return {
4961
5381
  result: "",
@@ -5025,12 +5445,14 @@ var ExuluContext = class {
5025
5445
  role
5026
5446
  );
5027
5447
  await db3.from(getChunksTableName(this.id)).where({ source }).delete();
5028
- await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
5029
- source,
5030
- content: chunk.content,
5031
- chunk_index: chunk.index,
5032
- embedding: import_knex5.default.toSql(chunk.vector)
5033
- })));
5448
+ if (chunks?.length) {
5449
+ await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
5450
+ source,
5451
+ content: chunk.content,
5452
+ chunk_index: chunk.index,
5453
+ embedding: import_knex5.default.toSql(chunk.vector)
5454
+ })));
5455
+ }
5034
5456
  await db3.from(getTableName(this.id)).where({ id: item.id }).update({
5035
5457
  embeddings_updated_at: (/* @__PURE__ */ new Date()).toISOString()
5036
5458
  }).returning("id");
@@ -5159,9 +5581,15 @@ var ExuluContext = class {
5159
5581
  if (queue?.queue.name) {
5160
5582
  console.log("[EXULU] embedder is in queue mode, scheduling job.");
5161
5583
  const job = await bullmqDecorator({
5584
+ timeoutInSeconds: queue.timeoutInSeconds || 180,
5162
5585
  label: `${this.embedder.name}`,
5163
5586
  embedder: this.embedder.id,
5164
5587
  context: this.id,
5588
+ backoff: queue.backoff || {
5589
+ type: "exponential",
5590
+ delay: 2e3
5591
+ },
5592
+ retries: queue.retries || 2,
5165
5593
  inputs: item,
5166
5594
  item: item.id,
5167
5595
  queue: queue.queue,
@@ -5404,68 +5832,8 @@ var import_express7 = require("express");
5404
5832
 
5405
5833
  // src/registry/routes.ts
5406
5834
  var import_express3 = require("express");
5407
-
5408
- // src/bullmq/queues.ts
5409
- var import_bullmq4 = require("bullmq");
5410
- var import_bullmq_otel = require("bullmq-otel");
5411
- var ExuluQueues = class {
5412
- queues;
5413
- constructor() {
5414
- this.queues = [];
5415
- }
5416
- queue(name) {
5417
- return this.queues.find((x) => x.queue?.name === name);
5418
- }
5419
- // name: string
5420
- // concurrency: global concurrency for the queue
5421
- // ratelimit: maximum number of jobs per second
5422
- // Rate limit is set on workers (see workers.ts), even global rate limits,
5423
- // that is a bit counter-intuitive. Since queues are registered using .user
5424
- // method of ExuluQueues we need to store the desired rate limit on the queue
5425
- // here so we can use the value when creating workers for the queue instance
5426
- // as there is no way to store a rate limit value natively on a bullm queue.
5427
- use = async (name, concurrency = 1, ratelimit = 1) => {
5428
- const existing = this.queues.find((x) => x.queue?.name === name);
5429
- if (existing) {
5430
- const globalConcurrency = await existing.queue.getGlobalConcurrency();
5431
- if (globalConcurrency !== concurrency) {
5432
- await existing.queue.setGlobalConcurrency(concurrency);
5433
- }
5434
- return {
5435
- queue: existing.queue,
5436
- ratelimit,
5437
- concurrency
5438
- };
5439
- }
5440
- if (!redisServer.host?.length || !redisServer.port?.length) {
5441
- console.error(`[EXULU] no redis server configured, but you are trying to use a queue ( ${name}), likely in an agent or embedder (look for ExuluQueues.use() ).`);
5442
- throw new Error(`[EXULU] no redis server configured.`);
5443
- }
5444
- const newQueue = new import_bullmq4.Queue(
5445
- `${name}`,
5446
- {
5447
- connection: redisServer,
5448
- telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
5449
- }
5450
- );
5451
- await newQueue.setGlobalConcurrency(concurrency);
5452
- this.queues.push({
5453
- queue: newQueue,
5454
- ratelimit,
5455
- concurrency
5456
- });
5457
- return {
5458
- queue: newQueue,
5459
- ratelimit,
5460
- concurrency
5461
- };
5462
- };
5463
- };
5464
- var queues = new ExuluQueues();
5465
-
5466
- // src/registry/routes.ts
5467
5835
  var import_express4 = __toESM(require("express"), 1);
5468
- var import_server3 = require("@apollo/server");
5836
+ var import_server2 = require("@apollo/server");
5469
5837
  var import_cors = __toESM(require("cors"), 1);
5470
5838
  var import_reflect_metadata = require("reflect-metadata");
5471
5839
  var import_express5 = require("@as-integrations/express5");
@@ -5476,8 +5844,6 @@ var import_openai = __toESM(require("openai"), 1);
5476
5844
  var import_fs = __toESM(require("fs"), 1);
5477
5845
  var import_node_crypto3 = require("crypto");
5478
5846
  var import_api = require("@opentelemetry/api");
5479
- var import_ai2 = require("ai");
5480
- var import_express_http_proxy = __toESM(require("express-http-proxy"), 1);
5481
5847
  var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
5482
5848
 
5483
5849
  // src/registry/utils/claude-messages.ts
@@ -5503,13 +5869,15 @@ var CLAUDE_MESSAGES = {
5503
5869
  };
5504
5870
 
5505
5871
  // src/registry/routes.ts
5872
+ var import_ai2 = require("ai");
5506
5873
  var REQUEST_SIZE_LIMIT = "50mb";
5507
5874
  var global_queues = {
5508
- logs_cleaner: "logs-cleaner"
5875
+ eval_runs: "eval_runs"
5509
5876
  };
5510
5877
  var {
5511
5878
  agentsSchema: agentsSchema2,
5512
5879
  projectsSchema: projectsSchema2,
5880
+ jobResultsSchema: jobResultsSchema2,
5513
5881
  testCasesSchema: testCasesSchema2,
5514
5882
  evalSetsSchema: evalSetsSchema2,
5515
5883
  evalRunsSchema: evalRunsSchema2,
@@ -5523,38 +5891,7 @@ var {
5523
5891
  rbacSchema: rbacSchema2,
5524
5892
  statisticsSchema: statisticsSchema2
5525
5893
  } = coreSchemas.get();
5526
- var createRecurringJobs = async () => {
5527
- console.log("[EXULU] creating recurring jobs.");
5528
- const recurringJobSchedulersLogs = [];
5529
- const queue = await queues.use(global_queues.logs_cleaner);
5530
- recurringJobSchedulersLogs.push({
5531
- name: global_queues.logs_cleaner,
5532
- pattern: "0 10 * * * *",
5533
- ttld: "30 days",
5534
- opts: {
5535
- backoff: 3,
5536
- attempts: 5,
5537
- removeOnFail: 1e3
5538
- }
5539
- });
5540
- await queue.queue.upsertJobScheduler(
5541
- "logs-cleaner-scheduler",
5542
- { pattern: "0 10 * * * *" },
5543
- // every 10 minutes
5544
- {
5545
- name: global_queues.logs_cleaner,
5546
- data: { ttld: 30 },
5547
- // time to live in days
5548
- opts: {
5549
- backoff: 3,
5550
- attempts: 5,
5551
- removeOnFail: 1e3
5552
- }
5553
- }
5554
- );
5555
- return queue;
5556
- };
5557
- var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) => {
5894
+ var createExpressRoutes = async (app, agents, tools, contexts, config, evals, tracer, queues2) => {
5558
5895
  var corsOptions = {
5559
5896
  origin: "*",
5560
5897
  exposedHeaders: "*",
@@ -5573,19 +5910,15 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5573
5910
  \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
5574
5911
  \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
5575
5912
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
5576
- Intelligence Management Platform
5913
+ Intelligence Management Platform - Server
5577
5914
 
5578
5915
  `);
5579
- if (redisServer.host?.length && redisServer.port?.length) {
5580
- await createRecurringJobs();
5581
- } else {
5582
- console.log("[EXULU] no redis server configured, not setting up recurring jobs.");
5583
- }
5584
5916
  const schema = createSDL([
5585
5917
  usersSchema2(),
5586
5918
  rolesSchema2(),
5587
5919
  agentsSchema2(),
5588
5920
  projectsSchema2(),
5921
+ jobResultsSchema2(),
5589
5922
  evalRunsSchema2(),
5590
5923
  platformConfigurationsSchema2(),
5591
5924
  evalSetsSchema2(),
@@ -5596,8 +5929,8 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5596
5929
  workflowTemplatesSchema2(),
5597
5930
  statisticsSchema2(),
5598
5931
  rbacSchema2()
5599
- ], contexts ?? [], agents, tools, config);
5600
- const server = new import_server3.ApolloServer({
5932
+ ], contexts ?? [], agents, tools, config, evals, queues2 || []);
5933
+ const server = new import_server2.ApolloServer({
5601
5934
  cache: new import_utils3.InMemoryLRUCache(),
5602
5935
  schema,
5603
5936
  introspection: true
@@ -5727,103 +6060,6 @@ Mood: friendly and intelligent.
5727
6060
  image: `${process.env.BACKEND}/${uuid}.png`
5728
6061
  });
5729
6062
  });
5730
- app.post("/evals/run/:id", async (req, res) => {
5731
- console.log("[EXULU] /evals/run/:id", req.params.id);
5732
- const authenticationResult = await requestValidators.authenticate(req);
5733
- if (!authenticationResult.user?.id) {
5734
- res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
5735
- return;
5736
- }
5737
- const user = authenticationResult.user;
5738
- const evalRunId = req.params.id;
5739
- if (!user.super_admin && (!user.role || user.role.evals !== "write")) {
5740
- res.status(403).json({
5741
- message: "You don't have permission to run evals. Required: super_admin or evals write access."
5742
- });
5743
- return;
5744
- }
5745
- const { db: db3 } = await postgresClient();
5746
- const evalRun = await db3.from("eval_runs").where({ id: evalRunId }).first();
5747
- if (!evalRun) {
5748
- res.status(404).json({
5749
- message: "Eval run not found."
5750
- });
5751
- return;
5752
- }
5753
- const hasAccessToEvalRun = await checkRecordAccess(evalRun, "write", user);
5754
- if (!hasAccessToEvalRun) {
5755
- res.status(403).json({
5756
- message: "You don't have access to this eval run."
5757
- });
5758
- return;
5759
- }
5760
- const testCaseIds = evalRun.test_case_ids ? typeof evalRun.test_case_ids === "string" ? JSON.parse(evalRun.test_case_ids) : evalRun.test_case_ids : [];
5761
- const evalFunctionIds = evalRun.eval_function_ids ? typeof evalRun.eval_function_ids === "string" ? JSON.parse(evalRun.eval_function_ids) : evalRun.eval_function_ids : [];
5762
- if (!testCaseIds || testCaseIds.length === 0) {
5763
- res.status(400).json({
5764
- message: "No test cases selected for this eval run."
5765
- });
5766
- return;
5767
- }
5768
- if (!evalFunctionIds || evalFunctionIds.length === 0) {
5769
- res.status(400).json({
5770
- message: "No eval functions selected for this eval run."
5771
- });
5772
- return;
5773
- }
5774
- const testCases = await db3.from("test_cases").whereIn("id", testCaseIds);
5775
- if (testCases.length === 0) {
5776
- res.status(404).json({
5777
- message: "No test cases found."
5778
- });
5779
- return;
5780
- }
5781
- const agentInstance = await loadAgent(evalRun.agentId);
5782
- if (!agentInstance) {
5783
- res.status(404).json({
5784
- message: "Agent instance not found."
5785
- });
5786
- return;
5787
- }
5788
- const evalQueue = await queues.use("evals");
5789
- const jobIds = [];
5790
- for (const testCase of testCases) {
5791
- const existingJobs = await evalQueue.queue.getJobs(["waiting", "active", "delayed", "paused"]);
5792
- const duplicateJob = existingJobs.find(
5793
- (job2) => job2.data.evalRunId === evalRunId && job2.data.testCaseId === testCase.id && job2.data.type === "eval"
5794
- );
5795
- if (duplicateJob) {
5796
- console.log(`[EXULU] Skipping duplicate job for eval run ${evalRunId} and test case ${testCase.id}`);
5797
- continue;
5798
- }
5799
- const job = await evalQueue.queue.add(`eval-${testCase.id}`, {
5800
- type: "eval",
5801
- evalRunId,
5802
- testCaseId: testCase.id,
5803
- evalFunctionIds,
5804
- // Array of eval function IDs - worker will create child jobs for these
5805
- agentId: evalRun.agentId,
5806
- inputs: testCase.inputs,
5807
- expected_output: testCase.expected_output,
5808
- expected_tools: testCase.expected_tools,
5809
- expected_knowledge_sources: testCase.expected_knowledge_sources,
5810
- expected_agent_tools: testCase.expected_agent_tools,
5811
- config: evalRun.config,
5812
- scoring_method: evalRun.scoring_method,
5813
- pass_threshold: evalRun.pass_threshold,
5814
- user: user.id,
5815
- role: user.role?.id
5816
- });
5817
- jobIds.push(job.id);
5818
- }
5819
- res.status(200).json({
5820
- message: `Created ${jobIds.length} eval jobs.`,
5821
- jobIds,
5822
- evalRunId,
5823
- testCaseCount: testCases.length,
5824
- evalFunctionCount: evalFunctionIds.length
5825
- });
5826
- });
5827
6063
  app.get("/ping", async (req, res) => {
5828
6064
  const authenticationResult = await requestValidators.authenticate(req);
5829
6065
  if (!authenticationResult.user?.id) {
@@ -5952,11 +6188,11 @@ Mood: friendly and intelligent.
5952
6188
  providerapikey = bytes.toString(import_crypto_js3.default.enc.Utf8);
5953
6189
  }
5954
6190
  if (!!headers.stream) {
5955
- await agent.generateStream({
5956
- express: {
5957
- res,
5958
- req
5959
- },
6191
+ const statistics = {
6192
+ label: agent.name,
6193
+ trigger: "agent"
6194
+ };
6195
+ const result = await agent.generateStream({
5960
6196
  contexts,
5961
6197
  user,
5962
6198
  instructions: agentInstance.instructions,
@@ -5966,10 +6202,79 @@ Mood: friendly and intelligent.
5966
6202
  allExuluTools: tools,
5967
6203
  providerapikey,
5968
6204
  toolConfigs: agentInstance.tools,
5969
- exuluConfig: config,
5970
- statistics: {
5971
- label: agent.name,
5972
- trigger: "agent"
6205
+ exuluConfig: config
6206
+ });
6207
+ result.stream.consumeStream();
6208
+ result.stream.pipeUIMessageStreamToResponse(res, {
6209
+ messageMetadata: ({ part }) => {
6210
+ if (part.type === "finish") {
6211
+ return {
6212
+ totalTokens: part.totalUsage.totalTokens,
6213
+ reasoningTokens: part.totalUsage.reasoningTokens,
6214
+ inputTokens: part.totalUsage.inputTokens,
6215
+ outputTokens: part.totalUsage.outputTokens,
6216
+ cachedInputTokens: part.totalUsage.cachedInputTokens
6217
+ };
6218
+ }
6219
+ },
6220
+ originalMessages: result.originalMessages,
6221
+ sendReasoning: true,
6222
+ sendSources: true,
6223
+ onError: (error) => {
6224
+ console.error("[EXULU] chat response error.", error);
6225
+ return errorHandler(error);
6226
+ },
6227
+ generateMessageId: (0, import_ai2.createIdGenerator)({
6228
+ prefix: "msg_",
6229
+ size: 16
6230
+ }),
6231
+ onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
6232
+ if (headers.session) {
6233
+ await saveChat({
6234
+ session: headers.session,
6235
+ user: user.id,
6236
+ messages: messages.filter((x) => !result.previousMessages.find((y) => y.id === x.id))
6237
+ });
6238
+ }
6239
+ const metadata = messages[messages.length - 1]?.metadata;
6240
+ console.log("[EXULU] Finished streaming", metadata);
6241
+ console.log("[EXULU] Statistics", {
6242
+ label: agent.name,
6243
+ trigger: "agent"
6244
+ });
6245
+ if (statistics) {
6246
+ await Promise.all([
6247
+ updateStatistic({
6248
+ name: "count",
6249
+ label: statistics.label,
6250
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6251
+ trigger: statistics.trigger,
6252
+ count: 1,
6253
+ user: user.id,
6254
+ role: user?.role?.id
6255
+ }),
6256
+ ...metadata?.inputTokens ? [
6257
+ updateStatistic({
6258
+ name: "inputTokens",
6259
+ label: statistics.label,
6260
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6261
+ trigger: statistics.trigger,
6262
+ count: metadata?.inputTokens,
6263
+ user: user.id,
6264
+ role: user?.role?.id
6265
+ })
6266
+ ] : [],
6267
+ ...metadata?.outputTokens ? [
6268
+ updateStatistic({
6269
+ name: "outputTokens",
6270
+ label: statistics.label,
6271
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6272
+ trigger: statistics.trigger,
6273
+ count: metadata?.outputTokens
6274
+ })
6275
+ ] : []
6276
+ ]);
6277
+ }
5973
6278
  }
5974
6279
  });
5975
6280
  return;
@@ -5978,7 +6283,7 @@ Mood: friendly and intelligent.
5978
6283
  user,
5979
6284
  instructions: agentInstance.instructions,
5980
6285
  session: headers.session,
5981
- message: req.body.message,
6286
+ inputMessages: [req.body.message],
5982
6287
  contexts,
5983
6288
  currentTools: enabledTools,
5984
6289
  allExuluTools: tools,
@@ -5995,96 +6300,11 @@ Mood: friendly and intelligent.
5995
6300
  }
5996
6301
  });
5997
6302
  });
5998
- if (config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
6303
+ if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
5999
6304
  await createUppyRoutes(app, config);
6000
6305
  } else {
6001
6306
  console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
6002
6307
  }
6003
- app.use("/xxx/anthropic/:id", import_express4.default.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }), (0, import_express_http_proxy.default)(
6004
- (req, res, next) => {
6005
- return "https://api.anthropic.com";
6006
- },
6007
- {
6008
- limit: "50mb",
6009
- memoizeHost: false,
6010
- preserveHostHdr: true,
6011
- secure: false,
6012
- reqAsBuffer: true,
6013
- proxyReqBodyDecorator: function(bodyContent, srcReq) {
6014
- return bodyContent;
6015
- },
6016
- userResDecorator: function(proxyRes, proxyResData, userReq, userRes) {
6017
- console.log("[EXULU] Proxy response!", proxyResData);
6018
- proxyResData = proxyResData.toString();
6019
- console.log("[EXULU] Proxy response string!", proxyResData);
6020
- return proxyResData;
6021
- },
6022
- proxyReqPathResolver: (req) => {
6023
- const prefix = `/gateway/anthropic/${req.params.id}`;
6024
- let path = req.url.startsWith(prefix) ? req.url.slice(prefix.length) : req.url;
6025
- if (!path.startsWith("/")) path = "/" + path;
6026
- console.log("[EXULU] Provider path:", path);
6027
- return path;
6028
- },
6029
- proxyReqOptDecorator: function(proxyReqOpts, srcReq) {
6030
- return new Promise(async (resolve, reject) => {
6031
- try {
6032
- const authenticationResult = await requestValidators.authenticate(srcReq);
6033
- if (!authenticationResult.user?.id) {
6034
- console.log("[EXULU] failed authentication result", authenticationResult);
6035
- reject(authenticationResult.message);
6036
- return;
6037
- }
6038
- console.log("[EXULU] Authenticated call", authenticationResult.user?.email);
6039
- const { db: db3 } = await postgresClient();
6040
- let query = db3("agents");
6041
- query.select("*");
6042
- query = applyAccessControl(agentsSchema2(), authenticationResult.user, query);
6043
- query.where({ id: srcReq.params.id });
6044
- const agent = await query.first();
6045
- if (!agent) {
6046
- reject(new Error("Agent with id " + srcReq.params.id + " not found."));
6047
- return;
6048
- }
6049
- console.log("[EXULU] Agent loaded", agent.name);
6050
- const backend = agents.find((x) => x.id === agent.backend);
6051
- if (!process.env.NEXTAUTH_SECRET) {
6052
- reject(new Error("Missing NEXTAUTH_SECRET"));
6053
- return;
6054
- }
6055
- if (!agent.providerapikey) {
6056
- reject(new Error("API Key not set for agent"));
6057
- return;
6058
- }
6059
- const variableName = agent.providerapikey;
6060
- const variable = await db3.from("variables").where({ name: variableName }).first();
6061
- console.log("[EXULU] Variable loaded", variable);
6062
- let providerapikey = variable.value;
6063
- if (!variable.encrypted) {
6064
- reject(new Error("API Key not encrypted for agent"));
6065
- return;
6066
- }
6067
- if (variable.encrypted) {
6068
- const bytes = import_crypto_js3.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6069
- providerapikey = bytes.toString(import_crypto_js3.default.enc.Utf8);
6070
- }
6071
- console.log("[EXULU] Provider API key", providerapikey);
6072
- proxyReqOpts.headers["x-api-key"] = providerapikey;
6073
- proxyReqOpts.rejectUnauthorized = false;
6074
- delete proxyReqOpts.headers["provider"];
6075
- const url = new URL("https://api.anthropic.com");
6076
- proxyReqOpts.headers["host"] = url.host;
6077
- proxyReqOpts.headers["anthropic-version"] = "2023-06-01";
6078
- console.log("[EXULU] Proxy request headers", proxyReqOpts.headers);
6079
- resolve(proxyReqOpts);
6080
- } catch (error) {
6081
- console.error("[EXULU] Proxy error", error);
6082
- reject(error);
6083
- }
6084
- });
6085
- }
6086
- }
6087
- ));
6088
6308
  app.get("/config", async (req, res) => {
6089
6309
  res.status(200).json({
6090
6310
  message: "Config fetched successfully.",
@@ -6256,10 +6476,36 @@ var createCustomAnthropicStreamingMessage = (message) => {
6256
6476
 
6257
6477
  // src/registry/workers.ts
6258
6478
  var import_ioredis = __toESM(require("ioredis"), 1);
6259
- var import_bullmq5 = require("bullmq");
6479
+ var import_bullmq4 = require("bullmq");
6260
6480
  var import_api2 = require("@opentelemetry/api");
6481
+ var import_uuid3 = require("uuid");
6482
+ var import_ai3 = require("ai");
6483
+ var import_crypto_js4 = __toESM(require("crypto-js"), 1);
6484
+
6485
+ // src/registry/log-metadata.ts
6486
+ function logMetadata(id, additionalMetadata) {
6487
+ return {
6488
+ __logMetadata: true,
6489
+ id,
6490
+ ...additionalMetadata
6491
+ };
6492
+ }
6493
+
6494
+ // src/registry/workers.ts
6261
6495
  var redisConnection;
6262
- var createWorkers = async (queues2, config, contexts, tracer) => {
6496
+ var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
6497
+ console.log(`
6498
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
6499
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
6500
+ \u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
6501
+ \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
6502
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
6503
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
6504
+ Intelligence Management Platform - Workers
6505
+
6506
+ `);
6507
+ console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
6508
+ console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
6263
6509
  if (!redisServer.host || !redisServer.port) {
6264
6510
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
6265
6511
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -6267,80 +6513,333 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6267
6513
  if (!redisConnection) {
6268
6514
  redisConnection = new import_ioredis.default({
6269
6515
  ...redisServer,
6516
+ enableOfflineQueue: true,
6517
+ retryStrategy: function(times) {
6518
+ return Math.max(Math.min(Math.exp(times), 2e4), 1e3);
6519
+ },
6270
6520
  maxRetriesPerRequest: null
6271
6521
  });
6272
6522
  }
6273
6523
  const workers = queues2.map((queue) => {
6274
- console.log(`[EXULU] creating worker for queue ${queue}.`);
6275
- const worker = new import_bullmq5.Worker(
6276
- `${queue}`,
6524
+ console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
6525
+ const worker = new import_bullmq4.Worker(
6526
+ `${queue.queue.name}`,
6277
6527
  async (bullmqJob) => {
6528
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
6529
+ name: bullmqJob.name,
6530
+ status: await bullmqJob.getState(),
6531
+ type: bullmqJob.data.type
6532
+ }));
6278
6533
  const { db: db3 } = await postgresClient();
6279
- try {
6280
- const data = bullmqJob.data;
6281
- bullmq.validate(bullmqJob.id, data);
6282
- if (data.type === "embedder") {
6283
- const context = contexts.find((context2) => context2.id === data.context);
6284
- if (!context) {
6285
- throw new Error(`Context ${data.context} not found in the registry.`);
6286
- }
6287
- if (!data.embedder) {
6288
- throw new Error(`No embedder set for embedder job.`);
6289
- }
6290
- const embedder = contexts.find((context2) => context2.embedder?.id === data.embedder);
6291
- if (!embedder) {
6292
- throw new Error(`Embedder ${data.embedder} not found in the registry.`);
6293
- }
6294
- const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
6295
- label: embedder.name,
6296
- trigger: data.trigger
6297
- }, data.role, bullmqJob.id);
6298
- return result;
6299
- }
6300
- if (data.type === "processor") {
6301
- const context = contexts.find((context2) => context2.id === data.context);
6302
- if (!context) {
6303
- throw new Error(`Context ${data.context} not found in the registry.`);
6304
- }
6305
- const field = context.fields.find((field2) => field2.name === data.inputs.field);
6306
- if (!field) {
6307
- throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
6308
- }
6309
- if (!field.processor) {
6310
- throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
6534
+ const data = bullmqJob.data;
6535
+ const timeoutMs = data.timeoutInSeconds * 1e3;
6536
+ const timeoutPromise = new Promise((_, reject) => {
6537
+ setTimeout(() => {
6538
+ reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${data.timeoutInSeconds}s`));
6539
+ }, timeoutMs);
6540
+ });
6541
+ const workPromise = (async () => {
6542
+ try {
6543
+ bullmq.validate(bullmqJob.id, data);
6544
+ if (data.type === "embedder") {
6545
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
6546
+ const label = `embedder-${bullmqJob.name}`;
6547
+ await db3.from("job_results").insert({
6548
+ job_id: bullmqJob.id,
6549
+ label,
6550
+ state: await bullmqJob.getState(),
6551
+ result: null,
6552
+ metadata: {}
6553
+ });
6554
+ const context = contexts.find((context2) => context2.id === data.context);
6555
+ if (!context) {
6556
+ throw new Error(`Context ${data.context} not found in the registry.`);
6557
+ }
6558
+ if (!data.embedder) {
6559
+ throw new Error(`No embedder set for embedder job.`);
6560
+ }
6561
+ const embedder = contexts.find((context2) => context2.embedder?.id === data.embedder);
6562
+ if (!embedder) {
6563
+ throw new Error(`Embedder ${data.embedder} not found in the registry.`);
6564
+ }
6565
+ const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
6566
+ label: embedder.name,
6567
+ trigger: data.trigger
6568
+ }, data.role, bullmqJob.id);
6569
+ return {
6570
+ result,
6571
+ metadata: {}
6572
+ };
6311
6573
  }
6312
- const exuluStorage = new ExuluStorage({ config });
6313
- if (!data.user) {
6314
- throw new Error(`User not set for processor job.`);
6574
+ if (data.type === "processor") {
6575
+ console.log("[EXULU] running a processor job.", logMetadata(bullmqJob.name));
6576
+ const label = `processor-${bullmqJob.name}`;
6577
+ await db3.from("job_results").insert({
6578
+ job_id: bullmqJob.id,
6579
+ label,
6580
+ state: await bullmqJob.getState(),
6581
+ result: null,
6582
+ metadata: {}
6583
+ });
6584
+ const context = contexts.find((context2) => context2.id === data.context);
6585
+ if (!context) {
6586
+ throw new Error(`Context ${data.context} not found in the registry.`);
6587
+ }
6588
+ const field = context.fields.find((field2) => field2.name === data.inputs.field);
6589
+ if (!field) {
6590
+ throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
6591
+ }
6592
+ if (!field.processor) {
6593
+ throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
6594
+ }
6595
+ const exuluStorage = new ExuluStorage({ config });
6596
+ if (!data.user) {
6597
+ throw new Error(`User not set for processor job.`);
6598
+ }
6599
+ if (!data.role) {
6600
+ throw new Error(`Role not set for processor job.`);
6601
+ }
6602
+ const result = await field.processor.execute({
6603
+ item: data.inputs,
6604
+ user: data.user,
6605
+ role: data.role,
6606
+ utils: {
6607
+ storage: exuluStorage,
6608
+ items: {
6609
+ update: context.updateItem,
6610
+ create: context.createItem,
6611
+ delete: context.deleteItem
6612
+ }
6613
+ },
6614
+ config
6615
+ });
6616
+ return {
6617
+ result,
6618
+ metadata: {}
6619
+ };
6315
6620
  }
6316
- if (!data.role) {
6317
- throw new Error(`Role not set for processor job.`);
6621
+ if (data.type === "eval_run") {
6622
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
6623
+ const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
6624
+ const existingResult = await db3.from("job_results").where({ label }).first();
6625
+ if (existingResult) {
6626
+ await db3.from("job_results").where({ label }).update({
6627
+ job_id: bullmqJob.id,
6628
+ label,
6629
+ state: await bullmqJob.getState(),
6630
+ result: null,
6631
+ metadata: {},
6632
+ tries: existingResult.tries + 1
6633
+ });
6634
+ } else {
6635
+ await db3.from("job_results").insert({
6636
+ job_id: bullmqJob.id,
6637
+ label,
6638
+ state: await bullmqJob.getState(),
6639
+ result: null,
6640
+ metadata: {},
6641
+ tries: 1
6642
+ });
6643
+ }
6644
+ const {
6645
+ agentInstance,
6646
+ backend: agentBackend,
6647
+ user,
6648
+ evalRun,
6649
+ testCase,
6650
+ messages: inputMessages
6651
+ } = await validateEvalPayload(data, agents);
6652
+ const retries = 3;
6653
+ let attempts = 0;
6654
+ const promise = new Promise(async (resolve, reject) => {
6655
+ while (attempts < retries) {
6656
+ try {
6657
+ const messages2 = await processUiMessagesFlow({
6658
+ agents,
6659
+ agentInstance,
6660
+ agentBackend,
6661
+ inputMessages,
6662
+ contexts,
6663
+ user,
6664
+ tools,
6665
+ config
6666
+ });
6667
+ resolve(messages2);
6668
+ break;
6669
+ } catch (error) {
6670
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
6671
+ error: error instanceof Error ? error.message : String(error)
6672
+ }));
6673
+ attempts++;
6674
+ if (attempts >= retries) {
6675
+ reject(error);
6676
+ }
6677
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
6678
+ }
6679
+ }
6680
+ });
6681
+ const result = await promise;
6682
+ const messages = result.messages;
6683
+ const metadata = result.metadata;
6684
+ const evalFunctions = evalRun.eval_functions;
6685
+ let evalFunctionResults = [];
6686
+ for (const evalFunction of evalFunctions) {
6687
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6688
+ if (!evalMethod) {
6689
+ throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
6690
+ }
6691
+ let result2;
6692
+ if (evalMethod.queue) {
6693
+ const queue2 = await evalMethod.queue;
6694
+ const jobData = {
6695
+ ...data,
6696
+ type: "eval_function",
6697
+ eval_functions: [{
6698
+ id: evalFunction.id,
6699
+ config: evalFunction.config || {}
6700
+ }],
6701
+ // updating the input messages with the messages we want to run the eval
6702
+ // function on, which are the output messages from the agent.
6703
+ inputs: messages
6704
+ };
6705
+ const redisId = (0, import_uuid3.v4)();
6706
+ const job = await queue2.queue.add("eval_function", jobData, {
6707
+ jobId: redisId,
6708
+ // Setting it to 3 as a sensible default, as
6709
+ // many AI services are quite unstable.
6710
+ attempts: queue2.retries || 3,
6711
+ // todo make this configurable?
6712
+ removeOnComplete: 5e3,
6713
+ removeOnFail: 1e4,
6714
+ backoff: queue2.backoff || {
6715
+ type: "exponential",
6716
+ delay: 2e3
6717
+ }
6718
+ });
6719
+ if (!job.id) {
6720
+ throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
6721
+ }
6722
+ result2 = await pollJobResult({ queue: queue2, jobId: job.id });
6723
+ const evalFunctionResult = {
6724
+ test_case_id: testCase.id,
6725
+ eval_run_id: evalRun.id,
6726
+ eval_function_id: evalFunction.id,
6727
+ result: result2 || 0
6728
+ };
6729
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6730
+ result: result2 || 0
6731
+ }));
6732
+ evalFunctionResults.push(evalFunctionResult);
6733
+ } else {
6734
+ result2 = await evalMethod.run(
6735
+ agentInstance,
6736
+ agentBackend,
6737
+ testCase,
6738
+ messages,
6739
+ evalFunction.config || {}
6740
+ );
6741
+ const evalFunctionResult = {
6742
+ test_case_id: testCase.id,
6743
+ eval_run_id: evalRun.id,
6744
+ eval_function_id: evalFunction.id,
6745
+ result: result2 || 0
6746
+ };
6747
+ evalFunctionResults.push(evalFunctionResult);
6748
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6749
+ result: result2 || 0
6750
+ }));
6751
+ }
6752
+ }
6753
+ const scores = evalFunctionResults.map((result2) => result2.result);
6754
+ let score = 0;
6755
+ switch (data.scoring_method) {
6756
+ case "median":
6757
+ score = getMedian(scores);
6758
+ break;
6759
+ case "average":
6760
+ score = getAverage(scores);
6761
+ break;
6762
+ case "sum":
6763
+ score = getSum(scores);
6764
+ break;
6765
+ }
6766
+ return {
6767
+ result: score,
6768
+ metadata: {
6769
+ ...evalFunctionResults,
6770
+ ...metadata
6771
+ }
6772
+ };
6318
6773
  }
6319
- const result = await field.processor.execute({
6320
- item: data.inputs,
6321
- user: data.user,
6322
- role: data.role,
6323
- utils: {
6324
- storage: exuluStorage,
6325
- items: {
6326
- update: context.updateItem
6774
+ if (data.type === "eval_function") {
6775
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
6776
+ if (data.eval_functions?.length !== 1) {
6777
+ throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
6778
+ }
6779
+ const label = `eval-function-${data.eval_run_id}-${data.test_case_id}-${data.eval_functions?.[0]?.id}`;
6780
+ const existingResult = await db3.from("job_results").where({ label }).first();
6781
+ if (existingResult) {
6782
+ await db3.from("job_results").where({ label }).update({
6783
+ job_id: bullmqJob.id,
6784
+ label,
6785
+ state: await bullmqJob.getState(),
6786
+ result: null,
6787
+ metadata: {},
6788
+ tries: existingResult.tries + 1
6789
+ });
6790
+ } else {
6791
+ await db3.from("job_results").insert({
6792
+ job_id: bullmqJob.id,
6793
+ label,
6794
+ state: await bullmqJob.getState(),
6795
+ result: null,
6796
+ metadata: {},
6797
+ tries: 1
6798
+ });
6799
+ }
6800
+ const {
6801
+ evalRun,
6802
+ agentInstance,
6803
+ backend,
6804
+ testCase,
6805
+ messages: inputMessages
6806
+ } = await validateEvalPayload(data, agents);
6807
+ const evalFunctions = evalRun.eval_functions;
6808
+ let result;
6809
+ for (const evalFunction of evalFunctions) {
6810
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6811
+ if (!evalMethod) {
6812
+ throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
6327
6813
  }
6328
- },
6329
- config
6330
- });
6331
- return result;
6814
+ result = await evalMethod.run(
6815
+ agentInstance,
6816
+ backend,
6817
+ testCase,
6818
+ inputMessages,
6819
+ evalFunction.config || {}
6820
+ );
6821
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
6822
+ result: result || 0
6823
+ }));
6824
+ }
6825
+ return {
6826
+ result,
6827
+ metadata: {}
6828
+ };
6829
+ }
6830
+ throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
6831
+ } catch (error) {
6832
+ console.error(`[EXULU] job failed.`, error instanceof Error ? error.message : String(error));
6833
+ throw error;
6332
6834
  }
6333
- } catch (error) {
6334
- await db3.from("jobs").where({ redis: bullmqJob.id }).update({
6335
- status: "failed",
6336
- finishedAt: /* @__PURE__ */ new Date(),
6337
- error: error instanceof Error ? error.message : String(error)
6338
- });
6339
- throw new Error(error instanceof Error ? error.message : String(error));
6340
- }
6835
+ })();
6836
+ return Promise.race([workPromise, timeoutPromise]);
6341
6837
  },
6342
6838
  {
6839
+ autorun: true,
6343
6840
  connection: redisConnection,
6841
+ removeOnComplete: { count: 1e3 },
6842
+ removeOnFail: { count: 5e3 },
6344
6843
  ...queue.ratelimit && {
6345
6844
  limiter: {
6346
6845
  max: queue.ratelimit,
@@ -6349,22 +6848,312 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6349
6848
  }
6350
6849
  }
6351
6850
  );
6352
- worker.on("completed", (job, returnvalue) => {
6353
- console.log(`[EXULU] completed job ${job.id}.`, returnvalue);
6851
+ worker.on("completed", async (job, returnvalue) => {
6852
+ console.log(`[EXULU] completed job ${job.id}.`, logMetadata(job.name, {
6853
+ result: returnvalue
6854
+ }));
6855
+ const { db: db3 } = await postgresClient();
6856
+ await db3.from("job_results").where({ job_id: job.id }).update({
6857
+ state: JOB_STATUS_ENUM.completed,
6858
+ result: returnvalue.result,
6859
+ metadata: returnvalue.metadata
6860
+ });
6354
6861
  });
6355
- worker.on("failed", (job, error, prev) => {
6862
+ worker.on("failed", async (job, error, prev) => {
6356
6863
  if (job?.id) {
6357
- console.error(`[EXULU] failed job ${job.id}.`);
6864
+ const { db: db3 } = await postgresClient();
6865
+ console.error(`[EXULU] failed job ${job.id}.`, error);
6866
+ await db3.from("job_results").where({ job_id: job.id }).update({
6867
+ state: JOB_STATUS_ENUM.failed,
6868
+ error
6869
+ });
6870
+ return;
6358
6871
  }
6359
- console.error(`[EXULU] job error.`, error);
6872
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
6873
+ error: error instanceof Error ? error.message : String(error)
6874
+ }) : error);
6875
+ });
6876
+ worker.on("error", (error) => {
6877
+ console.error(`[EXULU] worker error.`, error);
6360
6878
  });
6361
6879
  worker.on("progress", (job, progress) => {
6362
- console.log(`[EXULU] job progress ${job.id}.`, progress);
6880
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
6881
+ progress
6882
+ }));
6363
6883
  });
6884
+ const gracefulShutdown = async (signal) => {
6885
+ console.log(`Received ${signal}, closing server...`);
6886
+ await worker.close();
6887
+ process.exit(0);
6888
+ };
6889
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
6890
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
6364
6891
  return worker;
6365
6892
  });
6366
6893
  return workers;
6367
6894
  };
6895
+ var validateEvalPayload = async (data, agents) => {
6896
+ if (!data.eval_run_id) {
6897
+ throw new Error(`No eval run ID set for eval job.`);
6898
+ }
6899
+ if (!data.test_case_id) {
6900
+ throw new Error(`No test case ID set for eval job.`);
6901
+ }
6902
+ if (!data.user) {
6903
+ throw new Error(`No user set for eval job.`);
6904
+ }
6905
+ if (!data.role) {
6906
+ throw new Error(`No role set for eval job.`);
6907
+ }
6908
+ if (!data.agent_id) {
6909
+ throw new Error(`No agent ID set for eval job.`);
6910
+ }
6911
+ if (!data.inputs?.length) {
6912
+ throw new Error(`No inputs set for eval job, expected array of UIMessage objects.`);
6913
+ }
6914
+ const { db: db3 } = await postgresClient();
6915
+ const evalRun = await db3.from("eval_runs").where({ id: data.eval_run_id }).first();
6916
+ if (!evalRun) {
6917
+ throw new Error(`Eval run ${data.eval_run_id} not found in the database.`);
6918
+ }
6919
+ const agentInstance = await loadAgent(evalRun.agent_id);
6920
+ if (!agentInstance) {
6921
+ throw new Error(`Agent ${evalRun.agent_id} not found in the database.`);
6922
+ }
6923
+ const backend = agents.find((a) => a.id === agentInstance.backend);
6924
+ if (!backend) {
6925
+ throw new Error(`Backend ${agentInstance.backend} not found in the database.`);
6926
+ }
6927
+ const user = await db3.from("users").where({ id: data.user }).first();
6928
+ if (!user) {
6929
+ throw new Error(`User ${data.user} not found in the database.`);
6930
+ }
6931
+ const testCase = await db3.from("test_cases").where({ id: data.test_case_id }).first();
6932
+ if (!testCase) {
6933
+ throw new Error(`Test case ${data.test_case_id} not found in the database.`);
6934
+ }
6935
+ return {
6936
+ agentInstance,
6937
+ backend,
6938
+ user,
6939
+ testCase,
6940
+ evalRun,
6941
+ messages: data.inputs
6942
+ };
6943
+ };
6944
+ var pollJobResult = async ({ queue, jobId }) => {
6945
+ let attempts = 0;
6946
+ let timeoutInSeconds = queue.timeoutInSeconds || 180;
6947
+ const startTime = Date.now();
6948
+ let result;
6949
+ while (true) {
6950
+ attempts++;
6951
+ const job = await import_bullmq4.Job.fromId(queue.queue, jobId);
6952
+ if (!job) {
6953
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
6954
+ continue;
6955
+ }
6956
+ const elapsedTime = Date.now() - startTime;
6957
+ if (elapsedTime > timeoutInSeconds * 1e3) {
6958
+ throw new Error(`Job ${job.id} timed out after ${timeoutInSeconds} seconds for job eval function job ${job.name}.`);
6959
+ }
6960
+ console.log(`[EXULU] polling eval function job ${job.name} for state... (attempt ${attempts})`);
6961
+ const jobState = await job.getState();
6962
+ console.log(`[EXULU] eval function job ${job.name} state: ${jobState}`);
6963
+ if (jobState === "failed") {
6964
+ throw new Error(`Job ${job.name} (${job.id}) failed with error: ${job.failedReason}.`);
6965
+ }
6966
+ if (jobState === "completed") {
6967
+ console.log(`[EXULU] eval function job ${job.name} completed, getting result from database...`);
6968
+ const { db: db3 } = await postgresClient();
6969
+ const entry = await db3.from("job_results").where({ job_id: job.id }).first();
6970
+ result = entry?.result;
6971
+ if (result === void 0 || result === null || result === "") {
6972
+ throw new Error(`Eval function ${job.id} result not found in database for job eval function job ${job.name}.`);
6973
+ }
6974
+ console.log(`[EXULU] eval function ${job.id} result: ${result}`);
6975
+ break;
6976
+ }
6977
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
6978
+ }
6979
+ return result;
6980
+ };
6981
+ var processUiMessagesFlow = async ({
6982
+ agents,
6983
+ agentInstance,
6984
+ agentBackend,
6985
+ inputMessages,
6986
+ contexts,
6987
+ user,
6988
+ tools,
6989
+ config
6990
+ }) => {
6991
+ console.log("[EXULU] processing UI messages flow for agent.");
6992
+ console.log("[EXULU] input messages", inputMessages);
6993
+ console.log("[EXULU] agent tools", agentInstance.tools?.map((x) => x.name + " (" + x.id + ")"));
6994
+ const disabledTools = [];
6995
+ let enabledTools = await getEnabledTools(agentInstance, tools, disabledTools, agents, user);
6996
+ console.log("[EXULU] enabled tools", enabledTools?.map((x) => x.name + " (" + x.id + ")"));
6997
+ const variableName = agentInstance.providerapikey;
6998
+ const { db: db3 } = await postgresClient();
6999
+ const variable = await db3.from("variables").where({ name: variableName }).first();
7000
+ if (!variable) {
7001
+ throw new Error(`Provider API key variable not found for agent ${agentInstance.name} (${agentInstance.id}).`);
7002
+ }
7003
+ let providerapikey = variable.value;
7004
+ if (!variable.encrypted) {
7005
+ throw new Error(`Provider API key variable not encrypted for agent ${agentInstance.name} (${agentInstance.id}), for security reasons you are only allowed to use encrypted variables for provider API keys.`);
7006
+ }
7007
+ if (variable.encrypted) {
7008
+ const bytes = import_crypto_js4.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7009
+ providerapikey = bytes.toString(import_crypto_js4.default.enc.Utf8);
7010
+ }
7011
+ const messagesWithoutPlaceholder = inputMessages.filter(
7012
+ (message) => message.metadata?.type !== "placeholder"
7013
+ );
7014
+ console.log("[EXULU] messages without placeholder", messagesWithoutPlaceholder);
7015
+ let index = 0;
7016
+ let messageHistory = {
7017
+ messages: [],
7018
+ metadata: {
7019
+ tokens: {
7020
+ totalTokens: 0,
7021
+ reasoningTokens: 0,
7022
+ inputTokens: 0,
7023
+ outputTokens: 0,
7024
+ cachedInputTokens: 0
7025
+ },
7026
+ duration: 0
7027
+ }
7028
+ };
7029
+ for (const currentMessage of messagesWithoutPlaceholder) {
7030
+ console.log("[EXULU] running through the conversation");
7031
+ console.log("[EXULU] current index", index);
7032
+ console.log("[EXULU] current message", currentMessage);
7033
+ console.log("[EXULU] message history", messageHistory);
7034
+ const statistics = {
7035
+ label: agentInstance.name,
7036
+ trigger: "agent"
7037
+ };
7038
+ messageHistory = await new Promise(async (resolve, reject) => {
7039
+ const startTime = Date.now();
7040
+ try {
7041
+ const result = await agentBackend.generateStream({
7042
+ contexts,
7043
+ user,
7044
+ instructions: agentInstance.instructions,
7045
+ session: void 0,
7046
+ previousMessages: messageHistory.messages,
7047
+ message: currentMessage,
7048
+ currentTools: enabledTools,
7049
+ allExuluTools: tools,
7050
+ providerapikey,
7051
+ toolConfigs: agentInstance.tools,
7052
+ exuluConfig: config
7053
+ });
7054
+ console.log("[EXULU] consuming stream for agent.");
7055
+ const stream = result.stream.toUIMessageStream({
7056
+ messageMetadata: ({ part }) => {
7057
+ console.log("[EXULU] part", part.type);
7058
+ if (part.type === "finish") {
7059
+ return {
7060
+ totalTokens: part.totalUsage.totalTokens,
7061
+ reasoningTokens: part.totalUsage.reasoningTokens,
7062
+ inputTokens: part.totalUsage.inputTokens,
7063
+ outputTokens: part.totalUsage.outputTokens,
7064
+ cachedInputTokens: part.totalUsage.cachedInputTokens
7065
+ };
7066
+ }
7067
+ },
7068
+ originalMessages: result.originalMessages,
7069
+ sendReasoning: true,
7070
+ sendSources: true,
7071
+ onError: (error) => {
7072
+ console.error("[EXULU] Ui message stream error.", error);
7073
+ reject(error);
7074
+ return `Ui message stream error: ${error instanceof Error ? error.message : String(error)}`;
7075
+ },
7076
+ onFinish: async ({ messages, isContinuation, responseMessage }) => {
7077
+ const metadata = messages[messages.length - 1]?.metadata;
7078
+ console.log("[EXULU] Stream finished with messages:", messages);
7079
+ console.log("[EXULU] Stream metadata", metadata);
7080
+ await Promise.all([
7081
+ updateStatistic({
7082
+ name: "count",
7083
+ label: statistics.label,
7084
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7085
+ trigger: statistics.trigger,
7086
+ count: 1,
7087
+ user: user.id,
7088
+ role: user?.role?.id
7089
+ }),
7090
+ ...metadata?.inputTokens ? [
7091
+ updateStatistic({
7092
+ name: "inputTokens",
7093
+ label: statistics.label,
7094
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7095
+ trigger: statistics.trigger,
7096
+ count: metadata?.inputTokens,
7097
+ user: user.id,
7098
+ role: user?.role?.id
7099
+ })
7100
+ ] : [],
7101
+ ...metadata?.outputTokens ? [
7102
+ updateStatistic({
7103
+ name: "outputTokens",
7104
+ label: statistics.label,
7105
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7106
+ trigger: statistics.trigger,
7107
+ count: metadata?.outputTokens
7108
+ })
7109
+ ] : []
7110
+ ]);
7111
+ resolve({
7112
+ messages,
7113
+ metadata: {
7114
+ tokens: {
7115
+ totalTokens: messageHistory.metadata.tokens.totalTokens + metadata?.totalTokens,
7116
+ reasoningTokens: messageHistory.metadata.tokens.reasoningTokens + metadata?.reasoningTokens,
7117
+ inputTokens: messageHistory.metadata.tokens.inputTokens + metadata?.inputTokens,
7118
+ outputTokens: messageHistory.metadata.tokens.outputTokens + metadata?.outputTokens,
7119
+ cachedInputTokens: messageHistory.metadata.tokens.cachedInputTokens + metadata?.cachedInputTokens
7120
+ },
7121
+ duration: messageHistory.metadata.duration + (Date.now() - startTime)
7122
+ }
7123
+ });
7124
+ }
7125
+ });
7126
+ for await (const message of stream) {
7127
+ console.log("[EXULU] message", message);
7128
+ }
7129
+ } catch (error) {
7130
+ console.error(`[EXULU] error generating stream for agent ${agentInstance.name} (${agentInstance.id}).`, error);
7131
+ reject(error);
7132
+ }
7133
+ });
7134
+ index++;
7135
+ }
7136
+ console.log("[EXULU] finished processing UI messages flow for agent, messages result", messageHistory);
7137
+ return messageHistory;
7138
+ };
7139
+ function getMedian(arr) {
7140
+ if (arr.length === 0) return 0;
7141
+ const sortedArr = arr.slice().sort((a, b) => a - b);
7142
+ const mid = Math.floor(sortedArr.length / 2);
7143
+ if (sortedArr.length % 2 !== 0) {
7144
+ return sortedArr[mid];
7145
+ } else {
7146
+ return (sortedArr[mid - 1] + sortedArr[mid]) / 2;
7147
+ }
7148
+ }
7149
+ function getSum(arr) {
7150
+ if (arr.length === 0) return 0;
7151
+ return arr.reduce((a, b) => a + b, 0);
7152
+ }
7153
+ function getAverage(arr) {
7154
+ if (arr.length === 0) return 0;
7155
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
7156
+ }
6368
7157
 
6369
7158
  // src/mcp/index.ts
6370
7159
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -6517,7 +7306,6 @@ var claudeOpus4Agent = new ExuluAgent2({
6517
7306
  audio: [],
6518
7307
  video: []
6519
7308
  },
6520
- evals: [],
6521
7309
  maxContextLength: 2e5,
6522
7310
  config: {
6523
7311
  name: `CLAUDE-OPUS-4`,
@@ -6552,7 +7340,6 @@ var claudeSonnet4Agent = new ExuluAgent2({
6552
7340
  audio: [],
6553
7341
  video: []
6554
7342
  },
6555
- evals: [],
6556
7343
  maxContextLength: 2e5,
6557
7344
  config: {
6558
7345
  name: `CLAUDE-SONNET-4`,
@@ -6580,7 +7367,6 @@ var claudeSonnet45Agent = new ExuluAgent2({
6580
7367
  audio: [],
6581
7368
  video: []
6582
7369
  },
6583
- evals: [],
6584
7370
  maxContextLength: 2e5,
6585
7371
  config: {
6586
7372
  name: `CLAUDE-SONNET-4.5`,
@@ -6611,7 +7397,6 @@ var gpt5proAgent = new ExuluAgent2({
6611
7397
  audio: [],
6612
7398
  video: []
6613
7399
  },
6614
- evals: [],
6615
7400
  maxContextLength: 4e5,
6616
7401
  config: {
6617
7402
  name: `GPT-5-PRO`,
@@ -6639,7 +7424,6 @@ var gpt5CodexAgent = new ExuluAgent2({
6639
7424
  audio: [],
6640
7425
  video: []
6641
7426
  },
6642
- evals: [],
6643
7427
  maxContextLength: 4e5,
6644
7428
  config: {
6645
7429
  name: `GPT-5-CODEX`,
@@ -6667,7 +7451,6 @@ var gpt5MiniAgent = new ExuluAgent2({
6667
7451
  audio: [],
6668
7452
  video: []
6669
7453
  },
6670
- evals: [],
6671
7454
  maxContextLength: 4e5,
6672
7455
  config: {
6673
7456
  name: `GPT-5-MINI`,
@@ -6702,7 +7485,6 @@ var gpt5agent = new ExuluAgent2({
6702
7485
  audio: [],
6703
7486
  video: []
6704
7487
  },
6705
- evals: [],
6706
7488
  maxContextLength: 4e5,
6707
7489
  config: {
6708
7490
  name: `GPT-5`,
@@ -6737,7 +7519,6 @@ var gpt5NanoAgent = new ExuluAgent2({
6737
7519
  audio: [],
6738
7520
  video: []
6739
7521
  },
6740
- evals: [],
6741
7522
  maxContextLength: 4e5,
6742
7523
  config: {
6743
7524
  name: `GPT-5-NANO`,
@@ -6765,7 +7546,6 @@ var gpt41Agent = new ExuluAgent2({
6765
7546
  audio: [],
6766
7547
  video: []
6767
7548
  },
6768
- evals: [],
6769
7549
  maxContextLength: 1047576,
6770
7550
  config: {
6771
7551
  name: `GPT-4.1`,
@@ -6793,7 +7573,6 @@ var gpt41MiniAgent = new ExuluAgent2({
6793
7573
  audio: [],
6794
7574
  video: []
6795
7575
  },
6796
- evals: [],
6797
7576
  maxContextLength: 1047576,
6798
7577
  config: {
6799
7578
  name: `GPT-4.1-MINI`,
@@ -6821,7 +7600,6 @@ var gpt4oAgent = new ExuluAgent2({
6821
7600
  audio: [],
6822
7601
  video: []
6823
7602
  },
6824
- evals: [],
6825
7603
  maxContextLength: 128e3,
6826
7604
  config: {
6827
7605
  name: `Default agent`,
@@ -6849,7 +7627,6 @@ var gpt4oMiniAgent = new ExuluAgent2({
6849
7627
  audio: [],
6850
7628
  video: []
6851
7629
  },
6852
- evals: [],
6853
7630
  maxContextLength: 128e3,
6854
7631
  config: {
6855
7632
  name: `GPT-4O-MINI`,
@@ -6940,6 +7717,55 @@ var outputsContext = new ExuluContext({
6940
7717
  // src/registry/index.ts
6941
7718
  var import_winston2 = __toESM(require("winston"), 1);
6942
7719
  var import_util = __toESM(require("util"), 1);
7720
+
7721
+ // src/templates/evals/index.ts
7722
+ var import_zod3 = require("zod");
7723
+ var llmAsJudgeEval = new ExuluEval2({
7724
+ id: "llm_as_judge",
7725
+ name: "LLM as Judge",
7726
+ description: "Evaluate the output of the LLM as a judge.",
7727
+ execute: async ({ agent, backend, messages, testCase, config }) => {
7728
+ console.log("[EXULU] running llm as judge eval", { agent, backend, messages, testCase, config });
7729
+ let prompt = config?.prompt;
7730
+ if (!prompt) {
7731
+ console.error("[EXULU] prompt is required.");
7732
+ throw new Error("Prompt is required.");
7733
+ }
7734
+ const lastMessage = messages[messages.length - 1]?.parts?.filter((part) => part.type === "text").map((part) => part.text).join("\n");
7735
+ console.log("[EXULU] last message", lastMessage);
7736
+ if (!lastMessage) {
7737
+ return 0;
7738
+ }
7739
+ prompt = prompt.replace("{actual_output}", lastMessage);
7740
+ prompt = prompt.replace("{expected_output}", testCase.expected_output);
7741
+ if (!agent.providerapikey) {
7742
+ throw new Error(`Provider API key for agent ${agent.name} is required, variable name is ${agent.providerapikey} but it is not set.`);
7743
+ }
7744
+ const providerapikey = await ExuluVariables.get(agent.providerapikey);
7745
+ console.log("[EXULU] prompt", prompt);
7746
+ const response = await backend.generateSync({
7747
+ prompt,
7748
+ outputSchema: import_zod3.z.object({
7749
+ score: import_zod3.z.number().min(0).max(100).describe("The score between 0 and 100.")
7750
+ }),
7751
+ providerapikey
7752
+ });
7753
+ console.log("[EXULU] response", response);
7754
+ const score = parseFloat(response.score);
7755
+ if (isNaN(score)) {
7756
+ throw new Error(`Generated score from llm as a judge eval is not a number: ${response.score}`);
7757
+ }
7758
+ return score;
7759
+ },
7760
+ config: [{
7761
+ name: "prompt",
7762
+ description: "The prompt to send to the LLM as a judge, make sure to instruct the LLM to output a numerical score between 0 and 100. Add {actual_output} to the prompt to replace with the last message content, and {expected_output} to replace with the expected output."
7763
+ }],
7764
+ queue: queues.register("llm_as_judge", 1, 1).use(),
7765
+ llm: true
7766
+ });
7767
+
7768
+ // src/registry/index.ts
6943
7769
  var isDev = process.env.NODE_ENV !== "production";
6944
7770
  var consoleTransport = new import_winston2.default.transports.Console({
6945
7771
  format: isDev ? import_winston2.default.format.combine(
@@ -6950,6 +7776,20 @@ var consoleTransport = new import_winston2.default.transports.Console({
6950
7776
  })
6951
7777
  ) : import_winston2.default.format.json()
6952
7778
  });
7779
+ var formatArg = (arg) => typeof arg === "object" ? import_util.default.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7780
+ var createLogMethod = (logger, logLevel) => {
7781
+ return (...args) => {
7782
+ const lastArg = args[args.length - 1];
7783
+ let metadata = void 0;
7784
+ let messageArgs = args;
7785
+ if (lastArg && typeof lastArg === "object" && lastArg.__logMetadata === true) {
7786
+ metadata = lastArg;
7787
+ messageArgs = args.slice(0, -1);
7788
+ }
7789
+ const message = messageArgs.map(formatArg).join(" ");
7790
+ logger[logLevel](message, metadata);
7791
+ };
7792
+ };
6953
7793
  var isValidPostgresName = (id) => {
6954
7794
  const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6955
7795
  const isValid = regex.test(id);
@@ -6959,6 +7799,7 @@ var isValidPostgresName = (id) => {
6959
7799
  var ExuluApp = class {
6960
7800
  _agents = [];
6961
7801
  _config;
7802
+ _evals = [];
6962
7803
  _queues = [];
6963
7804
  _contexts = {};
6964
7805
  _tools = [];
@@ -6967,7 +7808,11 @@ var ExuluApp = class {
6967
7808
  }
6968
7809
  // Factory function so we can async
6969
7810
  // initialize the MCP server if needed.
6970
- create = async ({ contexts, agents, config, tools }) => {
7811
+ create = async ({ contexts, agents, config, tools, evals }) => {
7812
+ this._evals = [
7813
+ llmAsJudgeEval,
7814
+ ...evals ?? []
7815
+ ];
6971
7816
  this._contexts = {
6972
7817
  ...contexts,
6973
7818
  codeStandardsContext,
@@ -7019,11 +7864,12 @@ var ExuluApp = class {
7019
7864
  console.error(`%c[EXULU] Invalid ID found for a context, tool or agent: ${invalid.map((x) => x.id).join(", ")}. An ID must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or underscores and be a max length of 80 characters and at least 5 characters long.`, "color: orange; font-weight: bold; \n \n");
7020
7865
  throw new Error(`Invalid ID found for a context, tool or agent: ${invalid.map((x) => x.id).join(", ")}. An ID must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or underscores and be a max length of 80 characters and at least 5 characters long.`);
7021
7866
  }
7022
- const contextsArray = Object.values(contexts || {});
7023
7867
  const queueSet = /* @__PURE__ */ new Set();
7024
- for (const context of contextsArray) {
7025
- if (context.embedder?.queue) {
7026
- queueSet.add(await context.embedder.queue);
7868
+ if (redisServer.host?.length && redisServer.port?.length) {
7869
+ queues.register(global_queues.eval_runs, 1, 1);
7870
+ for (const queue of queues.list.values()) {
7871
+ const config2 = await queue.use();
7872
+ queueSet.add(config2);
7027
7873
  }
7028
7874
  }
7029
7875
  this._queues = [...new Set(queueSet.values())];
@@ -7099,7 +7945,7 @@ var ExuluApp = class {
7099
7945
  };
7100
7946
  bullmq = {
7101
7947
  workers: {
7102
- create: async () => {
7948
+ create: async (queues2) => {
7103
7949
  if (!this._config) {
7104
7950
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7105
7951
  }
@@ -7112,16 +7958,22 @@ var ExuluApp = class {
7112
7958
  enableOtel: this._config?.workers?.telemetry?.enabled ?? false,
7113
7959
  transports
7114
7960
  });
7115
- const formatArg = (arg) => typeof arg === "object" ? import_util.default.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7116
- console.log = (...args) => logger.info(args.map(formatArg).join(" "));
7117
- console.info = (...args) => logger.info(args.map(formatArg).join(" "));
7118
- console.warn = (...args) => logger.warn(args.map(formatArg).join(" "));
7119
- console.error = (...args) => logger.error(args.map(formatArg).join(" "));
7120
- console.debug = (...args) => logger.debug(args.map(formatArg).join(" "));
7961
+ console.log = createLogMethod(logger, "info");
7962
+ console.info = createLogMethod(logger, "info");
7963
+ console.warn = createLogMethod(logger, "warn");
7964
+ console.error = createLogMethod(logger, "error");
7965
+ console.debug = createLogMethod(logger, "debug");
7966
+ let filteredQueues = this._queues;
7967
+ if (queues2) {
7968
+ filteredQueues = filteredQueues.filter((q) => queues2.includes(q.queue.name));
7969
+ }
7121
7970
  return await createWorkers(
7122
- this._queues,
7971
+ this._agents,
7972
+ filteredQueues,
7123
7973
  this._config,
7124
7974
  Object.values(this._contexts ?? {}),
7975
+ this._evals,
7976
+ this._tools,
7125
7977
  tracer
7126
7978
  );
7127
7979
  }
@@ -7138,16 +7990,16 @@ var ExuluApp = class {
7138
7990
  if (this._config?.telemetry?.enabled) {
7139
7991
  tracer = import_api4.trace.getTracer("exulu", "1.0.0");
7140
7992
  }
7993
+ const transports = this._config?.logger?.winston?.transports ?? [consoleTransport];
7141
7994
  const logger = logger_default({
7142
7995
  enableOtel: this._config?.telemetry?.enabled ?? false,
7143
- transports: this._config?.logger?.winston?.transports ?? [consoleTransport]
7996
+ transports
7144
7997
  });
7145
- const formatArg = (arg) => typeof arg === "object" ? import_util.default.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7146
- console.log = (...args) => logger.info(args.map(formatArg).join(" "));
7147
- console.info = (...args) => logger.info(args.map(formatArg).join(" "));
7148
- console.warn = (...args) => logger.warn(args.map(formatArg).join(" "));
7149
- console.error = (...args) => logger.error(args.map(formatArg).join(" "));
7150
- console.debug = (...args) => logger.debug(args.map(formatArg).join(" "));
7998
+ console.log = createLogMethod(logger, "info");
7999
+ console.info = createLogMethod(logger, "info");
8000
+ console.warn = createLogMethod(logger, "warn");
8001
+ console.error = createLogMethod(logger, "error");
8002
+ console.debug = createLogMethod(logger, "debug");
7151
8003
  if (!this._config) {
7152
8004
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7153
8005
  }
@@ -7157,7 +8009,9 @@ var ExuluApp = class {
7157
8009
  this._tools,
7158
8010
  Object.values(this._contexts ?? {}),
7159
8011
  this._config,
7160
- tracer
8012
+ this._evals,
8013
+ tracer,
8014
+ this._queues
7161
8015
  );
7162
8016
  if (this._config?.MCP.enabled) {
7163
8017
  const mcp = new ExuluMCP();
@@ -7177,6 +8031,80 @@ var ExuluApp = class {
7177
8031
  };
7178
8032
  };
7179
8033
 
8034
+ // src/bullmq/queues.ts
8035
+ var import_bullmq5 = require("bullmq");
8036
+ var import_bullmq_otel = require("bullmq-otel");
8037
+ var ExuluQueues = class {
8038
+ queues;
8039
+ constructor() {
8040
+ this.queues = [];
8041
+ }
8042
+ list = /* @__PURE__ */ new Map();
8043
+ // list of queue names
8044
+ queue(name) {
8045
+ return this.queues.find((x) => x.queue?.name === name);
8046
+ }
8047
+ // name: string
8048
+ // concurrency: global concurrency for the queue
8049
+ // ratelimit: maximum number of jobs per second
8050
+ // Rate limit is set on workers (see workers.ts), even global rate limits,
8051
+ // that is a bit counter-intuitive. Since queues are registered using .user
8052
+ // method of ExuluQueues we need to store the desired rate limit on the queue
8053
+ // here so we can use the value when creating workers for the queue instance
8054
+ // as there is no way to store a rate limit value natively on a bullm queue.
8055
+ register = (name, concurrency = 1, ratelimit = 1) => {
8056
+ const use = async () => {
8057
+ const existing = this.queues.find((x) => x.queue?.name === name);
8058
+ if (existing) {
8059
+ const globalConcurrency = await existing.queue.getGlobalConcurrency();
8060
+ if (globalConcurrency !== concurrency) {
8061
+ await existing.queue.setGlobalConcurrency(concurrency);
8062
+ }
8063
+ return {
8064
+ queue: existing.queue,
8065
+ ratelimit,
8066
+ concurrency
8067
+ };
8068
+ }
8069
+ if (!redisServer.host?.length || !redisServer.port?.length) {
8070
+ console.error(`[EXULU] no redis server configured, but you are trying to use a queue ( ${name}), likely in an agent or embedder (look for ExuluQueues.register().use() ).`);
8071
+ throw new Error(`[EXULU] no redis server configured.`);
8072
+ }
8073
+ const newQueue = new import_bullmq5.Queue(
8074
+ `${name}`,
8075
+ {
8076
+ connection: {
8077
+ ...redisServer,
8078
+ enableOfflineQueue: false
8079
+ },
8080
+ telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
8081
+ }
8082
+ );
8083
+ await newQueue.setGlobalConcurrency(concurrency);
8084
+ this.queues.push({
8085
+ queue: newQueue,
8086
+ ratelimit,
8087
+ concurrency
8088
+ });
8089
+ return {
8090
+ queue: newQueue,
8091
+ ratelimit,
8092
+ concurrency
8093
+ };
8094
+ };
8095
+ this.list.set(name, {
8096
+ name,
8097
+ concurrency,
8098
+ ratelimit,
8099
+ use
8100
+ });
8101
+ return {
8102
+ use
8103
+ };
8104
+ };
8105
+ };
8106
+ var queues = new ExuluQueues();
8107
+
7180
8108
  // src/chunking/types/base.ts
7181
8109
  var Chunk = class _Chunk {
7182
8110
  /** The text of the chunk. */
@@ -8386,7 +9314,8 @@ var {
8386
9314
  variablesSchema: variablesSchema3,
8387
9315
  workflowTemplatesSchema: workflowTemplatesSchema3,
8388
9316
  rbacSchema: rbacSchema3,
8389
- projectsSchema: projectsSchema3
9317
+ projectsSchema: projectsSchema3,
9318
+ jobResultsSchema: jobResultsSchema3
8390
9319
  } = coreSchemas.get();
8391
9320
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
8392
9321
  for (const field of fields) {
@@ -8420,6 +9349,7 @@ var up = async function(knex) {
8420
9349
  platformConfigurationsSchema3(),
8421
9350
  statisticsSchema3(),
8422
9351
  projectsSchema3(),
9352
+ jobResultsSchema3(),
8423
9353
  rbacSchema3(),
8424
9354
  agentsSchema3(),
8425
9355
  variablesSchema3(),
@@ -8604,20 +9534,7 @@ var create = ({
8604
9534
  };
8605
9535
 
8606
9536
  // src/index.ts
8607
- var import_crypto_js4 = __toESM(require("crypto-js"), 1);
8608
-
8609
- // types/enums/jobs.ts
8610
- var JOB_STATUS_ENUM = {
8611
- completed: "completed",
8612
- failed: "failed",
8613
- delayed: "delayed",
8614
- active: "active",
8615
- waiting: "waiting",
8616
- paused: "paused",
8617
- stuck: "stuck"
8618
- };
8619
-
8620
- // src/index.ts
9537
+ var import_crypto_js5 = __toESM(require("crypto-js"), 1);
8621
9538
  var ExuluJobs = {
8622
9539
  redis: redisClient,
8623
9540
  jobs: {
@@ -8654,8 +9571,8 @@ var ExuluVariables = {
8654
9571
  throw new Error(`Variable ${name} not found.`);
8655
9572
  }
8656
9573
  if (variable.encrypted) {
8657
- const bytes = import_crypto_js4.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
8658
- variable.value = bytes.toString(import_crypto_js4.default.enc.Utf8);
9574
+ const bytes = import_crypto_js5.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
9575
+ variable.value = bytes.toString(import_crypto_js5.default.enc.Utf8);
8659
9576
  }
8660
9577
  return variable.value;
8661
9578
  }
@@ -8791,5 +9708,6 @@ var ExuluChunkers = {
8791
9708
  ExuluTool,
8792
9709
  ExuluUtils,
8793
9710
  ExuluVariables,
8794
- db
9711
+ db,
9712
+ logMetadata
8795
9713
  });