@exulu/backend 1.27.2 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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, queues3) {
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");
@@ -3675,6 +4088,7 @@ var addPrefixToKey = (keyPath, config) => {
3675
4088
  return `${prefix}/${keyPath}`;
3676
4089
  };
3677
4090
  var uploadFile = async (user, file, key, config, options = {}) => {
4091
+ console.log("[EXULU] Uploading file to S3", key);
3678
4092
  const client2 = getS3Client(config);
3679
4093
  let folder = `${user}/`;
3680
4094
  const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
@@ -4288,27 +4702,34 @@ function errorHandler(error) {
4288
4702
  }
4289
4703
  return JSON.stringify(error);
4290
4704
  }
4291
- var ExuluEval = class {
4705
+ var ExuluEval2 = class {
4292
4706
  id;
4293
4707
  name;
4294
4708
  description;
4709
+ llm;
4295
4710
  execute;
4296
4711
  config;
4297
4712
  queue;
4298
- constructor({ id, name, description, execute: execute2, config, queue }) {
4713
+ constructor({ id, name, description, execute: execute2, config, queue, llm }) {
4299
4714
  this.id = id;
4300
4715
  this.name = name;
4301
4716
  this.description = description;
4302
4717
  this.execute = execute2;
4303
4718
  this.config = config;
4719
+ this.llm = llm;
4304
4720
  this.queue = queue;
4305
4721
  }
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}`);
4722
+ async run(agent, backend, testCase, messages, config) {
4723
+ try {
4724
+ const score = await this.execute({ agent, backend, testCase, messages, config });
4725
+ if (score < 0 || score > 100) {
4726
+ throw new Error(`Eval function ${this.name} must return a score between 0 and 100, got ${score}`);
4727
+ }
4728
+ return score;
4729
+ } catch (error) {
4730
+ console.error(`[EXULU] error running eval function ${this.name}.`, error);
4731
+ throw new Error(`Error running eval function ${this.name}: ${error instanceof Error ? error.message : String(error)}`);
4310
4732
  }
4311
- return score;
4312
4733
  }
4313
4734
  };
4314
4735
  var ExuluAgent2 = class {
@@ -4323,13 +4744,13 @@ var ExuluAgent2 = class {
4323
4744
  type;
4324
4745
  streaming = false;
4325
4746
  maxContextLength;
4747
+ queue;
4326
4748
  rateLimit;
4327
4749
  config;
4328
- evals;
4329
4750
  // private memory: Memory | undefined; // TODO do own implementation
4330
4751
  model;
4331
4752
  capabilities;
4332
- constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, evals, provider }) {
4753
+ constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, provider, queue }) {
4333
4754
  this.id = id;
4334
4755
  this.name = name;
4335
4756
  this.description = description;
@@ -4338,7 +4759,7 @@ var ExuluAgent2 = class {
4338
4759
  this.config = config;
4339
4760
  this.type = type;
4340
4761
  this.maxContextLength = maxContextLength;
4341
- this.evals = evals;
4762
+ this.queue = queue;
4342
4763
  this.capabilities = capabilities || {
4343
4764
  text: false,
4344
4765
  images: [],
@@ -4437,7 +4858,7 @@ var ExuluAgent2 = class {
4437
4858
  prompt,
4438
4859
  user,
4439
4860
  session,
4440
- message,
4861
+ inputMessages,
4441
4862
  currentTools,
4442
4863
  allExuluTools,
4443
4864
  statistics,
@@ -4455,10 +4876,10 @@ var ExuluAgent2 = class {
4455
4876
  if (!this.config) {
4456
4877
  throw new Error("Config is required for generating.");
4457
4878
  }
4458
- if (prompt && message) {
4879
+ if (prompt && inputMessages?.length) {
4459
4880
  throw new Error("Message and prompt cannot be provided at the same time.");
4460
4881
  }
4461
- if (!prompt && !message) {
4882
+ if (!prompt && !inputMessages?.length) {
4462
4883
  throw new Error("Prompt or message is required for generating.");
4463
4884
  }
4464
4885
  if (outputSchema && !prompt) {
@@ -4468,18 +4889,18 @@ var ExuluAgent2 = class {
4468
4889
  apiKey: providerapikey
4469
4890
  });
4470
4891
  console.log("[EXULU] Model for agent: " + this.name, " created for generating sync.");
4471
- let messages = [];
4472
- if (message && session && user) {
4892
+ let messages = inputMessages || [];
4893
+ if (messages && session && user) {
4473
4894
  const previousMessages = await getAgentMessages({
4474
4895
  session,
4475
4896
  user: user.id,
4476
4897
  limit: 50,
4477
4898
  page: 1
4478
4899
  });
4479
- const previousMessagesContent = previousMessages.map((message2) => JSON.parse(message2.content));
4900
+ const previousMessagesContent = previousMessages.map((message) => JSON.parse(message.content));
4480
4901
  messages = await (0, import_ai.validateUIMessages)({
4481
4902
  // append the new message to the previous messages:
4482
- messages: [...previousMessagesContent, message]
4903
+ messages: [...previousMessagesContent, ...messages]
4483
4904
  });
4484
4905
  }
4485
4906
  console.log("[EXULU] Message count for agent: " + this.name, "loaded for generating sync.", messages.length);
@@ -4615,13 +5036,12 @@ var ExuluAgent2 = class {
4615
5036
  return "";
4616
5037
  };
4617
5038
  generateStream = async ({
4618
- express: express3,
4619
5039
  user,
4620
5040
  session,
4621
5041
  message,
5042
+ previousMessages,
4622
5043
  currentTools,
4623
5044
  allExuluTools,
4624
- statistics,
4625
5045
  toolConfigs,
4626
5046
  providerapikey,
4627
5047
  contexts,
@@ -4629,27 +5049,34 @@ var ExuluAgent2 = class {
4629
5049
  instructions
4630
5050
  }) => {
4631
5051
  if (!this.model) {
5052
+ console.error("[EXULU] Model is required for streaming.");
4632
5053
  throw new Error("Model is required for streaming.");
4633
5054
  }
4634
5055
  if (!this.config) {
5056
+ console.error("[EXULU] Config is required for streaming.");
4635
5057
  throw new Error("Config is required for generating.");
4636
5058
  }
4637
5059
  if (!message) {
5060
+ console.error("[EXULU] Message is required for streaming.");
4638
5061
  throw new Error("Message is required for streaming.");
4639
5062
  }
4640
5063
  const model = this.model.create({
4641
5064
  apiKey: providerapikey
4642
5065
  });
4643
5066
  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
- );
5067
+ let previousMessagesContent = previousMessages || [];
5068
+ if (session) {
5069
+ console.log("[EXULU] loading previous messages from session: " + session);
5070
+ const previousMessages2 = await getAgentMessages({
5071
+ session,
5072
+ user: user.id,
5073
+ limit: 50,
5074
+ page: 1
5075
+ });
5076
+ previousMessagesContent = previousMessages2.map(
5077
+ (message2) => JSON.parse(message2.content)
5078
+ );
5079
+ }
4653
5080
  messages = await (0, import_ai.validateUIMessages)({
4654
5081
  // append the new message to the previous messages:
4655
5082
  messages: [...previousMessagesContent, message]
@@ -4682,80 +5109,17 @@ var ExuluAgent2 = class {
4682
5109
  user,
4683
5110
  exuluConfig
4684
5111
  ),
4685
- onError: (error) => console.error("[EXULU] chat stream error.", error)
4686
- // stopWhen: [stepCountIs(1)],
4687
- });
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
- },
4701
- originalMessages: messages,
4702
- sendReasoning: true,
4703
- sendSources: true,
4704
5112
  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
- }
5113
+ console.error("[EXULU] chat stream error.", error);
5114
+ throw new Error(`Chat stream error: ${error instanceof Error ? error.message : String(error)}`);
4756
5115
  }
5116
+ // stopWhen: [stepCountIs(1)],
4757
5117
  });
4758
- return;
5118
+ return {
5119
+ stream: result,
5120
+ originalMessages: messages,
5121
+ previousMessages: previousMessagesContent
5122
+ };
4759
5123
  };
4760
5124
  };
4761
5125
  var getAgentMessages = async ({ session, user, limit, page }) => {
@@ -4946,16 +5310,21 @@ var ExuluContext = class {
4946
5310
  if (queue?.queue.name) {
4947
5311
  console.log("[EXULU] processor is in queue mode, scheduling job.");
4948
5312
  const job = await bullmqDecorator({
5313
+ timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 180,
4949
5314
  label: `${this.name} ${field.name} data processor`,
4950
5315
  processor: `${this.id}-${field.name}`,
4951
5316
  context: this.id,
4952
5317
  inputs: item,
4953
5318
  item: item.id,
4954
5319
  queue: queue.queue,
5320
+ backoff: queue.backoff || {
5321
+ type: "exponential",
5322
+ delay: 2e3
5323
+ },
5324
+ retries: queue.retries || 2,
4955
5325
  user,
4956
5326
  role,
4957
- trigger,
4958
- retries: 2
5327
+ trigger
4959
5328
  });
4960
5329
  return {
4961
5330
  result: "",
@@ -5025,12 +5394,14 @@ var ExuluContext = class {
5025
5394
  role
5026
5395
  );
5027
5396
  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
- })));
5397
+ if (chunks?.length) {
5398
+ await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
5399
+ source,
5400
+ content: chunk.content,
5401
+ chunk_index: chunk.index,
5402
+ embedding: import_knex5.default.toSql(chunk.vector)
5403
+ })));
5404
+ }
5034
5405
  await db3.from(getTableName(this.id)).where({ id: item.id }).update({
5035
5406
  embeddings_updated_at: (/* @__PURE__ */ new Date()).toISOString()
5036
5407
  }).returning("id");
@@ -5159,9 +5530,15 @@ var ExuluContext = class {
5159
5530
  if (queue?.queue.name) {
5160
5531
  console.log("[EXULU] embedder is in queue mode, scheduling job.");
5161
5532
  const job = await bullmqDecorator({
5533
+ timeoutInSeconds: queue.timeoutInSeconds || 180,
5162
5534
  label: `${this.embedder.name}`,
5163
5535
  embedder: this.embedder.id,
5164
5536
  context: this.id,
5537
+ backoff: queue.backoff || {
5538
+ type: "exponential",
5539
+ delay: 2e3
5540
+ },
5541
+ retries: queue.retries || 2,
5165
5542
  inputs: item,
5166
5543
  item: item.id,
5167
5544
  queue: queue.queue,
@@ -5413,6 +5790,8 @@ var ExuluQueues = class {
5413
5790
  constructor() {
5414
5791
  this.queues = [];
5415
5792
  }
5793
+ list = /* @__PURE__ */ new Map();
5794
+ // list of queue names
5416
5795
  queue(name) {
5417
5796
  return this.queues.find((x) => x.queue?.name === name);
5418
5797
  }
@@ -5424,40 +5803,54 @@ var ExuluQueues = class {
5424
5803
  // method of ExuluQueues we need to store the desired rate limit on the queue
5425
5804
  // here so we can use the value when creating workers for the queue instance
5426
5805
  // 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);
5806
+ register = (name, concurrency = 1, ratelimit = 1) => {
5807
+ const use = async () => {
5808
+ const existing = this.queues.find((x) => x.queue?.name === name);
5809
+ if (existing) {
5810
+ const globalConcurrency = await existing.queue.getGlobalConcurrency();
5811
+ if (globalConcurrency !== concurrency) {
5812
+ await existing.queue.setGlobalConcurrency(concurrency);
5813
+ }
5814
+ return {
5815
+ queue: existing.queue,
5816
+ ratelimit,
5817
+ concurrency
5818
+ };
5433
5819
  }
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")
5820
+ if (!redisServer.host?.length || !redisServer.port?.length) {
5821
+ 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() ).`);
5822
+ throw new Error(`[EXULU] no redis server configured.`);
5449
5823
  }
5450
- );
5451
- await newQueue.setGlobalConcurrency(concurrency);
5452
- this.queues.push({
5453
- queue: newQueue,
5824
+ const newQueue = new import_bullmq4.Queue(
5825
+ `${name}`,
5826
+ {
5827
+ connection: {
5828
+ ...redisServer,
5829
+ enableOfflineQueue: false
5830
+ },
5831
+ telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
5832
+ }
5833
+ );
5834
+ await newQueue.setGlobalConcurrency(concurrency);
5835
+ this.queues.push({
5836
+ queue: newQueue,
5837
+ ratelimit,
5838
+ concurrency
5839
+ });
5840
+ return {
5841
+ queue: newQueue,
5842
+ ratelimit,
5843
+ concurrency
5844
+ };
5845
+ };
5846
+ this.list.set(name, {
5847
+ name,
5848
+ concurrency,
5454
5849
  ratelimit,
5455
- concurrency
5850
+ use
5456
5851
  });
5457
5852
  return {
5458
- queue: newQueue,
5459
- ratelimit,
5460
- concurrency
5853
+ use
5461
5854
  };
5462
5855
  };
5463
5856
  };
@@ -5476,8 +5869,6 @@ var import_openai = __toESM(require("openai"), 1);
5476
5869
  var import_fs = __toESM(require("fs"), 1);
5477
5870
  var import_node_crypto3 = require("crypto");
5478
5871
  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
5872
  var import_sdk = __toESM(require("@anthropic-ai/sdk"), 1);
5482
5873
 
5483
5874
  // src/registry/utils/claude-messages.ts
@@ -5503,13 +5894,15 @@ var CLAUDE_MESSAGES = {
5503
5894
  };
5504
5895
 
5505
5896
  // src/registry/routes.ts
5897
+ var import_ai2 = require("ai");
5506
5898
  var REQUEST_SIZE_LIMIT = "50mb";
5507
5899
  var global_queues = {
5508
- logs_cleaner: "logs-cleaner"
5900
+ eval_runs: "eval_runs"
5509
5901
  };
5510
5902
  var {
5511
5903
  agentsSchema: agentsSchema2,
5512
5904
  projectsSchema: projectsSchema2,
5905
+ jobResultsSchema: jobResultsSchema2,
5513
5906
  testCasesSchema: testCasesSchema2,
5514
5907
  evalSetsSchema: evalSetsSchema2,
5515
5908
  evalRunsSchema: evalRunsSchema2,
@@ -5523,38 +5916,7 @@ var {
5523
5916
  rbacSchema: rbacSchema2,
5524
5917
  statisticsSchema: statisticsSchema2
5525
5918
  } = 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) => {
5919
+ var createExpressRoutes = async (app, agents, tools, contexts, config, evals, tracer, queues3) => {
5558
5920
  var corsOptions = {
5559
5921
  origin: "*",
5560
5922
  exposedHeaders: "*",
@@ -5573,19 +5935,15 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5573
5935
  \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
5936
  \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
5937
  \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
5938
+ Intelligence Management Platform - Server
5577
5939
 
5578
5940
  `);
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
5941
  const schema = createSDL([
5585
5942
  usersSchema2(),
5586
5943
  rolesSchema2(),
5587
5944
  agentsSchema2(),
5588
5945
  projectsSchema2(),
5946
+ jobResultsSchema2(),
5589
5947
  evalRunsSchema2(),
5590
5948
  platformConfigurationsSchema2(),
5591
5949
  evalSetsSchema2(),
@@ -5596,7 +5954,7 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5596
5954
  workflowTemplatesSchema2(),
5597
5955
  statisticsSchema2(),
5598
5956
  rbacSchema2()
5599
- ], contexts ?? [], agents, tools, config);
5957
+ ], contexts ?? [], agents, tools, config, evals, queues3 || []);
5600
5958
  const server = new import_server3.ApolloServer({
5601
5959
  cache: new import_utils3.InMemoryLRUCache(),
5602
5960
  schema,
@@ -5727,103 +6085,6 @@ Mood: friendly and intelligent.
5727
6085
  image: `${process.env.BACKEND}/${uuid}.png`
5728
6086
  });
5729
6087
  });
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
6088
  app.get("/ping", async (req, res) => {
5828
6089
  const authenticationResult = await requestValidators.authenticate(req);
5829
6090
  if (!authenticationResult.user?.id) {
@@ -5952,11 +6213,11 @@ Mood: friendly and intelligent.
5952
6213
  providerapikey = bytes.toString(import_crypto_js3.default.enc.Utf8);
5953
6214
  }
5954
6215
  if (!!headers.stream) {
5955
- await agent.generateStream({
5956
- express: {
5957
- res,
5958
- req
5959
- },
6216
+ const statistics = {
6217
+ label: agent.name,
6218
+ trigger: "agent"
6219
+ };
6220
+ const result = await agent.generateStream({
5960
6221
  contexts,
5961
6222
  user,
5962
6223
  instructions: agentInstance.instructions,
@@ -5966,10 +6227,79 @@ Mood: friendly and intelligent.
5966
6227
  allExuluTools: tools,
5967
6228
  providerapikey,
5968
6229
  toolConfigs: agentInstance.tools,
5969
- exuluConfig: config,
5970
- statistics: {
5971
- label: agent.name,
5972
- trigger: "agent"
6230
+ exuluConfig: config
6231
+ });
6232
+ result.stream.consumeStream();
6233
+ result.stream.pipeUIMessageStreamToResponse(res, {
6234
+ messageMetadata: ({ part }) => {
6235
+ if (part.type === "finish") {
6236
+ return {
6237
+ totalTokens: part.totalUsage.totalTokens,
6238
+ reasoningTokens: part.totalUsage.reasoningTokens,
6239
+ inputTokens: part.totalUsage.inputTokens,
6240
+ outputTokens: part.totalUsage.outputTokens,
6241
+ cachedInputTokens: part.totalUsage.cachedInputTokens
6242
+ };
6243
+ }
6244
+ },
6245
+ originalMessages: result.originalMessages,
6246
+ sendReasoning: true,
6247
+ sendSources: true,
6248
+ onError: (error) => {
6249
+ console.error("[EXULU] chat response error.", error);
6250
+ return errorHandler(error);
6251
+ },
6252
+ generateMessageId: (0, import_ai2.createIdGenerator)({
6253
+ prefix: "msg_",
6254
+ size: 16
6255
+ }),
6256
+ onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
6257
+ if (headers.session) {
6258
+ await saveChat({
6259
+ session: headers.session,
6260
+ user: user.id,
6261
+ messages: messages.filter((x) => !result.previousMessages.find((y) => y.id === x.id))
6262
+ });
6263
+ }
6264
+ const metadata = messages[messages.length - 1]?.metadata;
6265
+ console.log("[EXULU] Finished streaming", metadata);
6266
+ console.log("[EXULU] Statistics", {
6267
+ label: agent.name,
6268
+ trigger: "agent"
6269
+ });
6270
+ if (statistics) {
6271
+ await Promise.all([
6272
+ updateStatistic({
6273
+ name: "count",
6274
+ label: statistics.label,
6275
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6276
+ trigger: statistics.trigger,
6277
+ count: 1,
6278
+ user: user.id,
6279
+ role: user?.role?.id
6280
+ }),
6281
+ ...metadata?.inputTokens ? [
6282
+ updateStatistic({
6283
+ name: "inputTokens",
6284
+ label: statistics.label,
6285
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6286
+ trigger: statistics.trigger,
6287
+ count: metadata?.inputTokens,
6288
+ user: user.id,
6289
+ role: user?.role?.id
6290
+ })
6291
+ ] : [],
6292
+ ...metadata?.outputTokens ? [
6293
+ updateStatistic({
6294
+ name: "outputTokens",
6295
+ label: statistics.label,
6296
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6297
+ trigger: statistics.trigger,
6298
+ count: metadata?.outputTokens
6299
+ })
6300
+ ] : []
6301
+ ]);
6302
+ }
5973
6303
  }
5974
6304
  });
5975
6305
  return;
@@ -5978,7 +6308,7 @@ Mood: friendly and intelligent.
5978
6308
  user,
5979
6309
  instructions: agentInstance.instructions,
5980
6310
  session: headers.session,
5981
- message: req.body.message,
6311
+ inputMessages: [req.body.message],
5982
6312
  contexts,
5983
6313
  currentTools: enabledTools,
5984
6314
  allExuluTools: tools,
@@ -6000,91 +6330,6 @@ Mood: friendly and intelligent.
6000
6330
  } else {
6001
6331
  console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
6002
6332
  }
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
6333
  app.get("/config", async (req, res) => {
6089
6334
  res.status(200).json({
6090
6335
  message: "Config fetched successfully.",
@@ -6258,8 +6503,34 @@ var createCustomAnthropicStreamingMessage = (message) => {
6258
6503
  var import_ioredis = __toESM(require("ioredis"), 1);
6259
6504
  var import_bullmq5 = require("bullmq");
6260
6505
  var import_api2 = require("@opentelemetry/api");
6506
+ var import_uuid3 = require("uuid");
6507
+ var import_ai3 = require("ai");
6508
+ var import_crypto_js4 = __toESM(require("crypto-js"), 1);
6509
+
6510
+ // src/registry/log-metadata.ts
6511
+ function logMetadata(id, additionalMetadata) {
6512
+ return {
6513
+ __logMetadata: true,
6514
+ id,
6515
+ ...additionalMetadata
6516
+ };
6517
+ }
6518
+
6519
+ // src/registry/workers.ts
6261
6520
  var redisConnection;
6262
- var createWorkers = async (queues2, config, contexts, tracer) => {
6521
+ var createWorkers = async (agents, queues3, config, contexts, evals, tools, tracer) => {
6522
+ console.log(`
6523
+ \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
6524
+ \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
6525
+ \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
6526
+ \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
6527
+ \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
6528
+ \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
6529
+ Intelligence Management Platform - Workers
6530
+
6531
+ `);
6532
+ console.log("[EXULU] creating workers for " + queues3?.length + " queues.");
6533
+ console.log("[EXULU] queues", queues3.map((q) => q.queue.name));
6263
6534
  if (!redisServer.host || !redisServer.port) {
6264
6535
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
6265
6536
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -6267,80 +6538,333 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6267
6538
  if (!redisConnection) {
6268
6539
  redisConnection = new import_ioredis.default({
6269
6540
  ...redisServer,
6541
+ enableOfflineQueue: true,
6542
+ retryStrategy: function(times) {
6543
+ return Math.max(Math.min(Math.exp(times), 2e4), 1e3);
6544
+ },
6270
6545
  maxRetriesPerRequest: null
6271
6546
  });
6272
6547
  }
6273
- const workers = queues2.map((queue) => {
6274
- console.log(`[EXULU] creating worker for queue ${queue}.`);
6548
+ const workers = queues3.map((queue) => {
6549
+ console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
6275
6550
  const worker = new import_bullmq5.Worker(
6276
- `${queue}`,
6551
+ `${queue.queue.name}`,
6277
6552
  async (bullmqJob) => {
6553
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
6554
+ name: bullmqJob.name,
6555
+ status: await bullmqJob.getState(),
6556
+ type: bullmqJob.data.type
6557
+ }));
6278
6558
  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}.`);
6559
+ const data = bullmqJob.data;
6560
+ const timeoutMs = data.timeoutInSeconds * 1e3;
6561
+ const timeoutPromise = new Promise((_, reject) => {
6562
+ setTimeout(() => {
6563
+ reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${data.timeoutInSeconds}s`));
6564
+ }, timeoutMs);
6565
+ });
6566
+ const workPromise = (async () => {
6567
+ try {
6568
+ bullmq.validate(bullmqJob.id, data);
6569
+ if (data.type === "embedder") {
6570
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
6571
+ const label = `embedder-${bullmqJob.name}`;
6572
+ await db3.from("job_results").insert({
6573
+ job_id: bullmqJob.id,
6574
+ label,
6575
+ state: await bullmqJob.getState(),
6576
+ result: null,
6577
+ metadata: {}
6578
+ });
6579
+ const context = contexts.find((context2) => context2.id === data.context);
6580
+ if (!context) {
6581
+ throw new Error(`Context ${data.context} not found in the registry.`);
6582
+ }
6583
+ if (!data.embedder) {
6584
+ throw new Error(`No embedder set for embedder job.`);
6585
+ }
6586
+ const embedder = contexts.find((context2) => context2.embedder?.id === data.embedder);
6587
+ if (!embedder) {
6588
+ throw new Error(`Embedder ${data.embedder} not found in the registry.`);
6589
+ }
6590
+ const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
6591
+ label: embedder.name,
6592
+ trigger: data.trigger
6593
+ }, data.role, bullmqJob.id);
6594
+ return {
6595
+ result,
6596
+ metadata: {}
6597
+ };
6311
6598
  }
6312
- const exuluStorage = new ExuluStorage({ config });
6313
- if (!data.user) {
6314
- throw new Error(`User not set for processor job.`);
6599
+ if (data.type === "processor") {
6600
+ console.log("[EXULU] running a processor job.", logMetadata(bullmqJob.name));
6601
+ const label = `processor-${bullmqJob.name}`;
6602
+ await db3.from("job_results").insert({
6603
+ job_id: bullmqJob.id,
6604
+ label,
6605
+ state: await bullmqJob.getState(),
6606
+ result: null,
6607
+ metadata: {}
6608
+ });
6609
+ const context = contexts.find((context2) => context2.id === data.context);
6610
+ if (!context) {
6611
+ throw new Error(`Context ${data.context} not found in the registry.`);
6612
+ }
6613
+ const field = context.fields.find((field2) => field2.name === data.inputs.field);
6614
+ if (!field) {
6615
+ throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
6616
+ }
6617
+ if (!field.processor) {
6618
+ throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
6619
+ }
6620
+ const exuluStorage = new ExuluStorage({ config });
6621
+ if (!data.user) {
6622
+ throw new Error(`User not set for processor job.`);
6623
+ }
6624
+ if (!data.role) {
6625
+ throw new Error(`Role not set for processor job.`);
6626
+ }
6627
+ const result = await field.processor.execute({
6628
+ item: data.inputs,
6629
+ user: data.user,
6630
+ role: data.role,
6631
+ utils: {
6632
+ storage: exuluStorage,
6633
+ items: {
6634
+ update: context.updateItem,
6635
+ create: context.createItem,
6636
+ delete: context.deleteItem
6637
+ }
6638
+ },
6639
+ config
6640
+ });
6641
+ return {
6642
+ result,
6643
+ metadata: {}
6644
+ };
6315
6645
  }
6316
- if (!data.role) {
6317
- throw new Error(`Role not set for processor job.`);
6646
+ if (data.type === "eval_run") {
6647
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
6648
+ const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
6649
+ const existingResult = await db3.from("job_results").where({ label }).first();
6650
+ if (existingResult) {
6651
+ await db3.from("job_results").where({ label }).update({
6652
+ job_id: bullmqJob.id,
6653
+ label,
6654
+ state: await bullmqJob.getState(),
6655
+ result: null,
6656
+ metadata: {},
6657
+ tries: existingResult.tries + 1
6658
+ });
6659
+ } else {
6660
+ await db3.from("job_results").insert({
6661
+ job_id: bullmqJob.id,
6662
+ label,
6663
+ state: await bullmqJob.getState(),
6664
+ result: null,
6665
+ metadata: {},
6666
+ tries: 1
6667
+ });
6668
+ }
6669
+ const {
6670
+ agentInstance,
6671
+ backend: agentBackend,
6672
+ user,
6673
+ evalRun,
6674
+ testCase,
6675
+ messages: inputMessages
6676
+ } = await validateEvalPayload(data, agents);
6677
+ const retries = 3;
6678
+ let attempts = 0;
6679
+ const promise = new Promise(async (resolve, reject) => {
6680
+ while (attempts < retries) {
6681
+ try {
6682
+ const messages2 = await processUiMessagesFlow({
6683
+ agents,
6684
+ agentInstance,
6685
+ agentBackend,
6686
+ inputMessages,
6687
+ contexts,
6688
+ user,
6689
+ tools,
6690
+ config
6691
+ });
6692
+ resolve(messages2);
6693
+ break;
6694
+ } catch (error) {
6695
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
6696
+ error: error instanceof Error ? error.message : String(error)
6697
+ }));
6698
+ attempts++;
6699
+ if (attempts >= retries) {
6700
+ reject(error);
6701
+ }
6702
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
6703
+ }
6704
+ }
6705
+ });
6706
+ const result = await promise;
6707
+ const messages = result.messages;
6708
+ const metadata = result.metadata;
6709
+ const evalFunctions = evalRun.eval_functions;
6710
+ let evalFunctionResults = [];
6711
+ for (const evalFunction of evalFunctions) {
6712
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6713
+ if (!evalMethod) {
6714
+ throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
6715
+ }
6716
+ let result2;
6717
+ if (evalMethod.queue) {
6718
+ const queue2 = await evalMethod.queue;
6719
+ const jobData = {
6720
+ ...data,
6721
+ type: "eval_function",
6722
+ eval_functions: [{
6723
+ id: evalFunction.id,
6724
+ config: evalFunction.config || {}
6725
+ }],
6726
+ // updating the input messages with the messages we want to run the eval
6727
+ // function on, which are the output messages from the agent.
6728
+ inputs: messages
6729
+ };
6730
+ const redisId = (0, import_uuid3.v4)();
6731
+ const job = await queue2.queue.add("eval_function", jobData, {
6732
+ jobId: redisId,
6733
+ // Setting it to 3 as a sensible default, as
6734
+ // many AI services are quite unstable.
6735
+ attempts: queue2.retries || 3,
6736
+ // todo make this configurable?
6737
+ removeOnComplete: 5e3,
6738
+ removeOnFail: 1e4,
6739
+ backoff: queue2.backoff || {
6740
+ type: "exponential",
6741
+ delay: 2e3
6742
+ }
6743
+ });
6744
+ if (!job.id) {
6745
+ throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
6746
+ }
6747
+ result2 = await pollJobResult({ queue: queue2, jobId: job.id });
6748
+ const evalFunctionResult = {
6749
+ test_case_id: testCase.id,
6750
+ eval_run_id: evalRun.id,
6751
+ eval_function_id: evalFunction.id,
6752
+ result: result2 || 0
6753
+ };
6754
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6755
+ result: result2 || 0
6756
+ }));
6757
+ evalFunctionResults.push(evalFunctionResult);
6758
+ } else {
6759
+ result2 = await evalMethod.run(
6760
+ agentInstance,
6761
+ agentBackend,
6762
+ testCase,
6763
+ messages,
6764
+ evalFunction.config || {}
6765
+ );
6766
+ const evalFunctionResult = {
6767
+ test_case_id: testCase.id,
6768
+ eval_run_id: evalRun.id,
6769
+ eval_function_id: evalFunction.id,
6770
+ result: result2 || 0
6771
+ };
6772
+ evalFunctionResults.push(evalFunctionResult);
6773
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6774
+ result: result2 || 0
6775
+ }));
6776
+ }
6777
+ }
6778
+ const scores = evalFunctionResults.map((result2) => result2.result);
6779
+ let score = 0;
6780
+ switch (data.scoring_method) {
6781
+ case "median":
6782
+ score = getMedian(scores);
6783
+ break;
6784
+ case "average":
6785
+ score = getAverage(scores);
6786
+ break;
6787
+ case "sum":
6788
+ score = getSum(scores);
6789
+ break;
6790
+ }
6791
+ return {
6792
+ result: score,
6793
+ metadata: {
6794
+ ...evalFunctionResults,
6795
+ ...metadata
6796
+ }
6797
+ };
6318
6798
  }
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
6799
+ if (data.type === "eval_function") {
6800
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
6801
+ if (data.eval_functions?.length !== 1) {
6802
+ throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
6803
+ }
6804
+ const label = `eval-function-${data.eval_run_id}-${data.test_case_id}-${data.eval_functions?.[0]?.id}`;
6805
+ const existingResult = await db3.from("job_results").where({ label }).first();
6806
+ if (existingResult) {
6807
+ await db3.from("job_results").where({ label }).update({
6808
+ job_id: bullmqJob.id,
6809
+ label,
6810
+ state: await bullmqJob.getState(),
6811
+ result: null,
6812
+ metadata: {},
6813
+ tries: existingResult.tries + 1
6814
+ });
6815
+ } else {
6816
+ await db3.from("job_results").insert({
6817
+ job_id: bullmqJob.id,
6818
+ label,
6819
+ state: await bullmqJob.getState(),
6820
+ result: null,
6821
+ metadata: {},
6822
+ tries: 1
6823
+ });
6824
+ }
6825
+ const {
6826
+ evalRun,
6827
+ agentInstance,
6828
+ backend,
6829
+ testCase,
6830
+ messages: inputMessages
6831
+ } = await validateEvalPayload(data, agents);
6832
+ const evalFunctions = evalRun.eval_functions;
6833
+ let result;
6834
+ for (const evalFunction of evalFunctions) {
6835
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6836
+ if (!evalMethod) {
6837
+ 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
6838
  }
6328
- },
6329
- config
6330
- });
6331
- return result;
6839
+ result = await evalMethod.run(
6840
+ agentInstance,
6841
+ backend,
6842
+ testCase,
6843
+ inputMessages,
6844
+ evalFunction.config || {}
6845
+ );
6846
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
6847
+ result: result || 0
6848
+ }));
6849
+ }
6850
+ return {
6851
+ result,
6852
+ metadata: {}
6853
+ };
6854
+ }
6855
+ throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
6856
+ } catch (error) {
6857
+ console.error(`[EXULU] job failed.`, error instanceof Error ? error.message : String(error));
6858
+ throw error;
6332
6859
  }
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
- }
6860
+ })();
6861
+ return Promise.race([workPromise, timeoutPromise]);
6341
6862
  },
6342
6863
  {
6864
+ autorun: true,
6343
6865
  connection: redisConnection,
6866
+ removeOnComplete: { count: 1e3 },
6867
+ removeOnFail: { count: 5e3 },
6344
6868
  ...queue.ratelimit && {
6345
6869
  limiter: {
6346
6870
  max: queue.ratelimit,
@@ -6349,22 +6873,312 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6349
6873
  }
6350
6874
  }
6351
6875
  );
6352
- worker.on("completed", (job, returnvalue) => {
6353
- console.log(`[EXULU] completed job ${job.id}.`, returnvalue);
6876
+ worker.on("completed", async (job, returnvalue) => {
6877
+ console.log(`[EXULU] completed job ${job.id}.`, logMetadata(job.name, {
6878
+ result: returnvalue
6879
+ }));
6880
+ const { db: db3 } = await postgresClient();
6881
+ await db3.from("job_results").where({ job_id: job.id }).update({
6882
+ state: JOB_STATUS_ENUM.completed,
6883
+ result: returnvalue.result,
6884
+ metadata: returnvalue.metadata
6885
+ });
6354
6886
  });
6355
- worker.on("failed", (job, error, prev) => {
6887
+ worker.on("failed", async (job, error, prev) => {
6356
6888
  if (job?.id) {
6357
- console.error(`[EXULU] failed job ${job.id}.`);
6889
+ const { db: db3 } = await postgresClient();
6890
+ console.error(`[EXULU] failed job ${job.id}.`, error);
6891
+ await db3.from("job_results").where({ job_id: job.id }).update({
6892
+ state: JOB_STATUS_ENUM.failed,
6893
+ error
6894
+ });
6895
+ return;
6358
6896
  }
6359
- console.error(`[EXULU] job error.`, error);
6897
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
6898
+ error: error instanceof Error ? error.message : String(error)
6899
+ }) : error);
6900
+ });
6901
+ worker.on("error", (error) => {
6902
+ console.error(`[EXULU] worker error.`, error);
6360
6903
  });
6361
6904
  worker.on("progress", (job, progress) => {
6362
- console.log(`[EXULU] job progress ${job.id}.`, progress);
6905
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
6906
+ progress
6907
+ }));
6363
6908
  });
6909
+ const gracefulShutdown = async (signal) => {
6910
+ console.log(`Received ${signal}, closing server...`);
6911
+ await worker.close();
6912
+ process.exit(0);
6913
+ };
6914
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
6915
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
6364
6916
  return worker;
6365
6917
  });
6366
6918
  return workers;
6367
6919
  };
6920
+ var validateEvalPayload = async (data, agents) => {
6921
+ if (!data.eval_run_id) {
6922
+ throw new Error(`No eval run ID set for eval job.`);
6923
+ }
6924
+ if (!data.test_case_id) {
6925
+ throw new Error(`No test case ID set for eval job.`);
6926
+ }
6927
+ if (!data.user) {
6928
+ throw new Error(`No user set for eval job.`);
6929
+ }
6930
+ if (!data.role) {
6931
+ throw new Error(`No role set for eval job.`);
6932
+ }
6933
+ if (!data.agent_id) {
6934
+ throw new Error(`No agent ID set for eval job.`);
6935
+ }
6936
+ if (!data.inputs?.length) {
6937
+ throw new Error(`No inputs set for eval job, expected array of UIMessage objects.`);
6938
+ }
6939
+ const { db: db3 } = await postgresClient();
6940
+ const evalRun = await db3.from("eval_runs").where({ id: data.eval_run_id }).first();
6941
+ if (!evalRun) {
6942
+ throw new Error(`Eval run ${data.eval_run_id} not found in the database.`);
6943
+ }
6944
+ const agentInstance = await loadAgent(evalRun.agent_id);
6945
+ if (!agentInstance) {
6946
+ throw new Error(`Agent ${evalRun.agent_id} not found in the database.`);
6947
+ }
6948
+ const backend = agents.find((a) => a.id === agentInstance.backend);
6949
+ if (!backend) {
6950
+ throw new Error(`Backend ${agentInstance.backend} not found in the database.`);
6951
+ }
6952
+ const user = await db3.from("users").where({ id: data.user }).first();
6953
+ if (!user) {
6954
+ throw new Error(`User ${data.user} not found in the database.`);
6955
+ }
6956
+ const testCase = await db3.from("test_cases").where({ id: data.test_case_id }).first();
6957
+ if (!testCase) {
6958
+ throw new Error(`Test case ${data.test_case_id} not found in the database.`);
6959
+ }
6960
+ return {
6961
+ agentInstance,
6962
+ backend,
6963
+ user,
6964
+ testCase,
6965
+ evalRun,
6966
+ messages: data.inputs
6967
+ };
6968
+ };
6969
+ var pollJobResult = async ({ queue, jobId }) => {
6970
+ let attempts = 0;
6971
+ let timeoutInSeconds = queue.timeoutInSeconds || 180;
6972
+ const startTime = Date.now();
6973
+ let result;
6974
+ while (true) {
6975
+ attempts++;
6976
+ const job = await import_bullmq5.Job.fromId(queue.queue, jobId);
6977
+ if (!job) {
6978
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
6979
+ continue;
6980
+ }
6981
+ const elapsedTime = Date.now() - startTime;
6982
+ if (elapsedTime > timeoutInSeconds * 1e3) {
6983
+ throw new Error(`Job ${job.id} timed out after ${timeoutInSeconds} seconds for job eval function job ${job.name}.`);
6984
+ }
6985
+ console.log(`[EXULU] polling eval function job ${job.name} for state... (attempt ${attempts})`);
6986
+ const jobState = await job.getState();
6987
+ console.log(`[EXULU] eval function job ${job.name} state: ${jobState}`);
6988
+ if (jobState === "failed") {
6989
+ throw new Error(`Job ${job.name} (${job.id}) failed with error: ${job.failedReason}.`);
6990
+ }
6991
+ if (jobState === "completed") {
6992
+ console.log(`[EXULU] eval function job ${job.name} completed, getting result from database...`);
6993
+ const { db: db3 } = await postgresClient();
6994
+ const entry = await db3.from("job_results").where({ job_id: job.id }).first();
6995
+ result = entry?.result;
6996
+ if (result === void 0 || result === null || result === "") {
6997
+ throw new Error(`Eval function ${job.id} result not found in database for job eval function job ${job.name}.`);
6998
+ }
6999
+ console.log(`[EXULU] eval function ${job.id} result: ${result}`);
7000
+ break;
7001
+ }
7002
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
7003
+ }
7004
+ return result;
7005
+ };
7006
+ var processUiMessagesFlow = async ({
7007
+ agents,
7008
+ agentInstance,
7009
+ agentBackend,
7010
+ inputMessages,
7011
+ contexts,
7012
+ user,
7013
+ tools,
7014
+ config
7015
+ }) => {
7016
+ console.log("[EXULU] processing UI messages flow for agent.");
7017
+ console.log("[EXULU] input messages", inputMessages);
7018
+ console.log("[EXULU] agent tools", agentInstance.tools?.map((x) => x.name + " (" + x.id + ")"));
7019
+ const disabledTools = [];
7020
+ let enabledTools = await getEnabledTools(agentInstance, tools, disabledTools, agents, user);
7021
+ console.log("[EXULU] enabled tools", enabledTools?.map((x) => x.name + " (" + x.id + ")"));
7022
+ const variableName = agentInstance.providerapikey;
7023
+ const { db: db3 } = await postgresClient();
7024
+ const variable = await db3.from("variables").where({ name: variableName }).first();
7025
+ if (!variable) {
7026
+ throw new Error(`Provider API key variable not found for agent ${agentInstance.name} (${agentInstance.id}).`);
7027
+ }
7028
+ let providerapikey = variable.value;
7029
+ if (!variable.encrypted) {
7030
+ 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.`);
7031
+ }
7032
+ if (variable.encrypted) {
7033
+ const bytes = import_crypto_js4.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7034
+ providerapikey = bytes.toString(import_crypto_js4.default.enc.Utf8);
7035
+ }
7036
+ const messagesWithoutPlaceholder = inputMessages.filter(
7037
+ (message) => message.metadata?.type !== "placeholder"
7038
+ );
7039
+ console.log("[EXULU] messages without placeholder", messagesWithoutPlaceholder);
7040
+ let index = 0;
7041
+ let messageHistory = {
7042
+ messages: [],
7043
+ metadata: {
7044
+ tokens: {
7045
+ totalTokens: 0,
7046
+ reasoningTokens: 0,
7047
+ inputTokens: 0,
7048
+ outputTokens: 0,
7049
+ cachedInputTokens: 0
7050
+ },
7051
+ duration: 0
7052
+ }
7053
+ };
7054
+ for (const currentMessage of messagesWithoutPlaceholder) {
7055
+ console.log("[EXULU] running through the conversation");
7056
+ console.log("[EXULU] current index", index);
7057
+ console.log("[EXULU] current message", currentMessage);
7058
+ console.log("[EXULU] message history", messageHistory);
7059
+ const statistics = {
7060
+ label: agentInstance.name,
7061
+ trigger: "agent"
7062
+ };
7063
+ messageHistory = await new Promise(async (resolve, reject) => {
7064
+ const startTime = Date.now();
7065
+ try {
7066
+ const result = await agentBackend.generateStream({
7067
+ contexts,
7068
+ user,
7069
+ instructions: agentInstance.instructions,
7070
+ session: void 0,
7071
+ previousMessages: messageHistory.messages,
7072
+ message: currentMessage,
7073
+ currentTools: enabledTools,
7074
+ allExuluTools: tools,
7075
+ providerapikey,
7076
+ toolConfigs: agentInstance.tools,
7077
+ exuluConfig: config
7078
+ });
7079
+ console.log("[EXULU] consuming stream for agent.");
7080
+ const stream = result.stream.toUIMessageStream({
7081
+ messageMetadata: ({ part }) => {
7082
+ console.log("[EXULU] part", part.type);
7083
+ if (part.type === "finish") {
7084
+ return {
7085
+ totalTokens: part.totalUsage.totalTokens,
7086
+ reasoningTokens: part.totalUsage.reasoningTokens,
7087
+ inputTokens: part.totalUsage.inputTokens,
7088
+ outputTokens: part.totalUsage.outputTokens,
7089
+ cachedInputTokens: part.totalUsage.cachedInputTokens
7090
+ };
7091
+ }
7092
+ },
7093
+ originalMessages: result.originalMessages,
7094
+ sendReasoning: true,
7095
+ sendSources: true,
7096
+ onError: (error) => {
7097
+ console.error("[EXULU] Ui message stream error.", error);
7098
+ reject(error);
7099
+ return `Ui message stream error: ${error instanceof Error ? error.message : String(error)}`;
7100
+ },
7101
+ onFinish: async ({ messages, isContinuation, responseMessage }) => {
7102
+ const metadata = messages[messages.length - 1]?.metadata;
7103
+ console.log("[EXULU] Stream finished with messages:", messages);
7104
+ console.log("[EXULU] Stream metadata", metadata);
7105
+ await Promise.all([
7106
+ updateStatistic({
7107
+ name: "count",
7108
+ label: statistics.label,
7109
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7110
+ trigger: statistics.trigger,
7111
+ count: 1,
7112
+ user: user.id,
7113
+ role: user?.role?.id
7114
+ }),
7115
+ ...metadata?.inputTokens ? [
7116
+ updateStatistic({
7117
+ name: "inputTokens",
7118
+ label: statistics.label,
7119
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7120
+ trigger: statistics.trigger,
7121
+ count: metadata?.inputTokens,
7122
+ user: user.id,
7123
+ role: user?.role?.id
7124
+ })
7125
+ ] : [],
7126
+ ...metadata?.outputTokens ? [
7127
+ updateStatistic({
7128
+ name: "outputTokens",
7129
+ label: statistics.label,
7130
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7131
+ trigger: statistics.trigger,
7132
+ count: metadata?.outputTokens
7133
+ })
7134
+ ] : []
7135
+ ]);
7136
+ resolve({
7137
+ messages,
7138
+ metadata: {
7139
+ tokens: {
7140
+ totalTokens: messageHistory.metadata.tokens.totalTokens + metadata?.totalTokens,
7141
+ reasoningTokens: messageHistory.metadata.tokens.reasoningTokens + metadata?.reasoningTokens,
7142
+ inputTokens: messageHistory.metadata.tokens.inputTokens + metadata?.inputTokens,
7143
+ outputTokens: messageHistory.metadata.tokens.outputTokens + metadata?.outputTokens,
7144
+ cachedInputTokens: messageHistory.metadata.tokens.cachedInputTokens + metadata?.cachedInputTokens
7145
+ },
7146
+ duration: messageHistory.metadata.duration + (Date.now() - startTime)
7147
+ }
7148
+ });
7149
+ }
7150
+ });
7151
+ for await (const message of stream) {
7152
+ console.log("[EXULU] message", message);
7153
+ }
7154
+ } catch (error) {
7155
+ console.error(`[EXULU] error generating stream for agent ${agentInstance.name} (${agentInstance.id}).`, error);
7156
+ reject(error);
7157
+ }
7158
+ });
7159
+ index++;
7160
+ }
7161
+ console.log("[EXULU] finished processing UI messages flow for agent, messages result", messageHistory);
7162
+ return messageHistory;
7163
+ };
7164
+ function getMedian(arr) {
7165
+ if (arr.length === 0) return 0;
7166
+ const sortedArr = arr.slice().sort((a, b) => a - b);
7167
+ const mid = Math.floor(sortedArr.length / 2);
7168
+ if (sortedArr.length % 2 !== 0) {
7169
+ return sortedArr[mid];
7170
+ } else {
7171
+ return (sortedArr[mid - 1] + sortedArr[mid]) / 2;
7172
+ }
7173
+ }
7174
+ function getSum(arr) {
7175
+ if (arr.length === 0) return 0;
7176
+ return arr.reduce((a, b) => a + b, 0);
7177
+ }
7178
+ function getAverage(arr) {
7179
+ if (arr.length === 0) return 0;
7180
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
7181
+ }
6368
7182
 
6369
7183
  // src/mcp/index.ts
6370
7184
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -6517,7 +7331,6 @@ var claudeOpus4Agent = new ExuluAgent2({
6517
7331
  audio: [],
6518
7332
  video: []
6519
7333
  },
6520
- evals: [],
6521
7334
  maxContextLength: 2e5,
6522
7335
  config: {
6523
7336
  name: `CLAUDE-OPUS-4`,
@@ -6552,7 +7365,6 @@ var claudeSonnet4Agent = new ExuluAgent2({
6552
7365
  audio: [],
6553
7366
  video: []
6554
7367
  },
6555
- evals: [],
6556
7368
  maxContextLength: 2e5,
6557
7369
  config: {
6558
7370
  name: `CLAUDE-SONNET-4`,
@@ -6580,7 +7392,6 @@ var claudeSonnet45Agent = new ExuluAgent2({
6580
7392
  audio: [],
6581
7393
  video: []
6582
7394
  },
6583
- evals: [],
6584
7395
  maxContextLength: 2e5,
6585
7396
  config: {
6586
7397
  name: `CLAUDE-SONNET-4.5`,
@@ -6611,7 +7422,6 @@ var gpt5proAgent = new ExuluAgent2({
6611
7422
  audio: [],
6612
7423
  video: []
6613
7424
  },
6614
- evals: [],
6615
7425
  maxContextLength: 4e5,
6616
7426
  config: {
6617
7427
  name: `GPT-5-PRO`,
@@ -6639,7 +7449,6 @@ var gpt5CodexAgent = new ExuluAgent2({
6639
7449
  audio: [],
6640
7450
  video: []
6641
7451
  },
6642
- evals: [],
6643
7452
  maxContextLength: 4e5,
6644
7453
  config: {
6645
7454
  name: `GPT-5-CODEX`,
@@ -6667,7 +7476,6 @@ var gpt5MiniAgent = new ExuluAgent2({
6667
7476
  audio: [],
6668
7477
  video: []
6669
7478
  },
6670
- evals: [],
6671
7479
  maxContextLength: 4e5,
6672
7480
  config: {
6673
7481
  name: `GPT-5-MINI`,
@@ -6702,7 +7510,6 @@ var gpt5agent = new ExuluAgent2({
6702
7510
  audio: [],
6703
7511
  video: []
6704
7512
  },
6705
- evals: [],
6706
7513
  maxContextLength: 4e5,
6707
7514
  config: {
6708
7515
  name: `GPT-5`,
@@ -6737,7 +7544,6 @@ var gpt5NanoAgent = new ExuluAgent2({
6737
7544
  audio: [],
6738
7545
  video: []
6739
7546
  },
6740
- evals: [],
6741
7547
  maxContextLength: 4e5,
6742
7548
  config: {
6743
7549
  name: `GPT-5-NANO`,
@@ -6765,7 +7571,6 @@ var gpt41Agent = new ExuluAgent2({
6765
7571
  audio: [],
6766
7572
  video: []
6767
7573
  },
6768
- evals: [],
6769
7574
  maxContextLength: 1047576,
6770
7575
  config: {
6771
7576
  name: `GPT-4.1`,
@@ -6793,7 +7598,6 @@ var gpt41MiniAgent = new ExuluAgent2({
6793
7598
  audio: [],
6794
7599
  video: []
6795
7600
  },
6796
- evals: [],
6797
7601
  maxContextLength: 1047576,
6798
7602
  config: {
6799
7603
  name: `GPT-4.1-MINI`,
@@ -6821,7 +7625,6 @@ var gpt4oAgent = new ExuluAgent2({
6821
7625
  audio: [],
6822
7626
  video: []
6823
7627
  },
6824
- evals: [],
6825
7628
  maxContextLength: 128e3,
6826
7629
  config: {
6827
7630
  name: `Default agent`,
@@ -6849,7 +7652,6 @@ var gpt4oMiniAgent = new ExuluAgent2({
6849
7652
  audio: [],
6850
7653
  video: []
6851
7654
  },
6852
- evals: [],
6853
7655
  maxContextLength: 128e3,
6854
7656
  config: {
6855
7657
  name: `GPT-4O-MINI`,
@@ -6940,6 +7742,55 @@ var outputsContext = new ExuluContext({
6940
7742
  // src/registry/index.ts
6941
7743
  var import_winston2 = __toESM(require("winston"), 1);
6942
7744
  var import_util = __toESM(require("util"), 1);
7745
+
7746
+ // src/templates/evals/index.ts
7747
+ var import_zod3 = require("zod");
7748
+ var llmAsJudgeEval = new ExuluEval2({
7749
+ id: "llm_as_judge",
7750
+ name: "LLM as Judge",
7751
+ description: "Evaluate the output of the LLM as a judge.",
7752
+ execute: async ({ agent, backend, messages, testCase, config }) => {
7753
+ console.log("[EXULU] running llm as judge eval", { agent, backend, messages, testCase, config });
7754
+ let prompt = config?.prompt;
7755
+ if (!prompt) {
7756
+ console.error("[EXULU] prompt is required.");
7757
+ throw new Error("Prompt is required.");
7758
+ }
7759
+ const lastMessage = messages[messages.length - 1]?.parts?.filter((part) => part.type === "text").map((part) => part.text).join("\n");
7760
+ console.log("[EXULU] last message", lastMessage);
7761
+ if (!lastMessage) {
7762
+ return 0;
7763
+ }
7764
+ prompt = prompt.replace("{actual_output}", lastMessage);
7765
+ prompt = prompt.replace("{expected_output}", testCase.expected_output);
7766
+ if (!agent.providerapikey) {
7767
+ throw new Error(`Provider API key for agent ${agent.name} is required, variable name is ${agent.providerapikey} but it is not set.`);
7768
+ }
7769
+ const providerapikey = await ExuluVariables.get(agent.providerapikey);
7770
+ console.log("[EXULU] prompt", prompt);
7771
+ const response = await backend.generateSync({
7772
+ prompt,
7773
+ outputSchema: import_zod3.z.object({
7774
+ score: import_zod3.z.number().min(0).max(100).describe("The score between 0 and 100.")
7775
+ }),
7776
+ providerapikey
7777
+ });
7778
+ console.log("[EXULU] response", response);
7779
+ const score = parseFloat(response.score);
7780
+ if (isNaN(score)) {
7781
+ throw new Error(`Generated score from llm as a judge eval is not a number: ${response.score}`);
7782
+ }
7783
+ return score;
7784
+ },
7785
+ config: [{
7786
+ name: "prompt",
7787
+ 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."
7788
+ }],
7789
+ queue: queues.register("llm_as_judge", 1, 1).use(),
7790
+ llm: true
7791
+ });
7792
+
7793
+ // src/registry/index.ts
6943
7794
  var isDev = process.env.NODE_ENV !== "production";
6944
7795
  var consoleTransport = new import_winston2.default.transports.Console({
6945
7796
  format: isDev ? import_winston2.default.format.combine(
@@ -6950,6 +7801,20 @@ var consoleTransport = new import_winston2.default.transports.Console({
6950
7801
  })
6951
7802
  ) : import_winston2.default.format.json()
6952
7803
  });
7804
+ var formatArg = (arg) => typeof arg === "object" ? import_util.default.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7805
+ var createLogMethod = (logger, logLevel) => {
7806
+ return (...args) => {
7807
+ const lastArg = args[args.length - 1];
7808
+ let metadata = void 0;
7809
+ let messageArgs = args;
7810
+ if (lastArg && typeof lastArg === "object" && lastArg.__logMetadata === true) {
7811
+ metadata = lastArg;
7812
+ messageArgs = args.slice(0, -1);
7813
+ }
7814
+ const message = messageArgs.map(formatArg).join(" ");
7815
+ logger[logLevel](message, metadata);
7816
+ };
7817
+ };
6953
7818
  var isValidPostgresName = (id) => {
6954
7819
  const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6955
7820
  const isValid = regex.test(id);
@@ -6959,6 +7824,7 @@ var isValidPostgresName = (id) => {
6959
7824
  var ExuluApp = class {
6960
7825
  _agents = [];
6961
7826
  _config;
7827
+ _evals = [];
6962
7828
  _queues = [];
6963
7829
  _contexts = {};
6964
7830
  _tools = [];
@@ -6967,7 +7833,11 @@ var ExuluApp = class {
6967
7833
  }
6968
7834
  // Factory function so we can async
6969
7835
  // initialize the MCP server if needed.
6970
- create = async ({ contexts, agents, config, tools }) => {
7836
+ create = async ({ contexts, agents, config, tools, evals }) => {
7837
+ this._evals = [
7838
+ llmAsJudgeEval,
7839
+ ...evals ?? []
7840
+ ];
6971
7841
  this._contexts = {
6972
7842
  ...contexts,
6973
7843
  codeStandardsContext,
@@ -7019,11 +7889,12 @@ var ExuluApp = class {
7019
7889
  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
7890
  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
7891
  }
7022
- const contextsArray = Object.values(contexts || {});
7023
7892
  const queueSet = /* @__PURE__ */ new Set();
7024
- for (const context of contextsArray) {
7025
- if (context.embedder?.queue) {
7026
- queueSet.add(await context.embedder.queue);
7893
+ if (redisServer.host?.length && redisServer.port?.length) {
7894
+ queues.register(global_queues.eval_runs, 1, 1);
7895
+ for (const queue of queues.list.values()) {
7896
+ const config2 = await queue.use();
7897
+ queueSet.add(config2);
7027
7898
  }
7028
7899
  }
7029
7900
  this._queues = [...new Set(queueSet.values())];
@@ -7099,7 +7970,7 @@ var ExuluApp = class {
7099
7970
  };
7100
7971
  bullmq = {
7101
7972
  workers: {
7102
- create: async () => {
7973
+ create: async (queues3) => {
7103
7974
  if (!this._config) {
7104
7975
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7105
7976
  }
@@ -7112,16 +7983,22 @@ var ExuluApp = class {
7112
7983
  enableOtel: this._config?.workers?.telemetry?.enabled ?? false,
7113
7984
  transports
7114
7985
  });
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(" "));
7986
+ console.log = createLogMethod(logger, "info");
7987
+ console.info = createLogMethod(logger, "info");
7988
+ console.warn = createLogMethod(logger, "warn");
7989
+ console.error = createLogMethod(logger, "error");
7990
+ console.debug = createLogMethod(logger, "debug");
7991
+ let filteredQueues = this._queues;
7992
+ if (queues3) {
7993
+ filteredQueues = filteredQueues.filter((q) => queues3.includes(q.queue.name));
7994
+ }
7121
7995
  return await createWorkers(
7122
- this._queues,
7996
+ this._agents,
7997
+ filteredQueues,
7123
7998
  this._config,
7124
7999
  Object.values(this._contexts ?? {}),
8000
+ this._evals,
8001
+ this._tools,
7125
8002
  tracer
7126
8003
  );
7127
8004
  }
@@ -7138,16 +8015,16 @@ var ExuluApp = class {
7138
8015
  if (this._config?.telemetry?.enabled) {
7139
8016
  tracer = import_api4.trace.getTracer("exulu", "1.0.0");
7140
8017
  }
8018
+ const transports = this._config?.logger?.winston?.transports ?? [consoleTransport];
7141
8019
  const logger = logger_default({
7142
8020
  enableOtel: this._config?.telemetry?.enabled ?? false,
7143
- transports: this._config?.logger?.winston?.transports ?? [consoleTransport]
8021
+ transports
7144
8022
  });
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(" "));
8023
+ console.log = createLogMethod(logger, "info");
8024
+ console.info = createLogMethod(logger, "info");
8025
+ console.warn = createLogMethod(logger, "warn");
8026
+ console.error = createLogMethod(logger, "error");
8027
+ console.debug = createLogMethod(logger, "debug");
7151
8028
  if (!this._config) {
7152
8029
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7153
8030
  }
@@ -7157,7 +8034,9 @@ var ExuluApp = class {
7157
8034
  this._tools,
7158
8035
  Object.values(this._contexts ?? {}),
7159
8036
  this._config,
7160
- tracer
8037
+ this._evals,
8038
+ tracer,
8039
+ this._queues
7161
8040
  );
7162
8041
  if (this._config?.MCP.enabled) {
7163
8042
  const mcp = new ExuluMCP();
@@ -8386,7 +9265,8 @@ var {
8386
9265
  variablesSchema: variablesSchema3,
8387
9266
  workflowTemplatesSchema: workflowTemplatesSchema3,
8388
9267
  rbacSchema: rbacSchema3,
8389
- projectsSchema: projectsSchema3
9268
+ projectsSchema: projectsSchema3,
9269
+ jobResultsSchema: jobResultsSchema3
8390
9270
  } = coreSchemas.get();
8391
9271
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
8392
9272
  for (const field of fields) {
@@ -8420,6 +9300,7 @@ var up = async function(knex) {
8420
9300
  platformConfigurationsSchema3(),
8421
9301
  statisticsSchema3(),
8422
9302
  projectsSchema3(),
9303
+ jobResultsSchema3(),
8423
9304
  rbacSchema3(),
8424
9305
  agentsSchema3(),
8425
9306
  variablesSchema3(),
@@ -8604,20 +9485,7 @@ var create = ({
8604
9485
  };
8605
9486
 
8606
9487
  // 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
9488
+ var import_crypto_js5 = __toESM(require("crypto-js"), 1);
8621
9489
  var ExuluJobs = {
8622
9490
  redis: redisClient,
8623
9491
  jobs: {
@@ -8654,8 +9522,8 @@ var ExuluVariables = {
8654
9522
  throw new Error(`Variable ${name} not found.`);
8655
9523
  }
8656
9524
  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);
9525
+ const bytes = import_crypto_js5.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
9526
+ variable.value = bytes.toString(import_crypto_js5.default.enc.Utf8);
8659
9527
  }
8660
9528
  return variable.value;
8661
9529
  }
@@ -8791,5 +9659,6 @@ var ExuluChunkers = {
8791
9659
  ExuluTool,
8792
9660
  ExuluUtils,
8793
9661
  ExuluVariables,
8794
- db
9662
+ db,
9663
+ logMetadata
8795
9664
  });