@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.js CHANGED
@@ -66,7 +66,7 @@ var validateJob = (job) => {
66
66
  // src/registry/classes.ts
67
67
  import "bullmq";
68
68
  import { z } from "zod";
69
- import { convertToModelMessages, createIdGenerator, generateObject, generateText, streamText, tool, validateUIMessages, stepCountIs } from "ai";
69
+ import { convertToModelMessages, generateObject, generateText, streamText, tool, validateUIMessages, stepCountIs } from "ai";
70
70
 
71
71
  // types/enums/statistics.ts
72
72
  var STATISTICS_TYPE_ENUM = {
@@ -99,7 +99,17 @@ async function ensureDatabaseExists() {
99
99
  database: "postgres",
100
100
  // Connect to default database
101
101
  password: process.env.POSTGRES_DB_PASSWORD,
102
- ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
102
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
103
+ connectionTimeoutMillis: 1e4
104
+ },
105
+ pool: {
106
+ min: 2,
107
+ max: 4,
108
+ acquireTimeoutMillis: 3e4,
109
+ createTimeoutMillis: 3e4,
110
+ idleTimeoutMillis: 3e4,
111
+ reapIntervalMillis: 1e3,
112
+ createRetryIntervalMillis: 200
103
113
  }
104
114
  });
105
115
  try {
@@ -144,7 +154,17 @@ async function postgresClient() {
144
154
  user: process.env.POSTGRES_DB_USER,
145
155
  database: dbName,
146
156
  password: process.env.POSTGRES_DB_PASSWORD,
147
- ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
157
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
158
+ connectionTimeoutMillis: 1e4
159
+ },
160
+ pool: {
161
+ min: 2,
162
+ max: 20,
163
+ acquireTimeoutMillis: 3e4,
164
+ createTimeoutMillis: 3e4,
165
+ idleTimeoutMillis: 3e4,
166
+ reapIntervalMillis: 1e3,
167
+ createRetryIntervalMillis: 200
148
168
  }
149
169
  });
150
170
  try {
@@ -192,7 +212,9 @@ var bullmqDecorator = async ({
192
212
  workflow,
193
213
  item,
194
214
  context,
195
- retries
215
+ retries,
216
+ backoff,
217
+ timeoutInSeconds
196
218
  }) => {
197
219
  const types = [
198
220
  embedder,
@@ -216,30 +238,35 @@ var bullmqDecorator = async ({
216
238
  if (embedder) {
217
239
  type = "embedder";
218
240
  }
241
+ const jobData = {
242
+ label,
243
+ type: `${type}`,
244
+ timeoutInSeconds: timeoutInSeconds || 180,
245
+ // 3 minutes default
246
+ inputs,
247
+ ...user && { user },
248
+ ...role && { role },
249
+ ...trigger && { trigger },
250
+ ...workflow && { workflow },
251
+ ...embedder && { embedder },
252
+ ...processor && { processor },
253
+ ...evaluation && { evaluation },
254
+ ...item && { item },
255
+ ...context && { context }
256
+ };
219
257
  const redisId = uuidv4();
220
258
  const job = await queue.add(
221
259
  `${embedder || workflow || processor || evaluation}`,
222
- {
223
- label,
224
- type: `${type}`,
225
- inputs,
226
- ...user && { user },
227
- ...role && { role },
228
- ...trigger && { trigger },
229
- ...workflow && { workflow },
230
- ...embedder && { embedder },
231
- ...processor && { processor },
232
- ...evaluation && { evaluation },
233
- ...item && { item },
234
- ...context && { context }
235
- },
260
+ jobData,
236
261
  {
237
262
  jobId: redisId,
238
263
  // Setting it to 3 as a sensible default, as
239
264
  // many AI services are quite unstable.
240
265
  attempts: retries || 3,
241
266
  // todo make this configurable?
242
- backoff: {
267
+ removeOnComplete: 5e3,
268
+ removeOnFail: 1e4,
269
+ backoff: backoff || {
243
270
  type: "exponential",
244
271
  delay: 2e3
245
272
  }
@@ -684,6 +711,17 @@ var requestValidators = {
684
711
  // src/registry/utils/graphql.ts
685
712
  import bcrypt3 from "bcryptjs";
686
713
 
714
+ // types/enums/jobs.ts
715
+ var JOB_STATUS_ENUM = {
716
+ completed: "completed",
717
+ failed: "failed",
718
+ delayed: "delayed",
719
+ active: "active",
720
+ waiting: "waiting",
721
+ paused: "paused",
722
+ stuck: "stuck"
723
+ };
724
+
687
725
  // src/postgres/core-schema.ts
688
726
  var agentMessagesSchema = {
689
727
  type: "agent_messages",
@@ -1118,6 +1156,45 @@ var evalSetsSchema = {
1118
1156
  }
1119
1157
  ]
1120
1158
  };
1159
+ var jobResultsSchema = {
1160
+ type: "job_results",
1161
+ name: {
1162
+ plural: "job_results",
1163
+ singular: "job_result"
1164
+ },
1165
+ fields: [
1166
+ {
1167
+ name: "job_id",
1168
+ type: "text"
1169
+ },
1170
+ {
1171
+ name: "state",
1172
+ type: "text"
1173
+ },
1174
+ {
1175
+ name: "error",
1176
+ type: "json"
1177
+ },
1178
+ {
1179
+ name: "label",
1180
+ type: "text",
1181
+ index: true
1182
+ },
1183
+ {
1184
+ name: "tries",
1185
+ type: "number",
1186
+ default: 0
1187
+ },
1188
+ {
1189
+ name: "result",
1190
+ type: "json"
1191
+ },
1192
+ {
1193
+ name: "metadata",
1194
+ type: "json"
1195
+ }
1196
+ ]
1197
+ };
1121
1198
  var evalRunsSchema = {
1122
1199
  type: "eval_runs",
1123
1200
  name: {
@@ -1126,6 +1203,15 @@ var evalRunsSchema = {
1126
1203
  },
1127
1204
  RBAC: true,
1128
1205
  fields: [
1206
+ {
1207
+ name: "name",
1208
+ type: "text"
1209
+ },
1210
+ {
1211
+ name: "timeout_in_seconds",
1212
+ type: "number",
1213
+ default: 180
1214
+ },
1129
1215
  {
1130
1216
  name: "eval_set_id",
1131
1217
  type: "uuid",
@@ -1137,7 +1223,7 @@ var evalRunsSchema = {
1137
1223
  required: true
1138
1224
  },
1139
1225
  {
1140
- name: "eval_function_ids",
1226
+ name: "eval_functions",
1141
1227
  type: "json",
1142
1228
  required: true
1143
1229
  },
@@ -1148,7 +1234,7 @@ var evalRunsSchema = {
1148
1234
  {
1149
1235
  name: "scoring_method",
1150
1236
  type: "enum",
1151
- enumValues: ["mean", "sum", "average"],
1237
+ enumValues: ["median", "sum", "average"],
1152
1238
  required: true
1153
1239
  },
1154
1240
  {
@@ -1272,7 +1358,8 @@ var coreSchemas = {
1272
1358
  variablesSchema: () => addCoreFields(variablesSchema),
1273
1359
  rbacSchema: () => addCoreFields(rbacSchema),
1274
1360
  workflowTemplatesSchema: () => addCoreFields(workflowTemplatesSchema),
1275
- platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema)
1361
+ platformConfigurationsSchema: () => addCoreFields(platformConfigurationsSchema),
1362
+ jobResultsSchema: () => addCoreFields(jobResultsSchema)
1276
1363
  };
1277
1364
  }
1278
1365
  };
@@ -1335,11 +1422,11 @@ var bullmq = {
1335
1422
  if (!data.inputs) {
1336
1423
  throw new Error(`Missing property "inputs" in data for job ${id}.`);
1337
1424
  }
1338
- if (data.type !== "embedder" && data.type !== "workflow") {
1339
- throw new Error(`Property "type" in data for job ${id} must be of value "embedder" or "workflow".`);
1425
+ if (data.type !== "embedder" && data.type !== "workflow" && data.type !== "processor" && data.type !== "eval_run" && data.type !== "eval_function") {
1426
+ throw new Error(`Property "type" in data for job ${id} must be of value "embedder", "workflow", "processor", "eval_run" or "eval_function".`);
1340
1427
  }
1341
- if (!data.workflow && !data.embedder) {
1342
- throw new Error(`Either a workflow or embedder must be set for job ${id}.`);
1428
+ if (!data.workflow && !data.embedder && !data.processor && !data.eval_run_id && !data.eval_functions?.length) {
1429
+ throw new Error(`Either a workflow, embedder, processor, eval_run or eval_functions must be set for job ${id}.`);
1343
1430
  }
1344
1431
  }
1345
1432
  };
@@ -1543,6 +1630,7 @@ var generateApiKey = async (name, email) => {
1543
1630
  };
1544
1631
 
1545
1632
  // src/registry/utils/graphql.ts
1633
+ import { v4 as uuidv42 } from "uuid";
1546
1634
  var GraphQLDate = new GraphQLScalarType({
1547
1635
  name: "Date",
1548
1636
  description: "Date custom scalar type",
@@ -1638,7 +1726,6 @@ ${enumValues}
1638
1726
  fields.push(" maxContextLength: Int");
1639
1727
  fields.push(" provider: String");
1640
1728
  fields.push(" slug: String");
1641
- fields.push(" evals: [AgentEvalFunction]");
1642
1729
  }
1643
1730
  const rbacField = table.RBAC ? " RBAC: RBACData" : "";
1644
1731
  const typeDef = `
@@ -1850,11 +1937,6 @@ function createMutations(table, agents, contexts, tools, config) {
1850
1937
  if (!record) {
1851
1938
  throw new Error("Record not found");
1852
1939
  }
1853
- if (tableNamePlural === "jobs") {
1854
- if (!user.super_admin && record.created_by !== user.id) {
1855
- throw new Error("You are not authorized to edit this record");
1856
- }
1857
- }
1858
1940
  if (record.rights_mode === "public") {
1859
1941
  return true;
1860
1942
  }
@@ -2277,10 +2359,6 @@ function createMutations(table, agents, contexts, tools, config) {
2277
2359
  }
2278
2360
  var applyAccessControl = (table, user, query) => {
2279
2361
  const tableNamePlural = table.name.plural.toLowerCase();
2280
- if (!user.super_admin && table.name.plural === "jobs") {
2281
- query = query.where("created_by", user.id);
2282
- return query;
2283
- }
2284
2362
  if (table.name.plural !== "agent_sessions" && user.super_admin === true) {
2285
2363
  return query;
2286
2364
  }
@@ -2415,14 +2493,6 @@ var addAgentFields = async (requestedFields, agents, result, tools, user) => {
2415
2493
  if (requestedFields.includes("provider")) {
2416
2494
  result.provider = backend?.provider || "";
2417
2495
  }
2418
- if (requestedFields.includes("evals")) {
2419
- result.evals = backend?.evals?.map((evalFunc) => ({
2420
- id: evalFunc.id,
2421
- name: evalFunc.name,
2422
- description: evalFunc.description,
2423
- config: evalFunc.config || []
2424
- })) || [];
2425
- }
2426
2496
  if (!requestedFields.includes("backend")) {
2427
2497
  delete result.backend;
2428
2498
  }
@@ -3125,7 +3195,7 @@ var contextToTableDefinition = (context) => {
3125
3195
  });
3126
3196
  return addCoreFields(definition);
3127
3197
  };
3128
- function createSDL(tables, contexts, agents, tools, config) {
3198
+ function createSDL(tables, contexts, agents, tools, config, evals, queues3) {
3129
3199
  const contextSchemas = contexts.map((context) => contextToTableDefinition(context));
3130
3200
  tables.forEach((table) => {
3131
3201
  if (!table.fields.some((field) => field.name === "createdAt")) {
@@ -3320,15 +3390,39 @@ type PageInfo {
3320
3390
  typeDefs += `
3321
3391
  providers: ProviderPaginationResult
3322
3392
  `;
3393
+ typeDefs += `
3394
+ queue(queue: QueueEnum!): QueueResult
3395
+ `;
3396
+ typeDefs += `
3397
+ evals: EvalPaginationResult
3398
+ `;
3323
3399
  typeDefs += `
3324
3400
  contexts: ContextPaginationResult
3325
3401
  `;
3326
3402
  typeDefs += `
3327
3403
  contextById(id: ID!): Context
3328
3404
  `;
3405
+ mutationDefs += `
3406
+ runEval(id: ID!, test_case_ids: [ID!]): RunEvalReturnPayload
3407
+ `;
3408
+ mutationDefs += `
3409
+ drainQueue(queue: QueueEnum!): JobActionReturnPayload
3410
+ `;
3411
+ mutationDefs += `
3412
+ pauseQueue(queue: QueueEnum!): JobActionReturnPayload
3413
+ `;
3414
+ mutationDefs += `
3415
+ resumeQueue(queue: QueueEnum!): JobActionReturnPayload
3416
+ `;
3417
+ mutationDefs += `
3418
+ deleteJob(queue: QueueEnum!, id: String!): JobActionReturnPayload
3419
+ `;
3329
3420
  typeDefs += `
3330
3421
  tools: ToolPaginationResult
3331
3422
  `;
3423
+ typeDefs += `
3424
+ jobs(queue: QueueEnum!, statusses: [JobStateEnum!]): JobPaginationResult
3425
+ `;
3332
3426
  resolvers.Query["providers"] = async (_, args, context, info) => {
3333
3427
  const requestedFields = getRequestedFields(info);
3334
3428
  return {
@@ -3341,6 +3435,227 @@ type PageInfo {
3341
3435
  })
3342
3436
  };
3343
3437
  };
3438
+ resolvers.Query["queue"] = async (_, args, context, info) => {
3439
+ if (!args.queue) {
3440
+ throw new Error("Queue name is required");
3441
+ }
3442
+ const queue = queues.list.get(args.queue);
3443
+ if (!queue) {
3444
+ throw new Error("Queue not found");
3445
+ }
3446
+ const config2 = await queue.use();
3447
+ return {
3448
+ name: config2.queue.name,
3449
+ concurrency: config2.concurrency,
3450
+ ratelimit: config2.ratelimit,
3451
+ isMaxed: await config2.queue.isMaxed(),
3452
+ isPaused: await config2.queue.isPaused(),
3453
+ jobs: {
3454
+ paused: await config2.queue.isPaused(),
3455
+ completed: await config2.queue.getJobCountByTypes("completed"),
3456
+ failed: await config2.queue.getJobCountByTypes("failed"),
3457
+ waiting: await config2.queue.getJobCountByTypes("waiting"),
3458
+ active: await config2.queue.getJobCountByTypes("active"),
3459
+ delayed: await config2.queue.getJobCountByTypes("delayed")
3460
+ }
3461
+ };
3462
+ };
3463
+ resolvers.Mutation["runEval"] = async (_, args, context, info) => {
3464
+ console.log("[EXULU] /evals/run/:id", args.id);
3465
+ const user = context.user;
3466
+ const eval_run_id = args.id;
3467
+ if (!user.super_admin && (!user.role || user.role.evals !== "write")) {
3468
+ throw new Error("You don't have permission to run evals. Required: super_admin or evals write access.");
3469
+ }
3470
+ const { db: db3 } = await postgresClient();
3471
+ const evalRun = await db3.from("eval_runs").where({ id: eval_run_id }).first();
3472
+ if (!evalRun) {
3473
+ throw new Error("Eval run not found in database.");
3474
+ }
3475
+ const hasAccessToEvalRun = await checkRecordAccess(evalRun, "write", user);
3476
+ if (!hasAccessToEvalRun) {
3477
+ throw new Error("You don't have access to this eval run.");
3478
+ }
3479
+ let testCaseIds = evalRun.test_case_ids ? typeof evalRun.test_case_ids === "string" ? JSON.parse(evalRun.test_case_ids) : evalRun.test_case_ids : [];
3480
+ const eval_functions = evalRun.eval_functions ? typeof evalRun.eval_functions === "string" ? JSON.parse(evalRun.eval_functions) : evalRun.eval_functions : [];
3481
+ if (!testCaseIds || testCaseIds.length === 0) {
3482
+ throw new Error("No test cases selected for this eval run.");
3483
+ }
3484
+ if (!eval_functions || eval_functions.length === 0) {
3485
+ throw new Error("No eval functions selected for this eval run.");
3486
+ }
3487
+ if (args.test_case_ids) {
3488
+ testCaseIds = testCaseIds.filter((testCase) => args.test_case_ids.includes(testCase));
3489
+ }
3490
+ console.log("[EXULU] test cases ids filtered", testCaseIds);
3491
+ const testCases = await db3.from("test_cases").whereIn("id", testCaseIds);
3492
+ if (testCases.length === 0) {
3493
+ throw new Error("No test cases found for eval run.");
3494
+ }
3495
+ const agentInstance = await loadAgent(evalRun.agent_id);
3496
+ if (!agentInstance) {
3497
+ throw new Error("Agent instance not found for eval run.");
3498
+ }
3499
+ const evalQueue = await queues.register("eval_runs", 1, 1).use();
3500
+ const jobIds = [];
3501
+ for (const testCase of testCases) {
3502
+ const jobData = {
3503
+ label: `Eval Run ${eval_run_id} - Test Case ${testCase.id}`,
3504
+ trigger: "api",
3505
+ timeoutInSeconds: evalRun.timeout_in_seconds || 180,
3506
+ // default to 3 minutes
3507
+ type: "eval_run",
3508
+ eval_run_id,
3509
+ eval_run_name: evalRun.name,
3510
+ test_case_id: testCase.id,
3511
+ test_case_name: testCase.name,
3512
+ eval_functions,
3513
+ // Array of eval function IDs - worker will create child jobs for these
3514
+ agent_id: evalRun.agent_id,
3515
+ inputs: testCase.inputs,
3516
+ expected_output: testCase.expected_output,
3517
+ expected_tools: testCase.expected_tools,
3518
+ expected_knowledge_sources: testCase.expected_knowledge_sources,
3519
+ expected_agent_tools: testCase.expected_agent_tools,
3520
+ config: evalRun.config,
3521
+ scoring_method: evalRun.scoring_method,
3522
+ pass_threshold: evalRun.pass_threshold,
3523
+ user: user.id,
3524
+ role: user.role?.id
3525
+ };
3526
+ const redisId = uuidv42();
3527
+ const job = await evalQueue.queue.add("eval_run", jobData, {
3528
+ jobId: redisId,
3529
+ // Setting it to 3 as a sensible default, as
3530
+ // many AI services are quite unstable.
3531
+ attempts: evalQueue.retries || 1,
3532
+ removeOnComplete: 5e3,
3533
+ removeOnFail: 1e4,
3534
+ backoff: evalQueue.backoff || {
3535
+ type: "exponential",
3536
+ delay: 2e3
3537
+ }
3538
+ });
3539
+ jobIds.push(job.id);
3540
+ }
3541
+ const response = {
3542
+ jobs: jobIds,
3543
+ count: jobIds.length
3544
+ };
3545
+ const requestedFields = getRequestedFields(info);
3546
+ const mapped = {};
3547
+ requestedFields.forEach((field) => {
3548
+ mapped[field] = response[field];
3549
+ });
3550
+ return mapped;
3551
+ };
3552
+ resolvers.Mutation["drainQueue"] = async (_, args, context, info) => {
3553
+ if (!args.queue) {
3554
+ throw new Error("Queue name is required");
3555
+ }
3556
+ const queue = queues.list.get(args.queue);
3557
+ if (!queue) {
3558
+ throw new Error("Queue not found");
3559
+ }
3560
+ const config2 = await queue.use();
3561
+ await config2.queue.drain();
3562
+ return { success: true };
3563
+ };
3564
+ resolvers.Mutation["pauseQueue"] = async (_, args, context, info) => {
3565
+ if (!args.queue) {
3566
+ throw new Error("Queue name is required");
3567
+ }
3568
+ const queue = queues.list.get(args.queue);
3569
+ if (!queue) {
3570
+ throw new Error("Queue not found");
3571
+ }
3572
+ const config2 = await queue.use();
3573
+ await config2.queue.pause();
3574
+ return { success: true };
3575
+ };
3576
+ resolvers.Mutation["resumeQueue"] = async (_, args, context, info) => {
3577
+ if (!args.queue) {
3578
+ throw new Error("Queue name is required");
3579
+ }
3580
+ const queue = queues.list.get(args.queue);
3581
+ if (!queue) {
3582
+ throw new Error("Queue not found");
3583
+ }
3584
+ const config2 = await queue.use();
3585
+ await config2.queue.resume();
3586
+ return { success: true };
3587
+ };
3588
+ resolvers.Mutation["deleteJob"] = async (_, args, context, info) => {
3589
+ if (!args.id) {
3590
+ throw new Error("Job ID is required");
3591
+ }
3592
+ if (!args.queue) {
3593
+ throw new Error("Queue name is required");
3594
+ }
3595
+ const queue = queues.list.get(args.queue);
3596
+ if (!queue) {
3597
+ throw new Error("Queue not found");
3598
+ }
3599
+ const config2 = await queue.use();
3600
+ await config2.queue.remove(args.id);
3601
+ return { success: true };
3602
+ };
3603
+ resolvers.Query["evals"] = async (_, args, context, info) => {
3604
+ const requestedFields = getRequestedFields(info);
3605
+ return {
3606
+ items: evals.map((_eval) => {
3607
+ const object = {};
3608
+ requestedFields.forEach((field) => {
3609
+ object[field] = _eval[field];
3610
+ });
3611
+ return object;
3612
+ })
3613
+ };
3614
+ };
3615
+ resolvers.Query["jobs"] = async (_, args, context, info) => {
3616
+ if (!args.queue) {
3617
+ throw new Error("Queue name is required");
3618
+ }
3619
+ const { client: client2 } = await redisClient();
3620
+ if (!client2) {
3621
+ throw new Error("Redis client not created properly");
3622
+ }
3623
+ const {
3624
+ jobs,
3625
+ count
3626
+ } = await getJobsByQueueAndName(
3627
+ args.queue,
3628
+ args.statusses,
3629
+ args.page || 1,
3630
+ args.limit || 100
3631
+ );
3632
+ console.log("[EXULU] jobs", jobs.map((job) => job.name));
3633
+ const requestedFields = getRequestedFields(info);
3634
+ return {
3635
+ items: await Promise.all(jobs.map(async (job) => {
3636
+ const object = {};
3637
+ for (const field of requestedFields) {
3638
+ if (field === "data") {
3639
+ object[field] = job[field];
3640
+ } else if (field === "timestamp") {
3641
+ object[field] = new Date(job[field]).toISOString();
3642
+ } else if (field === "state") {
3643
+ object[field] = await job.getState();
3644
+ } else {
3645
+ object[field] = job[field];
3646
+ }
3647
+ }
3648
+ return object;
3649
+ })),
3650
+ pageInfo: {
3651
+ pageCount: Math.ceil(count / (args.limit || 100)),
3652
+ itemCount: count,
3653
+ currentPage: args.page || 1,
3654
+ hasPreviousPage: args.page && args.page > 1 ? true : false,
3655
+ hasNextPage: args.page && args.page < Math.ceil(jobs.length / (args.limit || 100)) ? true : false
3656
+ }
3657
+ };
3658
+ };
3344
3659
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3345
3660
  const data = contexts.map((context2) => ({
3346
3661
  id: context2.id,
@@ -3446,7 +3761,32 @@ type PageInfo {
3446
3761
  };
3447
3762
  modelDefs += `
3448
3763
  type ProviderPaginationResult {
3449
- items: [Provider]!
3764
+ items: [Provider]!
3765
+ }
3766
+ `;
3767
+ modelDefs += `
3768
+ type QueueResult {
3769
+ name: String!
3770
+ concurrency: Int!
3771
+ ratelimit: Int!
3772
+ isMaxed: Boolean!
3773
+ isPaused: Boolean!
3774
+ jobs: QueueJobsCounts
3775
+ }
3776
+ `;
3777
+ modelDefs += `
3778
+ type QueueJobsCounts {
3779
+ paused: Int!
3780
+ completed: Int!
3781
+ failed: Int!
3782
+ waiting: Int!
3783
+ active: Int!
3784
+ delayed: Int!
3785
+ }
3786
+ `;
3787
+ modelDefs += `
3788
+ type EvalPaginationResult {
3789
+ items: [Eval]!
3450
3790
  }
3451
3791
  `;
3452
3792
  modelDefs += `
@@ -3459,6 +3799,12 @@ type PageInfo {
3459
3799
  items: [Tool]!
3460
3800
  }
3461
3801
  `;
3802
+ modelDefs += `
3803
+ type JobPaginationResult {
3804
+ items: [Job]!
3805
+ pageInfo: PageInfo!
3806
+ }
3807
+ `;
3462
3808
  typeDefs += "}\n";
3463
3809
  mutationDefs += "}\n";
3464
3810
  const genericTypes = `
@@ -3516,6 +3862,19 @@ type Provider {
3516
3862
  type: EnumProviderType!
3517
3863
  }
3518
3864
 
3865
+ type Eval {
3866
+ id: ID!
3867
+ name: String!
3868
+ description: String!
3869
+ llm: Boolean!
3870
+ config: [EvalConfig!]
3871
+ }
3872
+
3873
+ type EvalConfig {
3874
+ name: String!
3875
+ description: String!
3876
+ }
3877
+
3519
3878
  type Context {
3520
3879
  id: ID!
3521
3880
  name: String!
@@ -3527,6 +3886,15 @@ type Context {
3527
3886
  configuration: JSON
3528
3887
  }
3529
3888
 
3889
+ type RunEvalReturnPayload {
3890
+ jobs: [String!]!
3891
+ count: Int!
3892
+ }
3893
+
3894
+ type JobActionReturnPayload {
3895
+ success: Boolean!
3896
+ }
3897
+
3530
3898
  type ContextField {
3531
3899
  name: String!
3532
3900
  type: String!
@@ -3542,10 +3910,35 @@ type Tool {
3542
3910
  config: JSON
3543
3911
  }
3544
3912
 
3913
+ type Job {
3914
+ id: String!
3915
+ name: String!
3916
+ returnvalue: JSON
3917
+ stacktrace: [String]
3918
+ failedReason: String
3919
+ state: String!
3920
+ data: JSON
3921
+ timestamp: Date
3922
+ }
3923
+
3545
3924
  enum EnumProviderType {
3546
3925
  agent
3547
3926
  }
3548
3927
 
3928
+ enum QueueEnum {
3929
+ ${queues.list.keys().toArray().join("\n")}
3930
+ }
3931
+
3932
+ enum JobStateEnum {
3933
+ ${JOB_STATUS_ENUM.active}
3934
+ ${JOB_STATUS_ENUM.waiting}
3935
+ ${JOB_STATUS_ENUM.delayed}
3936
+ ${JOB_STATUS_ENUM.failed}
3937
+ ${JOB_STATUS_ENUM.completed}
3938
+ ${JOB_STATUS_ENUM.paused}
3939
+ ${JOB_STATUS_ENUM.stuck}
3940
+ }
3941
+
3549
3942
  type StatisticsResult {
3550
3943
  group: String!
3551
3944
  count: Int!
@@ -3575,6 +3968,25 @@ var validateCreateOrRemoveSuperAdminPermission = async (tableNamePlural, input,
3575
3968
  }
3576
3969
  }
3577
3970
  };
3971
+ async function getJobsByQueueAndName(queueName, statusses, page, limit) {
3972
+ const queue = queues.list.get(queueName);
3973
+ if (!queue) {
3974
+ throw new Error(`Queue ${queueName} not found`);
3975
+ }
3976
+ const config = await queue.use();
3977
+ const startIndex = (page || 1) - 1;
3978
+ const endIndex = startIndex + (limit || 100);
3979
+ const jobs = await config.queue.getJobs(statusses || [], startIndex, endIndex, false);
3980
+ const counts = await config.queue.getJobCounts(...statusses || []);
3981
+ let total = 0;
3982
+ if (counts) {
3983
+ total = Object.keys(counts).reduce((acc, key) => acc + (counts[key] || 0), 0);
3984
+ }
3985
+ return {
3986
+ jobs,
3987
+ count: total
3988
+ };
3989
+ }
3578
3990
 
3579
3991
  // src/registry/classes.ts
3580
3992
  import {
@@ -3643,6 +4055,7 @@ var addPrefixToKey = (keyPath, config) => {
3643
4055
  return `${prefix}/${keyPath}`;
3644
4056
  };
3645
4057
  var uploadFile = async (user, file, key, config, options = {}) => {
4058
+ console.log("[EXULU] Uploading file to S3", key);
3646
4059
  const client2 = getS3Client(config);
3647
4060
  let folder = `${user}/`;
3648
4061
  const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
@@ -4256,27 +4669,34 @@ function errorHandler(error) {
4256
4669
  }
4257
4670
  return JSON.stringify(error);
4258
4671
  }
4259
- var ExuluEval = class {
4672
+ var ExuluEval2 = class {
4260
4673
  id;
4261
4674
  name;
4262
4675
  description;
4676
+ llm;
4263
4677
  execute;
4264
4678
  config;
4265
4679
  queue;
4266
- constructor({ id, name, description, execute: execute2, config, queue }) {
4680
+ constructor({ id, name, description, execute: execute2, config, queue, llm }) {
4267
4681
  this.id = id;
4268
4682
  this.name = name;
4269
4683
  this.description = description;
4270
4684
  this.execute = execute2;
4271
4685
  this.config = config;
4686
+ this.llm = llm;
4272
4687
  this.queue = queue;
4273
4688
  }
4274
- async run(messages, metadata, config) {
4275
- const score = await this.execute({ messages, metadata, config });
4276
- if (score < 0 || score > 100) {
4277
- throw new Error(`Eval function ${this.name} must return a score between 0 and 100, got ${score}`);
4689
+ async run(agent, backend, testCase, messages, config) {
4690
+ try {
4691
+ const score = await this.execute({ agent, backend, testCase, messages, config });
4692
+ if (score < 0 || score > 100) {
4693
+ throw new Error(`Eval function ${this.name} must return a score between 0 and 100, got ${score}`);
4694
+ }
4695
+ return score;
4696
+ } catch (error) {
4697
+ console.error(`[EXULU] error running eval function ${this.name}.`, error);
4698
+ throw new Error(`Error running eval function ${this.name}: ${error instanceof Error ? error.message : String(error)}`);
4278
4699
  }
4279
- return score;
4280
4700
  }
4281
4701
  };
4282
4702
  var ExuluAgent2 = class {
@@ -4291,13 +4711,13 @@ var ExuluAgent2 = class {
4291
4711
  type;
4292
4712
  streaming = false;
4293
4713
  maxContextLength;
4714
+ queue;
4294
4715
  rateLimit;
4295
4716
  config;
4296
- evals;
4297
4717
  // private memory: Memory | undefined; // TODO do own implementation
4298
4718
  model;
4299
4719
  capabilities;
4300
- constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, evals, provider }) {
4720
+ constructor({ id, name, description, config, rateLimit, capabilities, type, maxContextLength, provider, queue }) {
4301
4721
  this.id = id;
4302
4722
  this.name = name;
4303
4723
  this.description = description;
@@ -4306,7 +4726,7 @@ var ExuluAgent2 = class {
4306
4726
  this.config = config;
4307
4727
  this.type = type;
4308
4728
  this.maxContextLength = maxContextLength;
4309
- this.evals = evals;
4729
+ this.queue = queue;
4310
4730
  this.capabilities = capabilities || {
4311
4731
  text: false,
4312
4732
  images: [],
@@ -4405,7 +4825,7 @@ var ExuluAgent2 = class {
4405
4825
  prompt,
4406
4826
  user,
4407
4827
  session,
4408
- message,
4828
+ inputMessages,
4409
4829
  currentTools,
4410
4830
  allExuluTools,
4411
4831
  statistics,
@@ -4423,10 +4843,10 @@ var ExuluAgent2 = class {
4423
4843
  if (!this.config) {
4424
4844
  throw new Error("Config is required for generating.");
4425
4845
  }
4426
- if (prompt && message) {
4846
+ if (prompt && inputMessages?.length) {
4427
4847
  throw new Error("Message and prompt cannot be provided at the same time.");
4428
4848
  }
4429
- if (!prompt && !message) {
4849
+ if (!prompt && !inputMessages?.length) {
4430
4850
  throw new Error("Prompt or message is required for generating.");
4431
4851
  }
4432
4852
  if (outputSchema && !prompt) {
@@ -4436,18 +4856,18 @@ var ExuluAgent2 = class {
4436
4856
  apiKey: providerapikey
4437
4857
  });
4438
4858
  console.log("[EXULU] Model for agent: " + this.name, " created for generating sync.");
4439
- let messages = [];
4440
- if (message && session && user) {
4859
+ let messages = inputMessages || [];
4860
+ if (messages && session && user) {
4441
4861
  const previousMessages = await getAgentMessages({
4442
4862
  session,
4443
4863
  user: user.id,
4444
4864
  limit: 50,
4445
4865
  page: 1
4446
4866
  });
4447
- const previousMessagesContent = previousMessages.map((message2) => JSON.parse(message2.content));
4867
+ const previousMessagesContent = previousMessages.map((message) => JSON.parse(message.content));
4448
4868
  messages = await validateUIMessages({
4449
4869
  // append the new message to the previous messages:
4450
- messages: [...previousMessagesContent, message]
4870
+ messages: [...previousMessagesContent, ...messages]
4451
4871
  });
4452
4872
  }
4453
4873
  console.log("[EXULU] Message count for agent: " + this.name, "loaded for generating sync.", messages.length);
@@ -4583,13 +5003,12 @@ var ExuluAgent2 = class {
4583
5003
  return "";
4584
5004
  };
4585
5005
  generateStream = async ({
4586
- express: express3,
4587
5006
  user,
4588
5007
  session,
4589
5008
  message,
5009
+ previousMessages,
4590
5010
  currentTools,
4591
5011
  allExuluTools,
4592
- statistics,
4593
5012
  toolConfigs,
4594
5013
  providerapikey,
4595
5014
  contexts,
@@ -4597,27 +5016,34 @@ var ExuluAgent2 = class {
4597
5016
  instructions
4598
5017
  }) => {
4599
5018
  if (!this.model) {
5019
+ console.error("[EXULU] Model is required for streaming.");
4600
5020
  throw new Error("Model is required for streaming.");
4601
5021
  }
4602
5022
  if (!this.config) {
5023
+ console.error("[EXULU] Config is required for streaming.");
4603
5024
  throw new Error("Config is required for generating.");
4604
5025
  }
4605
5026
  if (!message) {
5027
+ console.error("[EXULU] Message is required for streaming.");
4606
5028
  throw new Error("Message is required for streaming.");
4607
5029
  }
4608
5030
  const model = this.model.create({
4609
5031
  apiKey: providerapikey
4610
5032
  });
4611
5033
  let messages = [];
4612
- const previousMessages = await getAgentMessages({
4613
- session,
4614
- user: user.id,
4615
- limit: 50,
4616
- page: 1
4617
- });
4618
- const previousMessagesContent = previousMessages.map(
4619
- (message2) => JSON.parse(message2.content)
4620
- );
5034
+ let previousMessagesContent = previousMessages || [];
5035
+ if (session) {
5036
+ console.log("[EXULU] loading previous messages from session: " + session);
5037
+ const previousMessages2 = await getAgentMessages({
5038
+ session,
5039
+ user: user.id,
5040
+ limit: 50,
5041
+ page: 1
5042
+ });
5043
+ previousMessagesContent = previousMessages2.map(
5044
+ (message2) => JSON.parse(message2.content)
5045
+ );
5046
+ }
4621
5047
  messages = await validateUIMessages({
4622
5048
  // append the new message to the previous messages:
4623
5049
  messages: [...previousMessagesContent, message]
@@ -4650,80 +5076,17 @@ var ExuluAgent2 = class {
4650
5076
  user,
4651
5077
  exuluConfig
4652
5078
  ),
4653
- onError: (error) => console.error("[EXULU] chat stream error.", error)
4654
- // stopWhen: [stepCountIs(1)],
4655
- });
4656
- result.consumeStream();
4657
- result.pipeUIMessageStreamToResponse(express3.res, {
4658
- messageMetadata: ({ part }) => {
4659
- if (part.type === "finish") {
4660
- return {
4661
- totalTokens: part.totalUsage.totalTokens,
4662
- reasoningTokens: part.totalUsage.reasoningTokens,
4663
- inputTokens: part.totalUsage.inputTokens,
4664
- outputTokens: part.totalUsage.outputTokens,
4665
- cachedInputTokens: part.totalUsage.cachedInputTokens
4666
- };
4667
- }
4668
- },
4669
- originalMessages: messages,
4670
- sendReasoning: true,
4671
- sendSources: true,
4672
5079
  onError: (error) => {
4673
- console.error("[EXULU] chat response error.", error);
4674
- return errorHandler(error);
4675
- },
4676
- generateMessageId: createIdGenerator({
4677
- prefix: "msg_",
4678
- size: 16
4679
- }),
4680
- onFinish: async ({ messages: messages2, isContinuation, isAborted, responseMessage }) => {
4681
- if (session) {
4682
- await saveChat({
4683
- session,
4684
- user: user.id,
4685
- messages: messages2.filter((x) => !previousMessagesContent.find((y) => y.id === x.id))
4686
- });
4687
- }
4688
- const metadata = messages2[messages2.length - 1]?.metadata;
4689
- console.log("[EXULU] Finished streaming", metadata);
4690
- console.log("[EXULU] Statistics", statistics);
4691
- if (statistics) {
4692
- await Promise.all([
4693
- updateStatistic({
4694
- name: "count",
4695
- label: statistics.label,
4696
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4697
- trigger: statistics.trigger,
4698
- count: 1,
4699
- user: user.id,
4700
- role: user?.role?.id
4701
- }),
4702
- ...metadata?.inputTokens ? [
4703
- updateStatistic({
4704
- name: "inputTokens",
4705
- label: statistics.label,
4706
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4707
- trigger: statistics.trigger,
4708
- count: metadata?.inputTokens,
4709
- user: user.id,
4710
- role: user?.role?.id
4711
- })
4712
- ] : [],
4713
- ...metadata?.outputTokens ? [
4714
- updateStatistic({
4715
- name: "outputTokens",
4716
- label: statistics.label,
4717
- type: STATISTICS_TYPE_ENUM.AGENT_RUN,
4718
- trigger: statistics.trigger,
4719
- count: metadata?.outputTokens
4720
- })
4721
- ] : []
4722
- ]);
4723
- }
5080
+ console.error("[EXULU] chat stream error.", error);
5081
+ throw new Error(`Chat stream error: ${error instanceof Error ? error.message : String(error)}`);
4724
5082
  }
5083
+ // stopWhen: [stepCountIs(1)],
4725
5084
  });
4726
- return;
5085
+ return {
5086
+ stream: result,
5087
+ originalMessages: messages,
5088
+ previousMessages: previousMessagesContent
5089
+ };
4727
5090
  };
4728
5091
  };
4729
5092
  var getAgentMessages = async ({ session, user, limit, page }) => {
@@ -4914,16 +5277,21 @@ var ExuluContext = class {
4914
5277
  if (queue?.queue.name) {
4915
5278
  console.log("[EXULU] processor is in queue mode, scheduling job.");
4916
5279
  const job = await bullmqDecorator({
5280
+ timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 180,
4917
5281
  label: `${this.name} ${field.name} data processor`,
4918
5282
  processor: `${this.id}-${field.name}`,
4919
5283
  context: this.id,
4920
5284
  inputs: item,
4921
5285
  item: item.id,
4922
5286
  queue: queue.queue,
5287
+ backoff: queue.backoff || {
5288
+ type: "exponential",
5289
+ delay: 2e3
5290
+ },
5291
+ retries: queue.retries || 2,
4923
5292
  user,
4924
5293
  role,
4925
- trigger,
4926
- retries: 2
5294
+ trigger
4927
5295
  });
4928
5296
  return {
4929
5297
  result: "",
@@ -4993,12 +5361,14 @@ var ExuluContext = class {
4993
5361
  role
4994
5362
  );
4995
5363
  await db3.from(getChunksTableName(this.id)).where({ source }).delete();
4996
- await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
4997
- source,
4998
- content: chunk.content,
4999
- chunk_index: chunk.index,
5000
- embedding: pgvector2.toSql(chunk.vector)
5001
- })));
5364
+ if (chunks?.length) {
5365
+ await db3.from(getChunksTableName(this.id)).insert(chunks.map((chunk) => ({
5366
+ source,
5367
+ content: chunk.content,
5368
+ chunk_index: chunk.index,
5369
+ embedding: pgvector2.toSql(chunk.vector)
5370
+ })));
5371
+ }
5002
5372
  await db3.from(getTableName(this.id)).where({ id: item.id }).update({
5003
5373
  embeddings_updated_at: (/* @__PURE__ */ new Date()).toISOString()
5004
5374
  }).returning("id");
@@ -5127,9 +5497,15 @@ var ExuluContext = class {
5127
5497
  if (queue?.queue.name) {
5128
5498
  console.log("[EXULU] embedder is in queue mode, scheduling job.");
5129
5499
  const job = await bullmqDecorator({
5500
+ timeoutInSeconds: queue.timeoutInSeconds || 180,
5130
5501
  label: `${this.embedder.name}`,
5131
5502
  embedder: this.embedder.id,
5132
5503
  context: this.id,
5504
+ backoff: queue.backoff || {
5505
+ type: "exponential",
5506
+ delay: 2e3
5507
+ },
5508
+ retries: queue.retries || 2,
5133
5509
  inputs: item,
5134
5510
  item: item.id,
5135
5511
  queue: queue.queue,
@@ -5381,6 +5757,8 @@ var ExuluQueues = class {
5381
5757
  constructor() {
5382
5758
  this.queues = [];
5383
5759
  }
5760
+ list = /* @__PURE__ */ new Map();
5761
+ // list of queue names
5384
5762
  queue(name) {
5385
5763
  return this.queues.find((x) => x.queue?.name === name);
5386
5764
  }
@@ -5392,40 +5770,54 @@ var ExuluQueues = class {
5392
5770
  // method of ExuluQueues we need to store the desired rate limit on the queue
5393
5771
  // here so we can use the value when creating workers for the queue instance
5394
5772
  // as there is no way to store a rate limit value natively on a bullm queue.
5395
- use = async (name, concurrency = 1, ratelimit = 1) => {
5396
- const existing = this.queues.find((x) => x.queue?.name === name);
5397
- if (existing) {
5398
- const globalConcurrency = await existing.queue.getGlobalConcurrency();
5399
- if (globalConcurrency !== concurrency) {
5400
- await existing.queue.setGlobalConcurrency(concurrency);
5773
+ register = (name, concurrency = 1, ratelimit = 1) => {
5774
+ const use = async () => {
5775
+ const existing = this.queues.find((x) => x.queue?.name === name);
5776
+ if (existing) {
5777
+ const globalConcurrency = await existing.queue.getGlobalConcurrency();
5778
+ if (globalConcurrency !== concurrency) {
5779
+ await existing.queue.setGlobalConcurrency(concurrency);
5780
+ }
5781
+ return {
5782
+ queue: existing.queue,
5783
+ ratelimit,
5784
+ concurrency
5785
+ };
5401
5786
  }
5402
- return {
5403
- queue: existing.queue,
5404
- ratelimit,
5405
- concurrency
5406
- };
5407
- }
5408
- if (!redisServer.host?.length || !redisServer.port?.length) {
5409
- 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() ).`);
5410
- throw new Error(`[EXULU] no redis server configured.`);
5411
- }
5412
- const newQueue = new Queue3(
5413
- `${name}`,
5414
- {
5415
- connection: redisServer,
5416
- telemetry: new BullMQOtel("simple-guide")
5787
+ if (!redisServer.host?.length || !redisServer.port?.length) {
5788
+ 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() ).`);
5789
+ throw new Error(`[EXULU] no redis server configured.`);
5417
5790
  }
5418
- );
5419
- await newQueue.setGlobalConcurrency(concurrency);
5420
- this.queues.push({
5421
- queue: newQueue,
5791
+ const newQueue = new Queue3(
5792
+ `${name}`,
5793
+ {
5794
+ connection: {
5795
+ ...redisServer,
5796
+ enableOfflineQueue: false
5797
+ },
5798
+ telemetry: new BullMQOtel("simple-guide")
5799
+ }
5800
+ );
5801
+ await newQueue.setGlobalConcurrency(concurrency);
5802
+ this.queues.push({
5803
+ queue: newQueue,
5804
+ ratelimit,
5805
+ concurrency
5806
+ });
5807
+ return {
5808
+ queue: newQueue,
5809
+ ratelimit,
5810
+ concurrency
5811
+ };
5812
+ };
5813
+ this.list.set(name, {
5814
+ name,
5815
+ concurrency,
5422
5816
  ratelimit,
5423
- concurrency
5817
+ use
5424
5818
  });
5425
5819
  return {
5426
- queue: newQueue,
5427
- ratelimit,
5428
- concurrency
5820
+ use
5429
5821
  };
5430
5822
  };
5431
5823
  };
@@ -5444,8 +5836,6 @@ import OpenAI from "openai";
5444
5836
  import fs from "fs";
5445
5837
  import { randomUUID as randomUUID3 } from "crypto";
5446
5838
  import "@opentelemetry/api";
5447
- import { jsonSchema } from "ai";
5448
- import proxy from "express-http-proxy";
5449
5839
  import Anthropic from "@anthropic-ai/sdk";
5450
5840
 
5451
5841
  // src/registry/utils/claude-messages.ts
@@ -5471,13 +5861,15 @@ var CLAUDE_MESSAGES = {
5471
5861
  };
5472
5862
 
5473
5863
  // src/registry/routes.ts
5864
+ import { createIdGenerator as createIdGenerator2 } from "ai";
5474
5865
  var REQUEST_SIZE_LIMIT = "50mb";
5475
5866
  var global_queues = {
5476
- logs_cleaner: "logs-cleaner"
5867
+ eval_runs: "eval_runs"
5477
5868
  };
5478
5869
  var {
5479
5870
  agentsSchema: agentsSchema2,
5480
5871
  projectsSchema: projectsSchema2,
5872
+ jobResultsSchema: jobResultsSchema2,
5481
5873
  testCasesSchema: testCasesSchema2,
5482
5874
  evalSetsSchema: evalSetsSchema2,
5483
5875
  evalRunsSchema: evalRunsSchema2,
@@ -5491,38 +5883,7 @@ var {
5491
5883
  rbacSchema: rbacSchema2,
5492
5884
  statisticsSchema: statisticsSchema2
5493
5885
  } = coreSchemas.get();
5494
- var createRecurringJobs = async () => {
5495
- console.log("[EXULU] creating recurring jobs.");
5496
- const recurringJobSchedulersLogs = [];
5497
- const queue = await queues.use(global_queues.logs_cleaner);
5498
- recurringJobSchedulersLogs.push({
5499
- name: global_queues.logs_cleaner,
5500
- pattern: "0 10 * * * *",
5501
- ttld: "30 days",
5502
- opts: {
5503
- backoff: 3,
5504
- attempts: 5,
5505
- removeOnFail: 1e3
5506
- }
5507
- });
5508
- await queue.queue.upsertJobScheduler(
5509
- "logs-cleaner-scheduler",
5510
- { pattern: "0 10 * * * *" },
5511
- // every 10 minutes
5512
- {
5513
- name: global_queues.logs_cleaner,
5514
- data: { ttld: 30 },
5515
- // time to live in days
5516
- opts: {
5517
- backoff: 3,
5518
- attempts: 5,
5519
- removeOnFail: 1e3
5520
- }
5521
- }
5522
- );
5523
- return queue;
5524
- };
5525
- var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) => {
5886
+ var createExpressRoutes = async (app, agents, tools, contexts, config, evals, tracer, queues3) => {
5526
5887
  var corsOptions = {
5527
5888
  origin: "*",
5528
5889
  exposedHeaders: "*",
@@ -5541,19 +5902,15 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5541
5902
  \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
5542
5903
  \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
5543
5904
  \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
5544
- Intelligence Management Platform
5905
+ Intelligence Management Platform - Server
5545
5906
 
5546
5907
  `);
5547
- if (redisServer.host?.length && redisServer.port?.length) {
5548
- await createRecurringJobs();
5549
- } else {
5550
- console.log("[EXULU] no redis server configured, not setting up recurring jobs.");
5551
- }
5552
5908
  const schema = createSDL([
5553
5909
  usersSchema2(),
5554
5910
  rolesSchema2(),
5555
5911
  agentsSchema2(),
5556
5912
  projectsSchema2(),
5913
+ jobResultsSchema2(),
5557
5914
  evalRunsSchema2(),
5558
5915
  platformConfigurationsSchema2(),
5559
5916
  evalSetsSchema2(),
@@ -5564,7 +5921,7 @@ var createExpressRoutes = async (app, agents, tools, contexts, config, tracer) =
5564
5921
  workflowTemplatesSchema2(),
5565
5922
  statisticsSchema2(),
5566
5923
  rbacSchema2()
5567
- ], contexts ?? [], agents, tools, config);
5924
+ ], contexts ?? [], agents, tools, config, evals, queues3 || []);
5568
5925
  const server = new ApolloServer({
5569
5926
  cache: new InMemoryLRUCache(),
5570
5927
  schema,
@@ -5695,103 +6052,6 @@ Mood: friendly and intelligent.
5695
6052
  image: `${process.env.BACKEND}/${uuid}.png`
5696
6053
  });
5697
6054
  });
5698
- app.post("/evals/run/:id", async (req, res) => {
5699
- console.log("[EXULU] /evals/run/:id", req.params.id);
5700
- const authenticationResult = await requestValidators.authenticate(req);
5701
- if (!authenticationResult.user?.id) {
5702
- res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
5703
- return;
5704
- }
5705
- const user = authenticationResult.user;
5706
- const evalRunId = req.params.id;
5707
- if (!user.super_admin && (!user.role || user.role.evals !== "write")) {
5708
- res.status(403).json({
5709
- message: "You don't have permission to run evals. Required: super_admin or evals write access."
5710
- });
5711
- return;
5712
- }
5713
- const { db: db3 } = await postgresClient();
5714
- const evalRun = await db3.from("eval_runs").where({ id: evalRunId }).first();
5715
- if (!evalRun) {
5716
- res.status(404).json({
5717
- message: "Eval run not found."
5718
- });
5719
- return;
5720
- }
5721
- const hasAccessToEvalRun = await checkRecordAccess(evalRun, "write", user);
5722
- if (!hasAccessToEvalRun) {
5723
- res.status(403).json({
5724
- message: "You don't have access to this eval run."
5725
- });
5726
- return;
5727
- }
5728
- const testCaseIds = evalRun.test_case_ids ? typeof evalRun.test_case_ids === "string" ? JSON.parse(evalRun.test_case_ids) : evalRun.test_case_ids : [];
5729
- const evalFunctionIds = evalRun.eval_function_ids ? typeof evalRun.eval_function_ids === "string" ? JSON.parse(evalRun.eval_function_ids) : evalRun.eval_function_ids : [];
5730
- if (!testCaseIds || testCaseIds.length === 0) {
5731
- res.status(400).json({
5732
- message: "No test cases selected for this eval run."
5733
- });
5734
- return;
5735
- }
5736
- if (!evalFunctionIds || evalFunctionIds.length === 0) {
5737
- res.status(400).json({
5738
- message: "No eval functions selected for this eval run."
5739
- });
5740
- return;
5741
- }
5742
- const testCases = await db3.from("test_cases").whereIn("id", testCaseIds);
5743
- if (testCases.length === 0) {
5744
- res.status(404).json({
5745
- message: "No test cases found."
5746
- });
5747
- return;
5748
- }
5749
- const agentInstance = await loadAgent(evalRun.agentId);
5750
- if (!agentInstance) {
5751
- res.status(404).json({
5752
- message: "Agent instance not found."
5753
- });
5754
- return;
5755
- }
5756
- const evalQueue = await queues.use("evals");
5757
- const jobIds = [];
5758
- for (const testCase of testCases) {
5759
- const existingJobs = await evalQueue.queue.getJobs(["waiting", "active", "delayed", "paused"]);
5760
- const duplicateJob = existingJobs.find(
5761
- (job2) => job2.data.evalRunId === evalRunId && job2.data.testCaseId === testCase.id && job2.data.type === "eval"
5762
- );
5763
- if (duplicateJob) {
5764
- console.log(`[EXULU] Skipping duplicate job for eval run ${evalRunId} and test case ${testCase.id}`);
5765
- continue;
5766
- }
5767
- const job = await evalQueue.queue.add(`eval-${testCase.id}`, {
5768
- type: "eval",
5769
- evalRunId,
5770
- testCaseId: testCase.id,
5771
- evalFunctionIds,
5772
- // Array of eval function IDs - worker will create child jobs for these
5773
- agentId: evalRun.agentId,
5774
- inputs: testCase.inputs,
5775
- expected_output: testCase.expected_output,
5776
- expected_tools: testCase.expected_tools,
5777
- expected_knowledge_sources: testCase.expected_knowledge_sources,
5778
- expected_agent_tools: testCase.expected_agent_tools,
5779
- config: evalRun.config,
5780
- scoring_method: evalRun.scoring_method,
5781
- pass_threshold: evalRun.pass_threshold,
5782
- user: user.id,
5783
- role: user.role?.id
5784
- });
5785
- jobIds.push(job.id);
5786
- }
5787
- res.status(200).json({
5788
- message: `Created ${jobIds.length} eval jobs.`,
5789
- jobIds,
5790
- evalRunId,
5791
- testCaseCount: testCases.length,
5792
- evalFunctionCount: evalFunctionIds.length
5793
- });
5794
- });
5795
6055
  app.get("/ping", async (req, res) => {
5796
6056
  const authenticationResult = await requestValidators.authenticate(req);
5797
6057
  if (!authenticationResult.user?.id) {
@@ -5920,11 +6180,11 @@ Mood: friendly and intelligent.
5920
6180
  providerapikey = bytes.toString(CryptoJS3.enc.Utf8);
5921
6181
  }
5922
6182
  if (!!headers.stream) {
5923
- await agent.generateStream({
5924
- express: {
5925
- res,
5926
- req
5927
- },
6183
+ const statistics = {
6184
+ label: agent.name,
6185
+ trigger: "agent"
6186
+ };
6187
+ const result = await agent.generateStream({
5928
6188
  contexts,
5929
6189
  user,
5930
6190
  instructions: agentInstance.instructions,
@@ -5934,10 +6194,79 @@ Mood: friendly and intelligent.
5934
6194
  allExuluTools: tools,
5935
6195
  providerapikey,
5936
6196
  toolConfigs: agentInstance.tools,
5937
- exuluConfig: config,
5938
- statistics: {
5939
- label: agent.name,
5940
- trigger: "agent"
6197
+ exuluConfig: config
6198
+ });
6199
+ result.stream.consumeStream();
6200
+ result.stream.pipeUIMessageStreamToResponse(res, {
6201
+ messageMetadata: ({ part }) => {
6202
+ if (part.type === "finish") {
6203
+ return {
6204
+ totalTokens: part.totalUsage.totalTokens,
6205
+ reasoningTokens: part.totalUsage.reasoningTokens,
6206
+ inputTokens: part.totalUsage.inputTokens,
6207
+ outputTokens: part.totalUsage.outputTokens,
6208
+ cachedInputTokens: part.totalUsage.cachedInputTokens
6209
+ };
6210
+ }
6211
+ },
6212
+ originalMessages: result.originalMessages,
6213
+ sendReasoning: true,
6214
+ sendSources: true,
6215
+ onError: (error) => {
6216
+ console.error("[EXULU] chat response error.", error);
6217
+ return errorHandler(error);
6218
+ },
6219
+ generateMessageId: createIdGenerator2({
6220
+ prefix: "msg_",
6221
+ size: 16
6222
+ }),
6223
+ onFinish: async ({ messages, isContinuation, isAborted, responseMessage }) => {
6224
+ if (headers.session) {
6225
+ await saveChat({
6226
+ session: headers.session,
6227
+ user: user.id,
6228
+ messages: messages.filter((x) => !result.previousMessages.find((y) => y.id === x.id))
6229
+ });
6230
+ }
6231
+ const metadata = messages[messages.length - 1]?.metadata;
6232
+ console.log("[EXULU] Finished streaming", metadata);
6233
+ console.log("[EXULU] Statistics", {
6234
+ label: agent.name,
6235
+ trigger: "agent"
6236
+ });
6237
+ if (statistics) {
6238
+ await Promise.all([
6239
+ updateStatistic({
6240
+ name: "count",
6241
+ label: statistics.label,
6242
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6243
+ trigger: statistics.trigger,
6244
+ count: 1,
6245
+ user: user.id,
6246
+ role: user?.role?.id
6247
+ }),
6248
+ ...metadata?.inputTokens ? [
6249
+ updateStatistic({
6250
+ name: "inputTokens",
6251
+ label: statistics.label,
6252
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6253
+ trigger: statistics.trigger,
6254
+ count: metadata?.inputTokens,
6255
+ user: user.id,
6256
+ role: user?.role?.id
6257
+ })
6258
+ ] : [],
6259
+ ...metadata?.outputTokens ? [
6260
+ updateStatistic({
6261
+ name: "outputTokens",
6262
+ label: statistics.label,
6263
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
6264
+ trigger: statistics.trigger,
6265
+ count: metadata?.outputTokens
6266
+ })
6267
+ ] : []
6268
+ ]);
6269
+ }
5941
6270
  }
5942
6271
  });
5943
6272
  return;
@@ -5946,7 +6275,7 @@ Mood: friendly and intelligent.
5946
6275
  user,
5947
6276
  instructions: agentInstance.instructions,
5948
6277
  session: headers.session,
5949
- message: req.body.message,
6278
+ inputMessages: [req.body.message],
5950
6279
  contexts,
5951
6280
  currentTools: enabledTools,
5952
6281
  allExuluTools: tools,
@@ -5968,91 +6297,6 @@ Mood: friendly and intelligent.
5968
6297
  } else {
5969
6298
  console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
5970
6299
  }
5971
- app.use("/xxx/anthropic/:id", express.raw({ type: "*/*", limit: REQUEST_SIZE_LIMIT }), proxy(
5972
- (req, res, next) => {
5973
- return "https://api.anthropic.com";
5974
- },
5975
- {
5976
- limit: "50mb",
5977
- memoizeHost: false,
5978
- preserveHostHdr: true,
5979
- secure: false,
5980
- reqAsBuffer: true,
5981
- proxyReqBodyDecorator: function(bodyContent, srcReq) {
5982
- return bodyContent;
5983
- },
5984
- userResDecorator: function(proxyRes, proxyResData, userReq, userRes) {
5985
- console.log("[EXULU] Proxy response!", proxyResData);
5986
- proxyResData = proxyResData.toString();
5987
- console.log("[EXULU] Proxy response string!", proxyResData);
5988
- return proxyResData;
5989
- },
5990
- proxyReqPathResolver: (req) => {
5991
- const prefix = `/gateway/anthropic/${req.params.id}`;
5992
- let path = req.url.startsWith(prefix) ? req.url.slice(prefix.length) : req.url;
5993
- if (!path.startsWith("/")) path = "/" + path;
5994
- console.log("[EXULU] Provider path:", path);
5995
- return path;
5996
- },
5997
- proxyReqOptDecorator: function(proxyReqOpts, srcReq) {
5998
- return new Promise(async (resolve, reject) => {
5999
- try {
6000
- const authenticationResult = await requestValidators.authenticate(srcReq);
6001
- if (!authenticationResult.user?.id) {
6002
- console.log("[EXULU] failed authentication result", authenticationResult);
6003
- reject(authenticationResult.message);
6004
- return;
6005
- }
6006
- console.log("[EXULU] Authenticated call", authenticationResult.user?.email);
6007
- const { db: db3 } = await postgresClient();
6008
- let query = db3("agents");
6009
- query.select("*");
6010
- query = applyAccessControl(agentsSchema2(), authenticationResult.user, query);
6011
- query.where({ id: srcReq.params.id });
6012
- const agent = await query.first();
6013
- if (!agent) {
6014
- reject(new Error("Agent with id " + srcReq.params.id + " not found."));
6015
- return;
6016
- }
6017
- console.log("[EXULU] Agent loaded", agent.name);
6018
- const backend = agents.find((x) => x.id === agent.backend);
6019
- if (!process.env.NEXTAUTH_SECRET) {
6020
- reject(new Error("Missing NEXTAUTH_SECRET"));
6021
- return;
6022
- }
6023
- if (!agent.providerapikey) {
6024
- reject(new Error("API Key not set for agent"));
6025
- return;
6026
- }
6027
- const variableName = agent.providerapikey;
6028
- const variable = await db3.from("variables").where({ name: variableName }).first();
6029
- console.log("[EXULU] Variable loaded", variable);
6030
- let providerapikey = variable.value;
6031
- if (!variable.encrypted) {
6032
- reject(new Error("API Key not encrypted for agent"));
6033
- return;
6034
- }
6035
- if (variable.encrypted) {
6036
- const bytes = CryptoJS3.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6037
- providerapikey = bytes.toString(CryptoJS3.enc.Utf8);
6038
- }
6039
- console.log("[EXULU] Provider API key", providerapikey);
6040
- proxyReqOpts.headers["x-api-key"] = providerapikey;
6041
- proxyReqOpts.rejectUnauthorized = false;
6042
- delete proxyReqOpts.headers["provider"];
6043
- const url = new URL("https://api.anthropic.com");
6044
- proxyReqOpts.headers["host"] = url.host;
6045
- proxyReqOpts.headers["anthropic-version"] = "2023-06-01";
6046
- console.log("[EXULU] Proxy request headers", proxyReqOpts.headers);
6047
- resolve(proxyReqOpts);
6048
- } catch (error) {
6049
- console.error("[EXULU] Proxy error", error);
6050
- reject(error);
6051
- }
6052
- });
6053
- }
6054
- }
6055
- ));
6056
6300
  app.get("/config", async (req, res) => {
6057
6301
  res.status(200).json({
6058
6302
  message: "Config fetched successfully.",
@@ -6224,10 +6468,36 @@ var createCustomAnthropicStreamingMessage = (message) => {
6224
6468
 
6225
6469
  // src/registry/workers.ts
6226
6470
  import IORedis from "ioredis";
6227
- import { Worker } from "bullmq";
6471
+ import { Job, Worker } from "bullmq";
6228
6472
  import "@opentelemetry/api";
6473
+ import { v4 as uuidv43 } from "uuid";
6474
+ import "ai";
6475
+ import CryptoJS4 from "crypto-js";
6476
+
6477
+ // src/registry/log-metadata.ts
6478
+ function logMetadata(id, additionalMetadata) {
6479
+ return {
6480
+ __logMetadata: true,
6481
+ id,
6482
+ ...additionalMetadata
6483
+ };
6484
+ }
6485
+
6486
+ // src/registry/workers.ts
6229
6487
  var redisConnection;
6230
- var createWorkers = async (queues2, config, contexts, tracer) => {
6488
+ var createWorkers = async (agents, queues3, config, contexts, evals, tools, tracer) => {
6489
+ console.log(`
6490
+ \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
6491
+ \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
6492
+ \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
6493
+ \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
6494
+ \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
6495
+ \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
6496
+ Intelligence Management Platform - Workers
6497
+
6498
+ `);
6499
+ console.log("[EXULU] creating workers for " + queues3?.length + " queues.");
6500
+ console.log("[EXULU] queues", queues3.map((q) => q.queue.name));
6231
6501
  if (!redisServer.host || !redisServer.port) {
6232
6502
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
6233
6503
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -6235,80 +6505,333 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6235
6505
  if (!redisConnection) {
6236
6506
  redisConnection = new IORedis({
6237
6507
  ...redisServer,
6508
+ enableOfflineQueue: true,
6509
+ retryStrategy: function(times) {
6510
+ return Math.max(Math.min(Math.exp(times), 2e4), 1e3);
6511
+ },
6238
6512
  maxRetriesPerRequest: null
6239
6513
  });
6240
6514
  }
6241
- const workers = queues2.map((queue) => {
6242
- console.log(`[EXULU] creating worker for queue ${queue}.`);
6515
+ const workers = queues3.map((queue) => {
6516
+ console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
6243
6517
  const worker = new Worker(
6244
- `${queue}`,
6518
+ `${queue.queue.name}`,
6245
6519
  async (bullmqJob) => {
6520
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
6521
+ name: bullmqJob.name,
6522
+ status: await bullmqJob.getState(),
6523
+ type: bullmqJob.data.type
6524
+ }));
6246
6525
  const { db: db3 } = await postgresClient();
6247
- try {
6248
- const data = bullmqJob.data;
6249
- bullmq.validate(bullmqJob.id, data);
6250
- if (data.type === "embedder") {
6251
- const context = contexts.find((context2) => context2.id === data.context);
6252
- if (!context) {
6253
- throw new Error(`Context ${data.context} not found in the registry.`);
6254
- }
6255
- if (!data.embedder) {
6256
- throw new Error(`No embedder set for embedder job.`);
6257
- }
6258
- const embedder = contexts.find((context2) => context2.embedder?.id === data.embedder);
6259
- if (!embedder) {
6260
- throw new Error(`Embedder ${data.embedder} not found in the registry.`);
6261
- }
6262
- const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
6263
- label: embedder.name,
6264
- trigger: data.trigger
6265
- }, data.role, bullmqJob.id);
6266
- return result;
6267
- }
6268
- if (data.type === "processor") {
6269
- const context = contexts.find((context2) => context2.id === data.context);
6270
- if (!context) {
6271
- throw new Error(`Context ${data.context} not found in the registry.`);
6272
- }
6273
- const field = context.fields.find((field2) => field2.name === data.inputs.field);
6274
- if (!field) {
6275
- throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
6276
- }
6277
- if (!field.processor) {
6278
- throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
6526
+ const data = bullmqJob.data;
6527
+ const timeoutMs = data.timeoutInSeconds * 1e3;
6528
+ const timeoutPromise = new Promise((_, reject) => {
6529
+ setTimeout(() => {
6530
+ reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${data.timeoutInSeconds}s`));
6531
+ }, timeoutMs);
6532
+ });
6533
+ const workPromise = (async () => {
6534
+ try {
6535
+ bullmq.validate(bullmqJob.id, data);
6536
+ if (data.type === "embedder") {
6537
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
6538
+ const label = `embedder-${bullmqJob.name}`;
6539
+ await db3.from("job_results").insert({
6540
+ job_id: bullmqJob.id,
6541
+ label,
6542
+ state: await bullmqJob.getState(),
6543
+ result: null,
6544
+ metadata: {}
6545
+ });
6546
+ const context = contexts.find((context2) => context2.id === data.context);
6547
+ if (!context) {
6548
+ throw new Error(`Context ${data.context} not found in the registry.`);
6549
+ }
6550
+ if (!data.embedder) {
6551
+ throw new Error(`No embedder set for embedder job.`);
6552
+ }
6553
+ const embedder = contexts.find((context2) => context2.embedder?.id === data.embedder);
6554
+ if (!embedder) {
6555
+ throw new Error(`Embedder ${data.embedder} not found in the registry.`);
6556
+ }
6557
+ const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
6558
+ label: embedder.name,
6559
+ trigger: data.trigger
6560
+ }, data.role, bullmqJob.id);
6561
+ return {
6562
+ result,
6563
+ metadata: {}
6564
+ };
6279
6565
  }
6280
- const exuluStorage = new ExuluStorage({ config });
6281
- if (!data.user) {
6282
- throw new Error(`User not set for processor job.`);
6566
+ if (data.type === "processor") {
6567
+ console.log("[EXULU] running a processor job.", logMetadata(bullmqJob.name));
6568
+ const label = `processor-${bullmqJob.name}`;
6569
+ await db3.from("job_results").insert({
6570
+ job_id: bullmqJob.id,
6571
+ label,
6572
+ state: await bullmqJob.getState(),
6573
+ result: null,
6574
+ metadata: {}
6575
+ });
6576
+ const context = contexts.find((context2) => context2.id === data.context);
6577
+ if (!context) {
6578
+ throw new Error(`Context ${data.context} not found in the registry.`);
6579
+ }
6580
+ const field = context.fields.find((field2) => field2.name === data.inputs.field);
6581
+ if (!field) {
6582
+ throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
6583
+ }
6584
+ if (!field.processor) {
6585
+ throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
6586
+ }
6587
+ const exuluStorage = new ExuluStorage({ config });
6588
+ if (!data.user) {
6589
+ throw new Error(`User not set for processor job.`);
6590
+ }
6591
+ if (!data.role) {
6592
+ throw new Error(`Role not set for processor job.`);
6593
+ }
6594
+ const result = await field.processor.execute({
6595
+ item: data.inputs,
6596
+ user: data.user,
6597
+ role: data.role,
6598
+ utils: {
6599
+ storage: exuluStorage,
6600
+ items: {
6601
+ update: context.updateItem,
6602
+ create: context.createItem,
6603
+ delete: context.deleteItem
6604
+ }
6605
+ },
6606
+ config
6607
+ });
6608
+ return {
6609
+ result,
6610
+ metadata: {}
6611
+ };
6283
6612
  }
6284
- if (!data.role) {
6285
- throw new Error(`Role not set for processor job.`);
6613
+ if (data.type === "eval_run") {
6614
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
6615
+ const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
6616
+ const existingResult = await db3.from("job_results").where({ label }).first();
6617
+ if (existingResult) {
6618
+ await db3.from("job_results").where({ label }).update({
6619
+ job_id: bullmqJob.id,
6620
+ label,
6621
+ state: await bullmqJob.getState(),
6622
+ result: null,
6623
+ metadata: {},
6624
+ tries: existingResult.tries + 1
6625
+ });
6626
+ } else {
6627
+ await db3.from("job_results").insert({
6628
+ job_id: bullmqJob.id,
6629
+ label,
6630
+ state: await bullmqJob.getState(),
6631
+ result: null,
6632
+ metadata: {},
6633
+ tries: 1
6634
+ });
6635
+ }
6636
+ const {
6637
+ agentInstance,
6638
+ backend: agentBackend,
6639
+ user,
6640
+ evalRun,
6641
+ testCase,
6642
+ messages: inputMessages
6643
+ } = await validateEvalPayload(data, agents);
6644
+ const retries = 3;
6645
+ let attempts = 0;
6646
+ const promise = new Promise(async (resolve, reject) => {
6647
+ while (attempts < retries) {
6648
+ try {
6649
+ const messages2 = await processUiMessagesFlow({
6650
+ agents,
6651
+ agentInstance,
6652
+ agentBackend,
6653
+ inputMessages,
6654
+ contexts,
6655
+ user,
6656
+ tools,
6657
+ config
6658
+ });
6659
+ resolve(messages2);
6660
+ break;
6661
+ } catch (error) {
6662
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
6663
+ error: error instanceof Error ? error.message : String(error)
6664
+ }));
6665
+ attempts++;
6666
+ if (attempts >= retries) {
6667
+ reject(error);
6668
+ }
6669
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
6670
+ }
6671
+ }
6672
+ });
6673
+ const result = await promise;
6674
+ const messages = result.messages;
6675
+ const metadata = result.metadata;
6676
+ const evalFunctions = evalRun.eval_functions;
6677
+ let evalFunctionResults = [];
6678
+ for (const evalFunction of evalFunctions) {
6679
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6680
+ if (!evalMethod) {
6681
+ throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
6682
+ }
6683
+ let result2;
6684
+ if (evalMethod.queue) {
6685
+ const queue2 = await evalMethod.queue;
6686
+ const jobData = {
6687
+ ...data,
6688
+ type: "eval_function",
6689
+ eval_functions: [{
6690
+ id: evalFunction.id,
6691
+ config: evalFunction.config || {}
6692
+ }],
6693
+ // updating the input messages with the messages we want to run the eval
6694
+ // function on, which are the output messages from the agent.
6695
+ inputs: messages
6696
+ };
6697
+ const redisId = uuidv43();
6698
+ const job = await queue2.queue.add("eval_function", jobData, {
6699
+ jobId: redisId,
6700
+ // Setting it to 3 as a sensible default, as
6701
+ // many AI services are quite unstable.
6702
+ attempts: queue2.retries || 3,
6703
+ // todo make this configurable?
6704
+ removeOnComplete: 5e3,
6705
+ removeOnFail: 1e4,
6706
+ backoff: queue2.backoff || {
6707
+ type: "exponential",
6708
+ delay: 2e3
6709
+ }
6710
+ });
6711
+ if (!job.id) {
6712
+ throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
6713
+ }
6714
+ result2 = await pollJobResult({ queue: queue2, jobId: job.id });
6715
+ const evalFunctionResult = {
6716
+ test_case_id: testCase.id,
6717
+ eval_run_id: evalRun.id,
6718
+ eval_function_id: evalFunction.id,
6719
+ result: result2 || 0
6720
+ };
6721
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6722
+ result: result2 || 0
6723
+ }));
6724
+ evalFunctionResults.push(evalFunctionResult);
6725
+ } else {
6726
+ result2 = await evalMethod.run(
6727
+ agentInstance,
6728
+ agentBackend,
6729
+ testCase,
6730
+ messages,
6731
+ evalFunction.config || {}
6732
+ );
6733
+ const evalFunctionResult = {
6734
+ test_case_id: testCase.id,
6735
+ eval_run_id: evalRun.id,
6736
+ eval_function_id: evalFunction.id,
6737
+ result: result2 || 0
6738
+ };
6739
+ evalFunctionResults.push(evalFunctionResult);
6740
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
6741
+ result: result2 || 0
6742
+ }));
6743
+ }
6744
+ }
6745
+ const scores = evalFunctionResults.map((result2) => result2.result);
6746
+ let score = 0;
6747
+ switch (data.scoring_method) {
6748
+ case "median":
6749
+ score = getMedian(scores);
6750
+ break;
6751
+ case "average":
6752
+ score = getAverage(scores);
6753
+ break;
6754
+ case "sum":
6755
+ score = getSum(scores);
6756
+ break;
6757
+ }
6758
+ return {
6759
+ result: score,
6760
+ metadata: {
6761
+ ...evalFunctionResults,
6762
+ ...metadata
6763
+ }
6764
+ };
6286
6765
  }
6287
- const result = await field.processor.execute({
6288
- item: data.inputs,
6289
- user: data.user,
6290
- role: data.role,
6291
- utils: {
6292
- storage: exuluStorage,
6293
- items: {
6294
- update: context.updateItem
6766
+ if (data.type === "eval_function") {
6767
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
6768
+ if (data.eval_functions?.length !== 1) {
6769
+ throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
6770
+ }
6771
+ const label = `eval-function-${data.eval_run_id}-${data.test_case_id}-${data.eval_functions?.[0]?.id}`;
6772
+ const existingResult = await db3.from("job_results").where({ label }).first();
6773
+ if (existingResult) {
6774
+ await db3.from("job_results").where({ label }).update({
6775
+ job_id: bullmqJob.id,
6776
+ label,
6777
+ state: await bullmqJob.getState(),
6778
+ result: null,
6779
+ metadata: {},
6780
+ tries: existingResult.tries + 1
6781
+ });
6782
+ } else {
6783
+ await db3.from("job_results").insert({
6784
+ job_id: bullmqJob.id,
6785
+ label,
6786
+ state: await bullmqJob.getState(),
6787
+ result: null,
6788
+ metadata: {},
6789
+ tries: 1
6790
+ });
6791
+ }
6792
+ const {
6793
+ evalRun,
6794
+ agentInstance,
6795
+ backend,
6796
+ testCase,
6797
+ messages: inputMessages
6798
+ } = await validateEvalPayload(data, agents);
6799
+ const evalFunctions = evalRun.eval_functions;
6800
+ let result;
6801
+ for (const evalFunction of evalFunctions) {
6802
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
6803
+ if (!evalMethod) {
6804
+ throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
6295
6805
  }
6296
- },
6297
- config
6298
- });
6299
- return result;
6806
+ result = await evalMethod.run(
6807
+ agentInstance,
6808
+ backend,
6809
+ testCase,
6810
+ inputMessages,
6811
+ evalFunction.config || {}
6812
+ );
6813
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
6814
+ result: result || 0
6815
+ }));
6816
+ }
6817
+ return {
6818
+ result,
6819
+ metadata: {}
6820
+ };
6821
+ }
6822
+ throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
6823
+ } catch (error) {
6824
+ console.error(`[EXULU] job failed.`, error instanceof Error ? error.message : String(error));
6825
+ throw error;
6300
6826
  }
6301
- } catch (error) {
6302
- await db3.from("jobs").where({ redis: bullmqJob.id }).update({
6303
- status: "failed",
6304
- finishedAt: /* @__PURE__ */ new Date(),
6305
- error: error instanceof Error ? error.message : String(error)
6306
- });
6307
- throw new Error(error instanceof Error ? error.message : String(error));
6308
- }
6827
+ })();
6828
+ return Promise.race([workPromise, timeoutPromise]);
6309
6829
  },
6310
6830
  {
6831
+ autorun: true,
6311
6832
  connection: redisConnection,
6833
+ removeOnComplete: { count: 1e3 },
6834
+ removeOnFail: { count: 5e3 },
6312
6835
  ...queue.ratelimit && {
6313
6836
  limiter: {
6314
6837
  max: queue.ratelimit,
@@ -6317,22 +6840,312 @@ var createWorkers = async (queues2, config, contexts, tracer) => {
6317
6840
  }
6318
6841
  }
6319
6842
  );
6320
- worker.on("completed", (job, returnvalue) => {
6321
- console.log(`[EXULU] completed job ${job.id}.`, returnvalue);
6843
+ worker.on("completed", async (job, returnvalue) => {
6844
+ console.log(`[EXULU] completed job ${job.id}.`, logMetadata(job.name, {
6845
+ result: returnvalue
6846
+ }));
6847
+ const { db: db3 } = await postgresClient();
6848
+ await db3.from("job_results").where({ job_id: job.id }).update({
6849
+ state: JOB_STATUS_ENUM.completed,
6850
+ result: returnvalue.result,
6851
+ metadata: returnvalue.metadata
6852
+ });
6322
6853
  });
6323
- worker.on("failed", (job, error, prev) => {
6854
+ worker.on("failed", async (job, error, prev) => {
6324
6855
  if (job?.id) {
6325
- console.error(`[EXULU] failed job ${job.id}.`);
6856
+ const { db: db3 } = await postgresClient();
6857
+ console.error(`[EXULU] failed job ${job.id}.`, error);
6858
+ await db3.from("job_results").where({ job_id: job.id }).update({
6859
+ state: JOB_STATUS_ENUM.failed,
6860
+ error
6861
+ });
6862
+ return;
6326
6863
  }
6327
- console.error(`[EXULU] job error.`, error);
6864
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
6865
+ error: error instanceof Error ? error.message : String(error)
6866
+ }) : error);
6867
+ });
6868
+ worker.on("error", (error) => {
6869
+ console.error(`[EXULU] worker error.`, error);
6328
6870
  });
6329
6871
  worker.on("progress", (job, progress) => {
6330
- console.log(`[EXULU] job progress ${job.id}.`, progress);
6872
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
6873
+ progress
6874
+ }));
6331
6875
  });
6876
+ const gracefulShutdown = async (signal) => {
6877
+ console.log(`Received ${signal}, closing server...`);
6878
+ await worker.close();
6879
+ process.exit(0);
6880
+ };
6881
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
6882
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
6332
6883
  return worker;
6333
6884
  });
6334
6885
  return workers;
6335
6886
  };
6887
+ var validateEvalPayload = async (data, agents) => {
6888
+ if (!data.eval_run_id) {
6889
+ throw new Error(`No eval run ID set for eval job.`);
6890
+ }
6891
+ if (!data.test_case_id) {
6892
+ throw new Error(`No test case ID set for eval job.`);
6893
+ }
6894
+ if (!data.user) {
6895
+ throw new Error(`No user set for eval job.`);
6896
+ }
6897
+ if (!data.role) {
6898
+ throw new Error(`No role set for eval job.`);
6899
+ }
6900
+ if (!data.agent_id) {
6901
+ throw new Error(`No agent ID set for eval job.`);
6902
+ }
6903
+ if (!data.inputs?.length) {
6904
+ throw new Error(`No inputs set for eval job, expected array of UIMessage objects.`);
6905
+ }
6906
+ const { db: db3 } = await postgresClient();
6907
+ const evalRun = await db3.from("eval_runs").where({ id: data.eval_run_id }).first();
6908
+ if (!evalRun) {
6909
+ throw new Error(`Eval run ${data.eval_run_id} not found in the database.`);
6910
+ }
6911
+ const agentInstance = await loadAgent(evalRun.agent_id);
6912
+ if (!agentInstance) {
6913
+ throw new Error(`Agent ${evalRun.agent_id} not found in the database.`);
6914
+ }
6915
+ const backend = agents.find((a) => a.id === agentInstance.backend);
6916
+ if (!backend) {
6917
+ throw new Error(`Backend ${agentInstance.backend} not found in the database.`);
6918
+ }
6919
+ const user = await db3.from("users").where({ id: data.user }).first();
6920
+ if (!user) {
6921
+ throw new Error(`User ${data.user} not found in the database.`);
6922
+ }
6923
+ const testCase = await db3.from("test_cases").where({ id: data.test_case_id }).first();
6924
+ if (!testCase) {
6925
+ throw new Error(`Test case ${data.test_case_id} not found in the database.`);
6926
+ }
6927
+ return {
6928
+ agentInstance,
6929
+ backend,
6930
+ user,
6931
+ testCase,
6932
+ evalRun,
6933
+ messages: data.inputs
6934
+ };
6935
+ };
6936
+ var pollJobResult = async ({ queue, jobId }) => {
6937
+ let attempts = 0;
6938
+ let timeoutInSeconds = queue.timeoutInSeconds || 180;
6939
+ const startTime = Date.now();
6940
+ let result;
6941
+ while (true) {
6942
+ attempts++;
6943
+ const job = await Job.fromId(queue.queue, jobId);
6944
+ if (!job) {
6945
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
6946
+ continue;
6947
+ }
6948
+ const elapsedTime = Date.now() - startTime;
6949
+ if (elapsedTime > timeoutInSeconds * 1e3) {
6950
+ throw new Error(`Job ${job.id} timed out after ${timeoutInSeconds} seconds for job eval function job ${job.name}.`);
6951
+ }
6952
+ console.log(`[EXULU] polling eval function job ${job.name} for state... (attempt ${attempts})`);
6953
+ const jobState = await job.getState();
6954
+ console.log(`[EXULU] eval function job ${job.name} state: ${jobState}`);
6955
+ if (jobState === "failed") {
6956
+ throw new Error(`Job ${job.name} (${job.id}) failed with error: ${job.failedReason}.`);
6957
+ }
6958
+ if (jobState === "completed") {
6959
+ console.log(`[EXULU] eval function job ${job.name} completed, getting result from database...`);
6960
+ const { db: db3 } = await postgresClient();
6961
+ const entry = await db3.from("job_results").where({ job_id: job.id }).first();
6962
+ result = entry?.result;
6963
+ if (result === void 0 || result === null || result === "") {
6964
+ throw new Error(`Eval function ${job.id} result not found in database for job eval function job ${job.name}.`);
6965
+ }
6966
+ console.log(`[EXULU] eval function ${job.id} result: ${result}`);
6967
+ break;
6968
+ }
6969
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
6970
+ }
6971
+ return result;
6972
+ };
6973
+ var processUiMessagesFlow = async ({
6974
+ agents,
6975
+ agentInstance,
6976
+ agentBackend,
6977
+ inputMessages,
6978
+ contexts,
6979
+ user,
6980
+ tools,
6981
+ config
6982
+ }) => {
6983
+ console.log("[EXULU] processing UI messages flow for agent.");
6984
+ console.log("[EXULU] input messages", inputMessages);
6985
+ console.log("[EXULU] agent tools", agentInstance.tools?.map((x) => x.name + " (" + x.id + ")"));
6986
+ const disabledTools = [];
6987
+ let enabledTools = await getEnabledTools(agentInstance, tools, disabledTools, agents, user);
6988
+ console.log("[EXULU] enabled tools", enabledTools?.map((x) => x.name + " (" + x.id + ")"));
6989
+ const variableName = agentInstance.providerapikey;
6990
+ const { db: db3 } = await postgresClient();
6991
+ const variable = await db3.from("variables").where({ name: variableName }).first();
6992
+ if (!variable) {
6993
+ throw new Error(`Provider API key variable not found for agent ${agentInstance.name} (${agentInstance.id}).`);
6994
+ }
6995
+ let providerapikey = variable.value;
6996
+ if (!variable.encrypted) {
6997
+ 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.`);
6998
+ }
6999
+ if (variable.encrypted) {
7000
+ const bytes = CryptoJS4.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
7001
+ providerapikey = bytes.toString(CryptoJS4.enc.Utf8);
7002
+ }
7003
+ const messagesWithoutPlaceholder = inputMessages.filter(
7004
+ (message) => message.metadata?.type !== "placeholder"
7005
+ );
7006
+ console.log("[EXULU] messages without placeholder", messagesWithoutPlaceholder);
7007
+ let index = 0;
7008
+ let messageHistory = {
7009
+ messages: [],
7010
+ metadata: {
7011
+ tokens: {
7012
+ totalTokens: 0,
7013
+ reasoningTokens: 0,
7014
+ inputTokens: 0,
7015
+ outputTokens: 0,
7016
+ cachedInputTokens: 0
7017
+ },
7018
+ duration: 0
7019
+ }
7020
+ };
7021
+ for (const currentMessage of messagesWithoutPlaceholder) {
7022
+ console.log("[EXULU] running through the conversation");
7023
+ console.log("[EXULU] current index", index);
7024
+ console.log("[EXULU] current message", currentMessage);
7025
+ console.log("[EXULU] message history", messageHistory);
7026
+ const statistics = {
7027
+ label: agentInstance.name,
7028
+ trigger: "agent"
7029
+ };
7030
+ messageHistory = await new Promise(async (resolve, reject) => {
7031
+ const startTime = Date.now();
7032
+ try {
7033
+ const result = await agentBackend.generateStream({
7034
+ contexts,
7035
+ user,
7036
+ instructions: agentInstance.instructions,
7037
+ session: void 0,
7038
+ previousMessages: messageHistory.messages,
7039
+ message: currentMessage,
7040
+ currentTools: enabledTools,
7041
+ allExuluTools: tools,
7042
+ providerapikey,
7043
+ toolConfigs: agentInstance.tools,
7044
+ exuluConfig: config
7045
+ });
7046
+ console.log("[EXULU] consuming stream for agent.");
7047
+ const stream = result.stream.toUIMessageStream({
7048
+ messageMetadata: ({ part }) => {
7049
+ console.log("[EXULU] part", part.type);
7050
+ if (part.type === "finish") {
7051
+ return {
7052
+ totalTokens: part.totalUsage.totalTokens,
7053
+ reasoningTokens: part.totalUsage.reasoningTokens,
7054
+ inputTokens: part.totalUsage.inputTokens,
7055
+ outputTokens: part.totalUsage.outputTokens,
7056
+ cachedInputTokens: part.totalUsage.cachedInputTokens
7057
+ };
7058
+ }
7059
+ },
7060
+ originalMessages: result.originalMessages,
7061
+ sendReasoning: true,
7062
+ sendSources: true,
7063
+ onError: (error) => {
7064
+ console.error("[EXULU] Ui message stream error.", error);
7065
+ reject(error);
7066
+ return `Ui message stream error: ${error instanceof Error ? error.message : String(error)}`;
7067
+ },
7068
+ onFinish: async ({ messages, isContinuation, responseMessage }) => {
7069
+ const metadata = messages[messages.length - 1]?.metadata;
7070
+ console.log("[EXULU] Stream finished with messages:", messages);
7071
+ console.log("[EXULU] Stream metadata", metadata);
7072
+ await Promise.all([
7073
+ updateStatistic({
7074
+ name: "count",
7075
+ label: statistics.label,
7076
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7077
+ trigger: statistics.trigger,
7078
+ count: 1,
7079
+ user: user.id,
7080
+ role: user?.role?.id
7081
+ }),
7082
+ ...metadata?.inputTokens ? [
7083
+ updateStatistic({
7084
+ name: "inputTokens",
7085
+ label: statistics.label,
7086
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7087
+ trigger: statistics.trigger,
7088
+ count: metadata?.inputTokens,
7089
+ user: user.id,
7090
+ role: user?.role?.id
7091
+ })
7092
+ ] : [],
7093
+ ...metadata?.outputTokens ? [
7094
+ updateStatistic({
7095
+ name: "outputTokens",
7096
+ label: statistics.label,
7097
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
7098
+ trigger: statistics.trigger,
7099
+ count: metadata?.outputTokens
7100
+ })
7101
+ ] : []
7102
+ ]);
7103
+ resolve({
7104
+ messages,
7105
+ metadata: {
7106
+ tokens: {
7107
+ totalTokens: messageHistory.metadata.tokens.totalTokens + metadata?.totalTokens,
7108
+ reasoningTokens: messageHistory.metadata.tokens.reasoningTokens + metadata?.reasoningTokens,
7109
+ inputTokens: messageHistory.metadata.tokens.inputTokens + metadata?.inputTokens,
7110
+ outputTokens: messageHistory.metadata.tokens.outputTokens + metadata?.outputTokens,
7111
+ cachedInputTokens: messageHistory.metadata.tokens.cachedInputTokens + metadata?.cachedInputTokens
7112
+ },
7113
+ duration: messageHistory.metadata.duration + (Date.now() - startTime)
7114
+ }
7115
+ });
7116
+ }
7117
+ });
7118
+ for await (const message of stream) {
7119
+ console.log("[EXULU] message", message);
7120
+ }
7121
+ } catch (error) {
7122
+ console.error(`[EXULU] error generating stream for agent ${agentInstance.name} (${agentInstance.id}).`, error);
7123
+ reject(error);
7124
+ }
7125
+ });
7126
+ index++;
7127
+ }
7128
+ console.log("[EXULU] finished processing UI messages flow for agent, messages result", messageHistory);
7129
+ return messageHistory;
7130
+ };
7131
+ function getMedian(arr) {
7132
+ if (arr.length === 0) return 0;
7133
+ const sortedArr = arr.slice().sort((a, b) => a - b);
7134
+ const mid = Math.floor(sortedArr.length / 2);
7135
+ if (sortedArr.length % 2 !== 0) {
7136
+ return sortedArr[mid];
7137
+ } else {
7138
+ return (sortedArr[mid - 1] + sortedArr[mid]) / 2;
7139
+ }
7140
+ }
7141
+ function getSum(arr) {
7142
+ if (arr.length === 0) return 0;
7143
+ return arr.reduce((a, b) => a + b, 0);
7144
+ }
7145
+ function getAverage(arr) {
7146
+ if (arr.length === 0) return 0;
7147
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
7148
+ }
6336
7149
 
6337
7150
  // src/mcp/index.ts
6338
7151
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -6485,7 +7298,6 @@ var claudeOpus4Agent = new ExuluAgent2({
6485
7298
  audio: [],
6486
7299
  video: []
6487
7300
  },
6488
- evals: [],
6489
7301
  maxContextLength: 2e5,
6490
7302
  config: {
6491
7303
  name: `CLAUDE-OPUS-4`,
@@ -6520,7 +7332,6 @@ var claudeSonnet4Agent = new ExuluAgent2({
6520
7332
  audio: [],
6521
7333
  video: []
6522
7334
  },
6523
- evals: [],
6524
7335
  maxContextLength: 2e5,
6525
7336
  config: {
6526
7337
  name: `CLAUDE-SONNET-4`,
@@ -6548,7 +7359,6 @@ var claudeSonnet45Agent = new ExuluAgent2({
6548
7359
  audio: [],
6549
7360
  video: []
6550
7361
  },
6551
- evals: [],
6552
7362
  maxContextLength: 2e5,
6553
7363
  config: {
6554
7364
  name: `CLAUDE-SONNET-4.5`,
@@ -6579,7 +7389,6 @@ var gpt5proAgent = new ExuluAgent2({
6579
7389
  audio: [],
6580
7390
  video: []
6581
7391
  },
6582
- evals: [],
6583
7392
  maxContextLength: 4e5,
6584
7393
  config: {
6585
7394
  name: `GPT-5-PRO`,
@@ -6607,7 +7416,6 @@ var gpt5CodexAgent = new ExuluAgent2({
6607
7416
  audio: [],
6608
7417
  video: []
6609
7418
  },
6610
- evals: [],
6611
7419
  maxContextLength: 4e5,
6612
7420
  config: {
6613
7421
  name: `GPT-5-CODEX`,
@@ -6635,7 +7443,6 @@ var gpt5MiniAgent = new ExuluAgent2({
6635
7443
  audio: [],
6636
7444
  video: []
6637
7445
  },
6638
- evals: [],
6639
7446
  maxContextLength: 4e5,
6640
7447
  config: {
6641
7448
  name: `GPT-5-MINI`,
@@ -6670,7 +7477,6 @@ var gpt5agent = new ExuluAgent2({
6670
7477
  audio: [],
6671
7478
  video: []
6672
7479
  },
6673
- evals: [],
6674
7480
  maxContextLength: 4e5,
6675
7481
  config: {
6676
7482
  name: `GPT-5`,
@@ -6705,7 +7511,6 @@ var gpt5NanoAgent = new ExuluAgent2({
6705
7511
  audio: [],
6706
7512
  video: []
6707
7513
  },
6708
- evals: [],
6709
7514
  maxContextLength: 4e5,
6710
7515
  config: {
6711
7516
  name: `GPT-5-NANO`,
@@ -6733,7 +7538,6 @@ var gpt41Agent = new ExuluAgent2({
6733
7538
  audio: [],
6734
7539
  video: []
6735
7540
  },
6736
- evals: [],
6737
7541
  maxContextLength: 1047576,
6738
7542
  config: {
6739
7543
  name: `GPT-4.1`,
@@ -6761,7 +7565,6 @@ var gpt41MiniAgent = new ExuluAgent2({
6761
7565
  audio: [],
6762
7566
  video: []
6763
7567
  },
6764
- evals: [],
6765
7568
  maxContextLength: 1047576,
6766
7569
  config: {
6767
7570
  name: `GPT-4.1-MINI`,
@@ -6789,7 +7592,6 @@ var gpt4oAgent = new ExuluAgent2({
6789
7592
  audio: [],
6790
7593
  video: []
6791
7594
  },
6792
- evals: [],
6793
7595
  maxContextLength: 128e3,
6794
7596
  config: {
6795
7597
  name: `Default agent`,
@@ -6817,7 +7619,6 @@ var gpt4oMiniAgent = new ExuluAgent2({
6817
7619
  audio: [],
6818
7620
  video: []
6819
7621
  },
6820
- evals: [],
6821
7622
  maxContextLength: 128e3,
6822
7623
  config: {
6823
7624
  name: `GPT-4O-MINI`,
@@ -6908,6 +7709,55 @@ var outputsContext = new ExuluContext({
6908
7709
  // src/registry/index.ts
6909
7710
  import winston2 from "winston";
6910
7711
  import util from "util";
7712
+
7713
+ // src/templates/evals/index.ts
7714
+ import { z as z3 } from "zod";
7715
+ var llmAsJudgeEval = new ExuluEval2({
7716
+ id: "llm_as_judge",
7717
+ name: "LLM as Judge",
7718
+ description: "Evaluate the output of the LLM as a judge.",
7719
+ execute: async ({ agent, backend, messages, testCase, config }) => {
7720
+ console.log("[EXULU] running llm as judge eval", { agent, backend, messages, testCase, config });
7721
+ let prompt = config?.prompt;
7722
+ if (!prompt) {
7723
+ console.error("[EXULU] prompt is required.");
7724
+ throw new Error("Prompt is required.");
7725
+ }
7726
+ const lastMessage = messages[messages.length - 1]?.parts?.filter((part) => part.type === "text").map((part) => part.text).join("\n");
7727
+ console.log("[EXULU] last message", lastMessage);
7728
+ if (!lastMessage) {
7729
+ return 0;
7730
+ }
7731
+ prompt = prompt.replace("{actual_output}", lastMessage);
7732
+ prompt = prompt.replace("{expected_output}", testCase.expected_output);
7733
+ if (!agent.providerapikey) {
7734
+ throw new Error(`Provider API key for agent ${agent.name} is required, variable name is ${agent.providerapikey} but it is not set.`);
7735
+ }
7736
+ const providerapikey = await ExuluVariables.get(agent.providerapikey);
7737
+ console.log("[EXULU] prompt", prompt);
7738
+ const response = await backend.generateSync({
7739
+ prompt,
7740
+ outputSchema: z3.object({
7741
+ score: z3.number().min(0).max(100).describe("The score between 0 and 100.")
7742
+ }),
7743
+ providerapikey
7744
+ });
7745
+ console.log("[EXULU] response", response);
7746
+ const score = parseFloat(response.score);
7747
+ if (isNaN(score)) {
7748
+ throw new Error(`Generated score from llm as a judge eval is not a number: ${response.score}`);
7749
+ }
7750
+ return score;
7751
+ },
7752
+ config: [{
7753
+ name: "prompt",
7754
+ 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."
7755
+ }],
7756
+ queue: queues.register("llm_as_judge", 1, 1).use(),
7757
+ llm: true
7758
+ });
7759
+
7760
+ // src/registry/index.ts
6911
7761
  var isDev = process.env.NODE_ENV !== "production";
6912
7762
  var consoleTransport = new winston2.transports.Console({
6913
7763
  format: isDev ? winston2.format.combine(
@@ -6918,6 +7768,20 @@ var consoleTransport = new winston2.transports.Console({
6918
7768
  })
6919
7769
  ) : winston2.format.json()
6920
7770
  });
7771
+ var formatArg = (arg) => typeof arg === "object" ? util.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7772
+ var createLogMethod = (logger, logLevel) => {
7773
+ return (...args) => {
7774
+ const lastArg = args[args.length - 1];
7775
+ let metadata = void 0;
7776
+ let messageArgs = args;
7777
+ if (lastArg && typeof lastArg === "object" && lastArg.__logMetadata === true) {
7778
+ metadata = lastArg;
7779
+ messageArgs = args.slice(0, -1);
7780
+ }
7781
+ const message = messageArgs.map(formatArg).join(" ");
7782
+ logger[logLevel](message, metadata);
7783
+ };
7784
+ };
6921
7785
  var isValidPostgresName = (id) => {
6922
7786
  const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6923
7787
  const isValid = regex.test(id);
@@ -6927,6 +7791,7 @@ var isValidPostgresName = (id) => {
6927
7791
  var ExuluApp = class {
6928
7792
  _agents = [];
6929
7793
  _config;
7794
+ _evals = [];
6930
7795
  _queues = [];
6931
7796
  _contexts = {};
6932
7797
  _tools = [];
@@ -6935,7 +7800,11 @@ var ExuluApp = class {
6935
7800
  }
6936
7801
  // Factory function so we can async
6937
7802
  // initialize the MCP server if needed.
6938
- create = async ({ contexts, agents, config, tools }) => {
7803
+ create = async ({ contexts, agents, config, tools, evals }) => {
7804
+ this._evals = [
7805
+ llmAsJudgeEval,
7806
+ ...evals ?? []
7807
+ ];
6939
7808
  this._contexts = {
6940
7809
  ...contexts,
6941
7810
  codeStandardsContext,
@@ -6987,11 +7856,12 @@ var ExuluApp = class {
6987
7856
  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");
6988
7857
  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.`);
6989
7858
  }
6990
- const contextsArray = Object.values(contexts || {});
6991
7859
  const queueSet = /* @__PURE__ */ new Set();
6992
- for (const context of contextsArray) {
6993
- if (context.embedder?.queue) {
6994
- queueSet.add(await context.embedder.queue);
7860
+ if (redisServer.host?.length && redisServer.port?.length) {
7861
+ queues.register(global_queues.eval_runs, 1, 1);
7862
+ for (const queue of queues.list.values()) {
7863
+ const config2 = await queue.use();
7864
+ queueSet.add(config2);
6995
7865
  }
6996
7866
  }
6997
7867
  this._queues = [...new Set(queueSet.values())];
@@ -7067,7 +7937,7 @@ var ExuluApp = class {
7067
7937
  };
7068
7938
  bullmq = {
7069
7939
  workers: {
7070
- create: async () => {
7940
+ create: async (queues3) => {
7071
7941
  if (!this._config) {
7072
7942
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7073
7943
  }
@@ -7080,16 +7950,22 @@ var ExuluApp = class {
7080
7950
  enableOtel: this._config?.workers?.telemetry?.enabled ?? false,
7081
7951
  transports
7082
7952
  });
7083
- const formatArg = (arg) => typeof arg === "object" ? util.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7084
- console.log = (...args) => logger.info(args.map(formatArg).join(" "));
7085
- console.info = (...args) => logger.info(args.map(formatArg).join(" "));
7086
- console.warn = (...args) => logger.warn(args.map(formatArg).join(" "));
7087
- console.error = (...args) => logger.error(args.map(formatArg).join(" "));
7088
- console.debug = (...args) => logger.debug(args.map(formatArg).join(" "));
7953
+ console.log = createLogMethod(logger, "info");
7954
+ console.info = createLogMethod(logger, "info");
7955
+ console.warn = createLogMethod(logger, "warn");
7956
+ console.error = createLogMethod(logger, "error");
7957
+ console.debug = createLogMethod(logger, "debug");
7958
+ let filteredQueues = this._queues;
7959
+ if (queues3) {
7960
+ filteredQueues = filteredQueues.filter((q) => queues3.includes(q.queue.name));
7961
+ }
7089
7962
  return await createWorkers(
7090
- this._queues,
7963
+ this._agents,
7964
+ filteredQueues,
7091
7965
  this._config,
7092
7966
  Object.values(this._contexts ?? {}),
7967
+ this._evals,
7968
+ this._tools,
7093
7969
  tracer
7094
7970
  );
7095
7971
  }
@@ -7106,16 +7982,16 @@ var ExuluApp = class {
7106
7982
  if (this._config?.telemetry?.enabled) {
7107
7983
  tracer = trace.getTracer("exulu", "1.0.0");
7108
7984
  }
7985
+ const transports = this._config?.logger?.winston?.transports ?? [consoleTransport];
7109
7986
  const logger = logger_default({
7110
7987
  enableOtel: this._config?.telemetry?.enabled ?? false,
7111
- transports: this._config?.logger?.winston?.transports ?? [consoleTransport]
7988
+ transports
7112
7989
  });
7113
- const formatArg = (arg) => typeof arg === "object" ? util.inspect(arg, { depth: null, colors: isDev }) : String(arg);
7114
- console.log = (...args) => logger.info(args.map(formatArg).join(" "));
7115
- console.info = (...args) => logger.info(args.map(formatArg).join(" "));
7116
- console.warn = (...args) => logger.warn(args.map(formatArg).join(" "));
7117
- console.error = (...args) => logger.error(args.map(formatArg).join(" "));
7118
- console.debug = (...args) => logger.debug(args.map(formatArg).join(" "));
7990
+ console.log = createLogMethod(logger, "info");
7991
+ console.info = createLogMethod(logger, "info");
7992
+ console.warn = createLogMethod(logger, "warn");
7993
+ console.error = createLogMethod(logger, "error");
7994
+ console.debug = createLogMethod(logger, "debug");
7119
7995
  if (!this._config) {
7120
7996
  throw new Error("Config not initialized, make sure to call await ExuluApp.create() first when starting your server.");
7121
7997
  }
@@ -7125,7 +8001,9 @@ var ExuluApp = class {
7125
8001
  this._tools,
7126
8002
  Object.values(this._contexts ?? {}),
7127
8003
  this._config,
7128
- tracer
8004
+ this._evals,
8005
+ tracer,
8006
+ this._queues
7129
8007
  );
7130
8008
  if (this._config?.MCP.enabled) {
7131
8009
  const mcp = new ExuluMCP();
@@ -8354,7 +9232,8 @@ var {
8354
9232
  variablesSchema: variablesSchema3,
8355
9233
  workflowTemplatesSchema: workflowTemplatesSchema3,
8356
9234
  rbacSchema: rbacSchema3,
8357
- projectsSchema: projectsSchema3
9235
+ projectsSchema: projectsSchema3,
9236
+ jobResultsSchema: jobResultsSchema3
8358
9237
  } = coreSchemas.get();
8359
9238
  var addMissingFields = async (knex, tableName, fields, skipFields = []) => {
8360
9239
  for (const field of fields) {
@@ -8388,6 +9267,7 @@ var up = async function(knex) {
8388
9267
  platformConfigurationsSchema3(),
8389
9268
  statisticsSchema3(),
8390
9269
  projectsSchema3(),
9270
+ jobResultsSchema3(),
8391
9271
  rbacSchema3(),
8392
9272
  agentsSchema3(),
8393
9273
  variablesSchema3(),
@@ -8572,20 +9452,7 @@ var create = ({
8572
9452
  };
8573
9453
 
8574
9454
  // src/index.ts
8575
- import CryptoJS4 from "crypto-js";
8576
-
8577
- // types/enums/jobs.ts
8578
- var JOB_STATUS_ENUM = {
8579
- completed: "completed",
8580
- failed: "failed",
8581
- delayed: "delayed",
8582
- active: "active",
8583
- waiting: "waiting",
8584
- paused: "paused",
8585
- stuck: "stuck"
8586
- };
8587
-
8588
- // src/index.ts
9455
+ import CryptoJS5 from "crypto-js";
8589
9456
  var ExuluJobs = {
8590
9457
  redis: redisClient,
8591
9458
  jobs: {
@@ -8622,8 +9489,8 @@ var ExuluVariables = {
8622
9489
  throw new Error(`Variable ${name} not found.`);
8623
9490
  }
8624
9491
  if (variable.encrypted) {
8625
- const bytes = CryptoJS4.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
8626
- variable.value = bytes.toString(CryptoJS4.enc.Utf8);
9492
+ const bytes = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
9493
+ variable.value = bytes.toString(CryptoJS5.enc.Utf8);
8627
9494
  }
8628
9495
  return variable.value;
8629
9496
  }
@@ -8751,12 +9618,13 @@ export {
8751
9618
  ExuluDefaultAgents,
8752
9619
  ExuluDefaultContexts,
8753
9620
  ExuluEmbedder,
8754
- ExuluEval,
9621
+ ExuluEval2 as ExuluEval,
8755
9622
  ExuluJobs,
8756
9623
  ExuluOtel,
8757
9624
  queues as ExuluQueues,
8758
9625
  ExuluTool2 as ExuluTool,
8759
9626
  ExuluUtils,
8760
9627
  ExuluVariables,
8761
- db2 as db
9628
+ db2 as db,
9629
+ logMetadata
8762
9630
  };