@exulu/backend 1.46.1 → 1.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.agents/skills/mintlify/SKILL.md +347 -0
  2. package/.editorconfig +15 -0
  3. package/.eslintrc.json +52 -0
  4. package/.jscpd.json +18 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc.json +12 -0
  7. package/CHANGELOG.md +2 -2
  8. package/README.md +747 -0
  9. package/SECURITY.md +5 -0
  10. package/dist/index.cjs +11747 -10227
  11. package/dist/index.d.cts +725 -667
  12. package/dist/index.d.ts +725 -667
  13. package/dist/index.js +12043 -10516
  14. package/ee/LICENSE.md +62 -0
  15. package/ee/agentic-retrieval/index.ts +1109 -0
  16. package/ee/documents/THIRD_PARTY_LICENSES/docling.txt +31 -0
  17. package/ee/documents/processing/build_pdf_processor.sh +35 -0
  18. package/ee/documents/processing/chunk_markdown.py +263 -0
  19. package/ee/documents/processing/doc_processor.ts +635 -0
  20. package/ee/documents/processing/pdf_processor.spec +115 -0
  21. package/ee/documents/processing/pdf_to_markdown.py +420 -0
  22. package/ee/documents/processing/requirements.txt +4 -0
  23. package/ee/entitlements.ts +49 -0
  24. package/ee/markdown.ts +686 -0
  25. package/ee/queues/decorator.ts +140 -0
  26. package/ee/queues/queues.ts +156 -0
  27. package/ee/queues/server.ts +6 -0
  28. package/ee/rbac-resolver.ts +51 -0
  29. package/ee/rbac-update.ts +111 -0
  30. package/ee/schemas.ts +348 -0
  31. package/ee/tokenizer.ts +80 -0
  32. package/ee/workers.ts +1423 -0
  33. package/eslint.config.js +88 -0
  34. package/jest.config.ts +25 -0
  35. package/license.md +73 -49
  36. package/mintlify-docs/.mintignore +7 -0
  37. package/mintlify-docs/AGENTS.md +33 -0
  38. package/mintlify-docs/CLAUDE.MD +50 -0
  39. package/mintlify-docs/CONTRIBUTING.md +32 -0
  40. package/mintlify-docs/LICENSE +21 -0
  41. package/mintlify-docs/README.md +55 -0
  42. package/mintlify-docs/ai-tools/claude-code.mdx +43 -0
  43. package/mintlify-docs/ai-tools/cursor.mdx +39 -0
  44. package/mintlify-docs/ai-tools/windsurf.mdx +39 -0
  45. package/mintlify-docs/api-reference/core-types/agent-types.mdx +110 -0
  46. package/mintlify-docs/api-reference/core-types/analytics-types.mdx +95 -0
  47. package/mintlify-docs/api-reference/core-types/configuration-types.mdx +83 -0
  48. package/mintlify-docs/api-reference/core-types/evaluation-types.mdx +106 -0
  49. package/mintlify-docs/api-reference/core-types/job-types.mdx +135 -0
  50. package/mintlify-docs/api-reference/core-types/overview.mdx +73 -0
  51. package/mintlify-docs/api-reference/core-types/prompt-types.mdx +102 -0
  52. package/mintlify-docs/api-reference/core-types/rbac-types.mdx +163 -0
  53. package/mintlify-docs/api-reference/core-types/session-types.mdx +77 -0
  54. package/mintlify-docs/api-reference/core-types/user-management.mdx +112 -0
  55. package/mintlify-docs/api-reference/core-types/workflow-types.mdx +88 -0
  56. package/mintlify-docs/api-reference/core-types.mdx +585 -0
  57. package/mintlify-docs/api-reference/dynamic-types.mdx +851 -0
  58. package/mintlify-docs/api-reference/endpoint/create.mdx +4 -0
  59. package/mintlify-docs/api-reference/endpoint/delete.mdx +4 -0
  60. package/mintlify-docs/api-reference/endpoint/get.mdx +4 -0
  61. package/mintlify-docs/api-reference/endpoint/webhook.mdx +4 -0
  62. package/mintlify-docs/api-reference/introduction.mdx +661 -0
  63. package/mintlify-docs/api-reference/mutations.mdx +1012 -0
  64. package/mintlify-docs/api-reference/openapi.json +217 -0
  65. package/mintlify-docs/api-reference/queries.mdx +1154 -0
  66. package/mintlify-docs/backend/introduction.mdx +218 -0
  67. package/mintlify-docs/changelog.mdx +387 -0
  68. package/mintlify-docs/community-edition.mdx +304 -0
  69. package/mintlify-docs/core/exulu-agent/api-reference.mdx +894 -0
  70. package/mintlify-docs/core/exulu-agent/configuration.mdx +690 -0
  71. package/mintlify-docs/core/exulu-agent/introduction.mdx +552 -0
  72. package/mintlify-docs/core/exulu-app/api-reference.mdx +481 -0
  73. package/mintlify-docs/core/exulu-app/configuration.mdx +319 -0
  74. package/mintlify-docs/core/exulu-app/introduction.mdx +117 -0
  75. package/mintlify-docs/core/exulu-authentication.mdx +810 -0
  76. package/mintlify-docs/core/exulu-chunkers/api-reference.mdx +1011 -0
  77. package/mintlify-docs/core/exulu-chunkers/configuration.mdx +596 -0
  78. package/mintlify-docs/core/exulu-chunkers/introduction.mdx +403 -0
  79. package/mintlify-docs/core/exulu-context/api-reference.mdx +911 -0
  80. package/mintlify-docs/core/exulu-context/configuration.mdx +648 -0
  81. package/mintlify-docs/core/exulu-context/introduction.mdx +394 -0
  82. package/mintlify-docs/core/exulu-database.mdx +811 -0
  83. package/mintlify-docs/core/exulu-default-agents.mdx +545 -0
  84. package/mintlify-docs/core/exulu-eval/api-reference.mdx +772 -0
  85. package/mintlify-docs/core/exulu-eval/configuration.mdx +680 -0
  86. package/mintlify-docs/core/exulu-eval/introduction.mdx +459 -0
  87. package/mintlify-docs/core/exulu-logging.mdx +464 -0
  88. package/mintlify-docs/core/exulu-otel.mdx +670 -0
  89. package/mintlify-docs/core/exulu-queues/api-reference.mdx +648 -0
  90. package/mintlify-docs/core/exulu-queues/configuration.mdx +650 -0
  91. package/mintlify-docs/core/exulu-queues/introduction.mdx +474 -0
  92. package/mintlify-docs/core/exulu-reranker/api-reference.mdx +630 -0
  93. package/mintlify-docs/core/exulu-reranker/configuration.mdx +663 -0
  94. package/mintlify-docs/core/exulu-reranker/introduction.mdx +516 -0
  95. package/mintlify-docs/core/exulu-tool/api-reference.mdx +723 -0
  96. package/mintlify-docs/core/exulu-tool/configuration.mdx +805 -0
  97. package/mintlify-docs/core/exulu-tool/introduction.mdx +539 -0
  98. package/mintlify-docs/core/exulu-variables/api-reference.mdx +699 -0
  99. package/mintlify-docs/core/exulu-variables/configuration.mdx +736 -0
  100. package/mintlify-docs/core/exulu-variables/introduction.mdx +511 -0
  101. package/mintlify-docs/development.mdx +94 -0
  102. package/mintlify-docs/docs.json +248 -0
  103. package/mintlify-docs/enterprise-edition.mdx +538 -0
  104. package/mintlify-docs/essentials/code.mdx +35 -0
  105. package/mintlify-docs/essentials/images.mdx +59 -0
  106. package/mintlify-docs/essentials/markdown.mdx +88 -0
  107. package/mintlify-docs/essentials/navigation.mdx +87 -0
  108. package/mintlify-docs/essentials/reusable-snippets.mdx +110 -0
  109. package/mintlify-docs/essentials/settings.mdx +318 -0
  110. package/mintlify-docs/favicon.svg +3 -0
  111. package/mintlify-docs/frontend/introduction.mdx +39 -0
  112. package/mintlify-docs/getting-started.mdx +267 -0
  113. package/mintlify-docs/guides/custom-agent.mdx +608 -0
  114. package/mintlify-docs/guides/first-agent.mdx +315 -0
  115. package/mintlify-docs/images/admin_ui.png +0 -0
  116. package/mintlify-docs/images/contexts.png +0 -0
  117. package/mintlify-docs/images/create_agents.png +0 -0
  118. package/mintlify-docs/images/evals.png +0 -0
  119. package/mintlify-docs/images/graphql.png +0 -0
  120. package/mintlify-docs/images/graphql_api.png +0 -0
  121. package/mintlify-docs/images/hero-dark.png +0 -0
  122. package/mintlify-docs/images/hero-light.png +0 -0
  123. package/mintlify-docs/images/hero.png +0 -0
  124. package/mintlify-docs/images/knowledge_sources.png +0 -0
  125. package/mintlify-docs/images/mcp.png +0 -0
  126. package/mintlify-docs/images/scaling.png +0 -0
  127. package/mintlify-docs/index.mdx +411 -0
  128. package/mintlify-docs/logo/dark.svg +9 -0
  129. package/mintlify-docs/logo/light.svg +9 -0
  130. package/mintlify-docs/partners.mdx +558 -0
  131. package/mintlify-docs/products.mdx +77 -0
  132. package/mintlify-docs/snippets/snippet-intro.mdx +4 -0
  133. package/mintlify-docs/styles.css +207 -0
  134. package/package.json +35 -4
  135. package/skills-lock.json +10 -0
  136. package/types/context-processor.ts +45 -0
  137. package/types/exulu-table-definition.ts +79 -0
  138. package/types/file-types.ts +18 -0
  139. package/types/models/agent.ts +10 -12
  140. package/types/models/exulu-agent-tool-config.ts +11 -0
  141. package/types/models/rate-limiter-rules.ts +7 -0
  142. package/types/provider-config.ts +21 -0
  143. package/types/queue-config.ts +16 -0
  144. package/types/rbac-rights-modes.ts +1 -0
  145. package/types/statistics.ts +20 -0
  146. package/types/workflow.ts +31 -0
  147. package/changelogs/10.11.2025_03.12.2025.md +0 -316
  148. package/documentation/logging.md +0 -122
  149. package/documentation/otel.md +0 -145
  150. package/types/models/agent-backend.ts +0 -15
  151. /package/{documentation → devops/documentation}/patch-older-releases.md +0 -0
package/ee/workers.ts ADDED
@@ -0,0 +1,1423 @@
1
+ import IORedis from "ioredis";
2
+ import { redisServer } from "@EE/queues/server.ts";
3
+ import { Job, Worker, type JobState } from "bullmq";
4
+ import { bullmq } from "@SRC/validators/bullmq.ts";
5
+ import { getEnabledTools } from "@SRC/utils/enabled-tools.ts";
6
+ import { ExuluStorage } from "@SRC/exulu/storage.ts";
7
+ import type { ExuluAgent } from "@EXULU_TYPES/models/agent.ts";
8
+ import type { ExuluQueueConfig } from "@EXULU_TYPES/queue-config.ts";
9
+ import { getTableName, type ExuluContext } from "@SRC/exulu/context.ts";
10
+ import type { ExuluReranker } from "@SRC/exulu/reranker.ts";
11
+ import type { ExuluEval } from "@SRC/exulu/evals.ts";
12
+ import type { ExuluTool } from "@SRC/exulu/tool.ts";
13
+ import { postgresClient } from "@SRC/postgres/client";
14
+ import type { BullMqJobData } from "@EE/queues/decorator.ts";
15
+ import { type Tracer } from "@opentelemetry/api";
16
+ import { v4 as uuidv4 } from "uuid";
17
+ import { type UIMessage } from "ai";
18
+ import CryptoJS from "crypto-js";
19
+ import { STATISTICS_TYPE_ENUM, type STATISTICS_TYPE } from "@EXULU_TYPES/enums/statistics";
20
+ import type { User } from "@EXULU_TYPES/models/user";
21
+ import type { EvalRun } from "@EXULU_TYPES/models/eval-run";
22
+ import type { TestCase } from "@EXULU_TYPES/models/test-case";
23
+ import { JOB_STATUS_ENUM } from "@EXULU_TYPES/enums/jobs";
24
+ import type { EvalRunEvalFunction } from "@EXULU_TYPES/models/eval-run";
25
+ import type { ExuluWorkflow } from "@EXULU_TYPES/workflow.ts";
26
+ import type { STATISTICS_LABELS } from "@EXULU_TYPES/statistics.ts";
27
+ import { sanitizeToolName } from "@SRC/utils/sanitize-tool-name.ts";
28
+ import type { ExuluConfig } from "@SRC/exulu/app/index.ts";
29
+ import { updateStatistic } from "@SRC/exulu/statistics";
30
+ import type { ExuluProvider } from "@SRC/exulu/provider.ts";
31
+ import { exuluApp } from "@SRC/exulu/app/singleton";
32
+
33
+ let redisConnection: IORedis;
34
+
35
+ // Global handlers to prevent process crashes from unhandled errors
36
+ // This is critical for BullMQ workers to properly mark jobs as failed
37
+ let unhandledRejectionHandlerInstalled = false;
38
+
39
+ const installGlobalErrorHandlers = () => {
40
+ if (unhandledRejectionHandlerInstalled) return;
41
+
42
+ process.on("unhandledRejection", (reason: any) => {
43
+ console.error(
44
+ "[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.",
45
+ {
46
+ reason: reason instanceof Error ? reason.message : String(reason),
47
+ stack: reason instanceof Error ? reason.stack : undefined,
48
+ },
49
+ );
50
+ // Don't exit - let the worker continue and BullMQ will handle job failure
51
+ });
52
+
53
+ process.on("uncaughtException", (error: Error) => {
54
+ console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
55
+ error: error.message,
56
+ stack: error.stack,
57
+ });
58
+ // Don't exit for database timeouts and similar recoverable errors
59
+ // Only exit for truly fatal errors
60
+ if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
61
+ console.error("[EXULU] Fatal error detected, exiting process.");
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ unhandledRejectionHandlerInstalled = true;
67
+ console.log("[EXULU] Global error handlers installed to prevent worker crashes");
68
+ };
69
+
70
+ export const createWorkers = async (
71
+ providers: ExuluProvider[],
72
+ queues: ExuluQueueConfig[],
73
+ config: ExuluConfig,
74
+ contexts: ExuluContext[],
75
+ rerankers: ExuluReranker[],
76
+ evals: ExuluEval[],
77
+ tools: ExuluTool[],
78
+ tracer?: Tracer,
79
+ ) => {
80
+ console.log("[EXULU] creating workers for " + queues?.length + " queues.");
81
+ console.log(
82
+ "[EXULU] queues",
83
+ queues.map((q) => q.queue.name),
84
+ );
85
+ // Initializes any required workers for processing embedder
86
+ // and agent jobs in the defined queues by checking the registry.
87
+
88
+ // Install global error handlers to prevent crashes
89
+ installGlobalErrorHandlers();
90
+
91
+ // Increase max listeners to accommodate multiple workers (each adds SIGINT/SIGTERM listeners)
92
+ // Each worker adds 2 listeners (SIGINT + SIGTERM), so set to queues.length * 2 + buffer
93
+ process.setMaxListeners(Math.max(queues.length * 2 + 5, 15));
94
+
95
+ if (!redisServer.host || !redisServer.port) {
96
+ console.error(
97
+ "[EXULU] you are trying to start worker, but no redis server is configured in the environment.",
98
+ );
99
+ throw new Error("No redis server configured in the environment, so cannot start worker.");
100
+ }
101
+
102
+ if (!redisConnection) {
103
+ let url = "";
104
+ if (redisServer.username) {
105
+ url = `redis://${redisServer.username}:${redisServer.password}@${redisServer.host}:${redisServer.port}`;
106
+ } else {
107
+ url = `redis://${redisServer.host}:${redisServer.port}`;
108
+ }
109
+
110
+ redisConnection = new IORedis(url, {
111
+ enableOfflineQueue: true,
112
+ retryStrategy: function (times: number) {
113
+ return Math.max(Math.min(Math.exp(times), 20000), 1000);
114
+ },
115
+ maxRetriesPerRequest: null,
116
+ });
117
+ }
118
+
119
+ const workers = queues.map((queue) => {
120
+ console.log(`[EXULU] creating worker for queue ${queue.queue.name}.`);
121
+
122
+ const worker = new Worker(
123
+ `${queue.queue.name}`,
124
+ async (
125
+ bullmqJob: Job,
126
+ ): Promise<{
127
+ result: any;
128
+ metadata: any;
129
+ }> => {
130
+ console.log("[EXULU] starting execution for job", {
131
+ name: bullmqJob.name,
132
+ jobId: bullmqJob.id,
133
+ status: await bullmqJob.getState(),
134
+ type: bullmqJob.data.type,
135
+ });
136
+
137
+ const { db } = await postgresClient();
138
+
139
+ // Type casting data here, couldn't get it to merge
140
+ // on the main object while keeping auto completion.
141
+ const data: BullMqJobData = bullmqJob.data;
142
+
143
+ const timeoutInSeconds = data.timeoutInSeconds || queue.timeoutInSeconds || 600;
144
+ // Create timeout promise with proper error handling
145
+ const timeoutMs = timeoutInSeconds * 1000;
146
+ let timeoutHandle: NodeJS.Timeout;
147
+ const timeoutPromise: Promise<{
148
+ result: any;
149
+ metadata: any;
150
+ }> = new Promise((_, reject) => {
151
+ timeoutHandle = setTimeout(() => {
152
+ const timeoutError = new Error(
153
+ `Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`,
154
+ );
155
+ console.error(`[EXULU] ${timeoutError.message}`);
156
+ reject(timeoutError);
157
+ }, timeoutMs);
158
+ });
159
+
160
+ // Wrap the actual work in a promise
161
+ const workPromise: Promise<{
162
+ result: any;
163
+ metadata: any;
164
+ }> = (async () => {
165
+ try {
166
+ console.log(
167
+ `[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`,
168
+ );
169
+ bullmq.validate(bullmqJob.id, data);
170
+
171
+ if (data.type === "embedder") {
172
+ console.log("[EXULU] running an embedder job.", bullmqJob.name);
173
+
174
+ const label = `embedder-${bullmqJob.name}`;
175
+
176
+ await db.from("job_results").insert({
177
+ job_id: bullmqJob.id,
178
+ label: label,
179
+ state: await bullmqJob.getState(),
180
+ result: null,
181
+ metadata: {},
182
+ });
183
+
184
+ const context = contexts.find((context) => context.id === data.context);
185
+
186
+ if (!context) {
187
+ throw new Error(`Context ${data.context} not found in the registry.`);
188
+ }
189
+
190
+ if (!data.embedder) {
191
+ throw new Error(`No embedder set for embedder job.`);
192
+ }
193
+
194
+ const embedder = contexts.find((context) => context.embedder?.id === data.embedder);
195
+
196
+ if (!embedder) {
197
+ throw new Error(`Embedder ${data.embedder} not found in the registry.`);
198
+ }
199
+
200
+ const result = await context.createAndUpsertEmbeddings(
201
+ data.inputs,
202
+ config,
203
+ data.user,
204
+ {
205
+ label: embedder.name,
206
+ trigger: data.trigger,
207
+ },
208
+ data.role,
209
+ bullmqJob.id,
210
+ );
211
+
212
+ return {
213
+ result,
214
+ metadata: {},
215
+ };
216
+ }
217
+
218
+ if (data.type === "processor") {
219
+ console.log(
220
+ "[EXULU] running a processor job, job name: ",
221
+ bullmqJob.name,
222
+ " job id: ",
223
+ bullmqJob.id,
224
+ " job data: ",
225
+ data,
226
+ " job queue: ",
227
+ bullmqJob.queueName,
228
+ );
229
+
230
+ const label = `processor-${bullmqJob.name}`;
231
+
232
+ await db.from("job_results").insert({
233
+ job_id: bullmqJob.id,
234
+ label: label,
235
+ state: await bullmqJob.getState(),
236
+ result: null,
237
+ metadata: {},
238
+ });
239
+
240
+ const context = contexts.find((context) => context.id === data.context);
241
+
242
+ if (!context) {
243
+ throw new Error(`Context ${data.context} not found in the registry.`);
244
+ }
245
+
246
+ if (!data.inputs.id) {
247
+ throw new Error(
248
+ `[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`,
249
+ );
250
+ }
251
+
252
+ if (!context.processor) {
253
+ throw new Error(
254
+ `Tried to run a processor job for context ${context.id}, but no processor is set.`,
255
+ );
256
+ }
257
+
258
+ const exuluStorage = new ExuluStorage({ config });
259
+
260
+ console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
261
+ const processorResult = await context.processor.execute({
262
+ item: data.inputs,
263
+ user: data.user,
264
+ role: data.role,
265
+ utils: {
266
+ storage: exuluStorage,
267
+ },
268
+ exuluConfig: config,
269
+ });
270
+
271
+ if (!processorResult) {
272
+ throw new Error(
273
+ `[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`,
274
+ );
275
+ }
276
+
277
+ // The field key is used to define a processor, but is
278
+ // not part of the database, so remove it here before
279
+ // we upadte the item in the db.
280
+ delete processorResult.field;
281
+
282
+ // Update the item in the db with the processor result
283
+ await db
284
+ .from(getTableName(context.id))
285
+ .where({
286
+ id: processorResult.id,
287
+ })
288
+ .update({
289
+ ...processorResult,
290
+ last_processed_at: new Date().toISOString(),
291
+ });
292
+
293
+ let jobs: string[] = [];
294
+ if (context.processor?.config?.generateEmbeddings) {
295
+ // If the processor was configured to automatically trigger
296
+ // the generation of embeddings, we trigger it here.
297
+ // IMPORTANT: We need to fetch the complete item from the database
298
+ // to ensure we have all fields (especially external_id) for embeddings
299
+ const fullItem = await db
300
+ .from(getTableName(context.id))
301
+ .where({
302
+ id: processorResult.id,
303
+ })
304
+ .first();
305
+
306
+ if (!fullItem) {
307
+ throw new Error(
308
+ `[EXULU] Item ${processorResult.id} not found after processor update in context ${context.id}`,
309
+ );
310
+ }
311
+
312
+ const { job: embeddingsJob } = await context.embeddings.generate.one({
313
+ item: fullItem,
314
+ user: data.user,
315
+ role: data.role,
316
+ trigger: "processor",
317
+ config,
318
+ });
319
+
320
+ if (embeddingsJob) {
321
+ jobs.push(embeddingsJob);
322
+ }
323
+ }
324
+
325
+ return {
326
+ result: processorResult,
327
+ metadata: {
328
+ jobs: jobs.length > 0 ? jobs.join(",") : undefined,
329
+ },
330
+ };
331
+ }
332
+
333
+ if (data.type === "workflow") {
334
+ console.log("[EXULU] running a workflow job.", bullmqJob.name);
335
+
336
+ const label = `workflow-run-${data.workflow}`;
337
+
338
+ await db.from("job_results").insert({
339
+ job_id: bullmqJob.id,
340
+ label: label,
341
+ state: await bullmqJob.getState(),
342
+ result: null,
343
+ metadata: {},
344
+ tries: 1,
345
+ });
346
+
347
+ const {
348
+ agent,
349
+ provider,
350
+ user,
351
+ messages: inputMessages,
352
+ } = await validateWorkflowPayload(data, providers);
353
+
354
+ const retries = 3;
355
+ let attempts = 0;
356
+
357
+ // todo allow setting queue on agent provider and then create a job with type "agent"
358
+ const promise = new Promise<{
359
+ messages: UIMessage[];
360
+ metadata: {
361
+ tokens: {
362
+ totalTokens: number;
363
+ reasoningTokens: number;
364
+ inputTokens: number;
365
+ outputTokens: number;
366
+ cachedInputTokens: number;
367
+ };
368
+ duration: number;
369
+ };
370
+ }>(async (resolve, reject) => {
371
+ while (attempts < retries) {
372
+ try {
373
+ const messages = await processUiMessagesFlow({
374
+ providers,
375
+ agent,
376
+ provider,
377
+ inputMessages,
378
+ contexts,
379
+ rerankers,
380
+ user,
381
+ tools,
382
+ config,
383
+ variables: data.inputs,
384
+ });
385
+ resolve(messages);
386
+ break;
387
+ } catch (error: unknown) {
388
+ console.error(
389
+ `[EXULU] error processing UI messages flow for agent ${agent.name} (${agent.id}).`,
390
+ error instanceof Error ? error.message : String(error),
391
+ );
392
+ attempts++;
393
+ if (attempts >= retries) {
394
+ reject(new Error(error instanceof Error ? error.message : String(error)));
395
+ }
396
+ await new Promise((resolve) => setTimeout((resolve) => resolve(true), 2000));
397
+ }
398
+ }
399
+ });
400
+
401
+ const result = await promise;
402
+ const messages = result.messages;
403
+ const metadata = result.metadata;
404
+
405
+ return {
406
+ result: messages[messages.length - 1], // last message
407
+ metadata: {
408
+ messages,
409
+ ...metadata,
410
+ },
411
+ };
412
+ }
413
+
414
+ if (data.type === "eval_run") {
415
+ console.log("[EXULU] running an eval run job.", bullmqJob.name);
416
+
417
+ const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
418
+
419
+ const existingResult = await db.from("job_results").where({ label: label }).first();
420
+
421
+ if (existingResult) {
422
+ // update existing
423
+ console.log("[EXULU] found existing job result, so ");
424
+ await db
425
+ .from("job_results")
426
+ .where({ label: label })
427
+ .update({
428
+ job_id: bullmqJob.id,
429
+ label: label,
430
+ state: await bullmqJob.getState(),
431
+ result: null,
432
+ metadata: {},
433
+ tries: existingResult.tries + 1,
434
+ });
435
+ } else {
436
+ await db.from("job_results").insert({
437
+ job_id: bullmqJob.id,
438
+ label: label,
439
+ state: await bullmqJob.getState(),
440
+ result: null,
441
+ metadata: {},
442
+ tries: 1,
443
+ });
444
+ }
445
+
446
+ const {
447
+ agent,
448
+ provider,
449
+ user,
450
+ evalRun,
451
+ testCase,
452
+ messages: inputMessages,
453
+ } = await validateEvalPayload(data, providers);
454
+
455
+ const retries = 3;
456
+ let attempts = 0;
457
+
458
+ // todo allow setting queue on agent Provider and then create a job with type "agent"
459
+ const promise = new Promise<{
460
+ messages: UIMessage[];
461
+ metadata: {
462
+ tokens: {
463
+ totalTokens: number;
464
+ reasoningTokens: number;
465
+ inputTokens: number;
466
+ outputTokens: number;
467
+ cachedInputTokens: number;
468
+ };
469
+ duration: number;
470
+ };
471
+ }>(async (resolve, reject) => {
472
+ while (attempts < retries) {
473
+ try {
474
+ const messages = await processUiMessagesFlow({
475
+ providers,
476
+ agent,
477
+ provider,
478
+ inputMessages,
479
+ contexts,
480
+ rerankers,
481
+ user,
482
+ tools,
483
+ config,
484
+ });
485
+ resolve(messages);
486
+ break;
487
+ } catch (error: unknown) {
488
+ console.error(
489
+ `[EXULU] error processing UI messages flow for agent ${agent.name} (${agent.id}).`,
490
+ error instanceof Error ? error.message : String(error),
491
+ );
492
+ attempts++;
493
+ if (attempts >= retries) {
494
+ reject(new Error(error instanceof Error ? error.message : String(error)));
495
+ }
496
+ await new Promise((resolve) => setTimeout((resolve) => resolve(true), 2000));
497
+ }
498
+ }
499
+ });
500
+
501
+ const result = await promise;
502
+ const messages = result.messages;
503
+ const metadata = result.metadata;
504
+
505
+ const evalFunctions: EvalRunEvalFunction[] = evalRun.eval_functions;
506
+
507
+ let evalFunctionResults: {
508
+ test_case_id: string;
509
+ eval_run_id: string;
510
+ eval_function_id: string;
511
+ result: number;
512
+ }[] = [];
513
+
514
+ for (const evalFunction of evalFunctions) {
515
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
516
+
517
+ if (!evalMethod) {
518
+ throw new Error(
519
+ `Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`,
520
+ );
521
+ }
522
+
523
+ let result: number | undefined;
524
+
525
+ // If queue is defined, schedule the sub-task, and wait for it to
526
+ // complete by polling it every 5 seconds.
527
+ if (evalMethod.queue) {
528
+ const queue = await evalMethod.queue;
529
+ const jobData: BullMqJobData = {
530
+ ...data,
531
+ type: "eval_function",
532
+ eval_functions: [
533
+ {
534
+ id: evalFunction.id,
535
+ config: evalFunction.config || {},
536
+ },
537
+ ],
538
+ // updating the input messages with the messages we want to run the eval
539
+ // function on, which are the output messages from the agent.
540
+ inputs: messages,
541
+ };
542
+
543
+ const redisId = uuidv4();
544
+ const job = await queue.queue.add("eval_function", jobData, {
545
+ jobId: redisId,
546
+ // Setting it to 3 as a sensible default, as
547
+ // many AI services are quite unstable.
548
+ attempts: queue.retries || 3, // todo make this configurable?
549
+ removeOnComplete: 5000,
550
+ removeOnFail: 5000,
551
+ backoff: queue.backoff || {
552
+ type: "exponential",
553
+ delay: 2000,
554
+ },
555
+ });
556
+
557
+ if (!job.id) {
558
+ throw new Error(
559
+ `Tried to add job to queue ${queue.queue.name} but failed to get the job ID.`,
560
+ );
561
+ }
562
+
563
+ result = await pollJobResult({ queue, jobId: job.id });
564
+
565
+ const evalFunctionResult = {
566
+ test_case_id: testCase.id,
567
+ eval_run_id: evalRun.id,
568
+ eval_function_id: evalFunction.id,
569
+ eval_function_name: evalFunction.name,
570
+ eval_function_config: evalFunction.config || {},
571
+ result: result || 0,
572
+ };
573
+
574
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, {
575
+ result: result || 0,
576
+ });
577
+
578
+ evalFunctionResults.push(evalFunctionResult);
579
+
580
+ // If queue is not defined, execute the eval function directly.
581
+ // and use the result immediately below.
582
+ } else {
583
+ result = await evalMethod.run(
584
+ agent,
585
+ provider,
586
+ testCase,
587
+ messages,
588
+ evalFunction.config || {},
589
+ );
590
+
591
+ const evalFunctionResult = {
592
+ test_case_id: testCase.id,
593
+ eval_run_id: evalRun.id,
594
+ eval_function_id: evalFunction.id,
595
+ result: result || 0,
596
+ };
597
+
598
+ evalFunctionResults.push(evalFunctionResult);
599
+
600
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, {
601
+ result: result || 0,
602
+ });
603
+ }
604
+ }
605
+
606
+ const scores = evalFunctionResults.map((result) => result.result);
607
+
608
+ console.log("[EXULU] Exulu eval run scores for test case: " + testCase.id, scores);
609
+
610
+ let score = 0;
611
+ switch (data.scoring_method?.toLowerCase()) {
612
+ case "median":
613
+ console.log("[EXULU] Calculating median score");
614
+ score = getMedian(scores);
615
+ break;
616
+ case "average":
617
+ console.log("[EXULU] Calculating average score");
618
+ score = getAverage(scores);
619
+ break;
620
+ case "sum":
621
+ console.log("[EXULU] Calculating sum score");
622
+ score = getSum(scores);
623
+ break;
624
+ default:
625
+ console.log("[EXULU] Calculating average score");
626
+ score = getAverage(scores);
627
+ }
628
+
629
+ return {
630
+ result: score,
631
+ metadata: {
632
+ messages,
633
+ function_results: [...evalFunctionResults],
634
+ ...metadata,
635
+ },
636
+ };
637
+ }
638
+
639
+ if (data.type === "eval_function") {
640
+ console.log("[EXULU] running an eval function job.", bullmqJob.name);
641
+
642
+ if (data.eval_functions?.length !== 1) {
643
+ throw new Error(
644
+ `Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`,
645
+ );
646
+ }
647
+
648
+ const label = `eval-function-${data.eval_run_id}-${data.test_case_id}-${data.eval_functions?.[0]?.id}`;
649
+
650
+ const existingResult = await db.from("job_results").where({ label: label }).first();
651
+
652
+ if (existingResult) {
653
+ // update existing
654
+ await db
655
+ .from("job_results")
656
+ .where({ label: label })
657
+ .update({
658
+ job_id: bullmqJob.id,
659
+ label: label,
660
+ state: await bullmqJob.getState(),
661
+ result: null,
662
+ metadata: {},
663
+ tries: existingResult.tries + 1,
664
+ });
665
+ } else {
666
+ await db.from("job_results").insert({
667
+ job_id: bullmqJob.id,
668
+ label: label,
669
+ state: await bullmqJob.getState(),
670
+ result: null,
671
+ metadata: {},
672
+ tries: 1,
673
+ });
674
+ }
675
+
676
+ const {
677
+ evalRun,
678
+ agent,
679
+ provider,
680
+ testCase,
681
+ messages: inputMessages,
682
+ } = await validateEvalPayload(data, providers);
683
+
684
+ const evalFunctions: {
685
+ id: string;
686
+ config: Record<string, any>;
687
+ }[] = evalRun.eval_functions;
688
+
689
+ let result: number | undefined;
690
+
691
+ for (const evalFunction of evalFunctions) {
692
+ // todo run the eval execute function using the input.messages array and return the numerical result
693
+ const evalMethod = evals.find((e) => e.id === evalFunction.id);
694
+
695
+ if (!evalMethod) {
696
+ throw new Error(
697
+ `Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`,
698
+ );
699
+ }
700
+
701
+ result = await evalMethod.run(
702
+ agent,
703
+ provider,
704
+ testCase,
705
+ inputMessages,
706
+ evalFunction.config || {},
707
+ );
708
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, {
709
+ result: result || 0,
710
+ });
711
+ }
712
+
713
+ return {
714
+ result,
715
+ metadata: {},
716
+ };
717
+ }
718
+
719
+ if (data.type === "source") {
720
+ console.log("[EXULU] running a source job.", bullmqJob.name);
721
+
722
+ if (!data.source) {
723
+ throw new Error(`No source id set for source job.`);
724
+ }
725
+
726
+ if (!data.context) {
727
+ throw new Error(`No context id set for source job.`);
728
+ }
729
+
730
+ const context = contexts.find((c) => c.id === data.context);
731
+
732
+ if (!context) {
733
+ throw new Error(`Context ${data.context} not found in the registry.`);
734
+ }
735
+
736
+ const source = context.sources.find((s) => s.id === data.source);
737
+
738
+ if (!source) {
739
+ throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
740
+ }
741
+
742
+ const result = await source.execute(data.inputs);
743
+
744
+ let jobs: string[] = [];
745
+ let items: string[] = [];
746
+
747
+ for (const item of result) {
748
+ const { item: createdItem, job } = await context.createItem(
749
+ item,
750
+ config,
751
+ data.user,
752
+ data.role,
753
+ item.external_id || item.id ? true : false,
754
+ );
755
+ if (job) {
756
+ jobs.push(job);
757
+ console.log(
758
+ `[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`,
759
+ {
760
+ item: createdItem,
761
+ job: job,
762
+ },
763
+ );
764
+ }
765
+ if (createdItem.id) {
766
+ items.push(createdItem.id);
767
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, {
768
+ item: createdItem,
769
+ });
770
+ }
771
+ }
772
+
773
+ await updateStatistic({
774
+ name: "count",
775
+ label: source.id,
776
+ type: STATISTICS_TYPE_ENUM.SOURCE_UPDATE as STATISTICS_TYPE,
777
+ trigger: "api",
778
+ count: 1,
779
+ user: data?.user,
780
+ role: data?.role,
781
+ });
782
+
783
+ return {
784
+ result,
785
+ metadata: {
786
+ jobs,
787
+ items,
788
+ },
789
+ };
790
+ }
791
+
792
+ throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
793
+ } catch (error: unknown) {
794
+ console.error(
795
+ `[EXULU] job failed.`,
796
+ error instanceof Error ? error.message : String(error),
797
+ );
798
+ throw error;
799
+ }
800
+ })();
801
+
802
+ // Race between work and timeout with proper cleanup
803
+ try {
804
+ const result = await Promise.race([workPromise, timeoutPromise]);
805
+ // Clear timeout if work completes successfully
806
+ clearTimeout(timeoutHandle!);
807
+ return result;
808
+ } catch (error: unknown) {
809
+ // Clear timeout on error
810
+ clearTimeout(timeoutHandle!);
811
+ console.error(
812
+ `[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`,
813
+ error instanceof Error ? error.message : String(error),
814
+ );
815
+ throw error;
816
+ }
817
+ },
818
+ {
819
+ autorun: true,
820
+ connection: redisConnection,
821
+ concurrency: queue.concurrency?.worker || 1,
822
+ removeOnComplete: { count: 1000 },
823
+ removeOnFail: { count: 5000 },
824
+ ...(queue.ratelimit && {
825
+ limiter: {
826
+ max: queue.ratelimit,
827
+ duration: 1000,
828
+ },
829
+ }),
830
+ },
831
+ );
832
+
833
+ worker.on(
834
+ "completed",
835
+ async (
836
+ job,
837
+ returnvalue: {
838
+ result: any;
839
+ metadata: any;
840
+ },
841
+ ) => {
842
+ console.log(`[EXULU] completed job ${job.id}.`, returnvalue);
843
+
844
+ const { db } = await postgresClient();
845
+
846
+ await db
847
+ .from("job_results")
848
+ .where({ job_id: job.id })
849
+ .update({
850
+ state: JOB_STATUS_ENUM.completed,
851
+ result: returnvalue.result != null ? JSON.stringify(returnvalue.result) : null,
852
+ metadata: returnvalue.metadata != null ? JSON.stringify(returnvalue.metadata) : null,
853
+ });
854
+ },
855
+ );
856
+
857
+ worker.on("failed", async (job, error: Error, prev: string) => {
858
+ if (job?.id) {
859
+ const { db } = await postgresClient();
860
+
861
+ console.error(`[EXULU] failed job ${job.id}.`, error);
862
+
863
+ await db.from("job_results").where({ job_id: job.id }).update({
864
+ state: JOB_STATUS_ENUM.failed,
865
+ error,
866
+ });
867
+ return;
868
+ }
869
+ console.error(
870
+ `[EXULU] job failed.`,
871
+ job?.name
872
+ ? {
873
+ error: error instanceof Error ? error.message : String(error),
874
+ }
875
+ : error,
876
+ );
877
+ });
878
+
879
+ worker.on("error", (error: Error) => {
880
+ console.error(`[EXULU] worker error.`, error);
881
+ });
882
+
883
+ worker.on("progress", (job, progress) => {
884
+ console.log(`[EXULU] job progress ${job.id}.`, job.name, {
885
+ progress: progress,
886
+ });
887
+ });
888
+
889
+ const gracefulShutdown = async (signal) => {
890
+ console.log(`Received ${signal}, closing server...`);
891
+ await worker.close();
892
+ // Other asynchronous closings
893
+ process.exit(0);
894
+ };
895
+
896
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
897
+
898
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
899
+
900
+ return worker;
901
+ });
902
+
903
+ return workers;
904
+ };
905
+
906
+ export const validateWorkflowPayload = async (
907
+ data: BullMqJobData,
908
+ providers: ExuluProvider[],
909
+ ): Promise<{
910
+ agent: ExuluAgent;
911
+ provider: ExuluProvider;
912
+ user: User;
913
+ workflow: ExuluWorkflow;
914
+ variables: Record<string, any>;
915
+ messages: UIMessage[];
916
+ }> => {
917
+ if (!data.workflow) {
918
+ throw new Error(`No workflow ID set for workflow job.`);
919
+ }
920
+
921
+ if (!data.user) {
922
+ throw new Error(`No user set for workflow job.`);
923
+ }
924
+
925
+ if (!data.role) {
926
+ throw new Error(`No role set for workflow job.`);
927
+ }
928
+
929
+ const { db } = await postgresClient();
930
+
931
+ const workflow = await db.from("workflow_templates").where({ id: data.workflow }).first();
932
+
933
+ if (!workflow) {
934
+ throw new Error(`Workflow ${data.workflow} not found in the database.`);
935
+ }
936
+
937
+ const agent = await exuluApp.get().agent(workflow.agent);
938
+
939
+ if (!agent) {
940
+ throw new Error(`Agent ${workflow.agent} not found in the database.`);
941
+ }
942
+
943
+ const provider = providers.find((a) => a.id === agent.provider);
944
+
945
+ if (!provider) {
946
+ throw new Error(`Provider ${agent.provider} not found in the database.`);
947
+ }
948
+
949
+ const user = await db.from("users").where({ id: data.user }).first();
950
+
951
+ if (!user) {
952
+ throw new Error(`User ${data.user} not found in the database.`);
953
+ }
954
+
955
+ return {
956
+ agent,
957
+ provider,
958
+ user,
959
+ workflow,
960
+ variables: data.inputs,
961
+ messages: workflow.steps_json,
962
+ };
963
+ };
964
+
965
+ const validateEvalPayload = async (
966
+ data: BullMqJobData,
967
+ providers: ExuluProvider[],
968
+ ): Promise<{
969
+ agent: ExuluAgent;
970
+ provider: ExuluProvider;
971
+ user: User;
972
+ testCase: TestCase;
973
+ evalRun: EvalRun;
974
+ messages: UIMessage[];
975
+ }> => {
976
+ if (!data.eval_run_id) {
977
+ throw new Error(`No eval run ID set for eval job.`);
978
+ }
979
+
980
+ if (!data.test_case_id) {
981
+ throw new Error(`No test case ID set for eval job.`);
982
+ }
983
+
984
+ if (!data.user) {
985
+ throw new Error(`No user set for eval job.`);
986
+ }
987
+
988
+ if (!data.role) {
989
+ throw new Error(`No role set for eval job.`);
990
+ }
991
+
992
+ if (!data.agent_id) {
993
+ throw new Error(`No agent ID set for eval job.`);
994
+ }
995
+
996
+ if (!data.inputs?.length) {
997
+ throw new Error(`No inputs set for eval job, expected array of UIMessage objects.`);
998
+ }
999
+
1000
+ const { db } = await postgresClient();
1001
+
1002
+ const evalRun = await db.from("eval_runs").where({ id: data.eval_run_id }).first();
1003
+
1004
+ if (!evalRun) {
1005
+ throw new Error(`Eval run ${data.eval_run_id} not found in the database.`);
1006
+ }
1007
+
1008
+ const agent = await exuluApp.get().agent(evalRun.agent_id);
1009
+
1010
+ if (!agent) {
1011
+ throw new Error(`Agent ${evalRun.agent_id} not found in the database.`);
1012
+ }
1013
+
1014
+ const provider = providers.find((a) => a.id === agent.provider);
1015
+
1016
+ if (!provider) {
1017
+ throw new Error(`Provider ${agent.provider} not found in the database.`);
1018
+ }
1019
+
1020
+ const user = await db.from("users").where({ id: data.user }).first();
1021
+
1022
+ if (!user) {
1023
+ throw new Error(`User ${data.user} not found in the database.`);
1024
+ }
1025
+
1026
+ const testCase = await db.from("test_cases").where({ id: data.test_case_id }).first();
1027
+
1028
+ if (!testCase) {
1029
+ throw new Error(`Test case ${data.test_case_id} not found in the database.`);
1030
+ }
1031
+
1032
+ return {
1033
+ agent,
1034
+ provider,
1035
+ user,
1036
+ testCase,
1037
+ evalRun,
1038
+ messages: data.inputs,
1039
+ };
1040
+ };
1041
+
1042
+ const pollJobResult = async ({
1043
+ queue,
1044
+ jobId,
1045
+ }: {
1046
+ queue: ExuluQueueConfig;
1047
+ jobId: string;
1048
+ }): Promise<any> => {
1049
+ let attempts = 0;
1050
+ let timeoutInSeconds = queue.timeoutInSeconds || 180;
1051
+ const startTime = Date.now();
1052
+
1053
+ let result: any;
1054
+ while (true) {
1055
+ attempts++;
1056
+
1057
+ const job = await Job.fromId(queue.queue, jobId);
1058
+ if (!job) {
1059
+ await new Promise((resolve) => setTimeout((resolve) => resolve(true), 2000));
1060
+ continue;
1061
+ }
1062
+
1063
+ const elapsedTime = Date.now() - startTime;
1064
+ if (elapsedTime > timeoutInSeconds * 1000) {
1065
+ throw new Error(
1066
+ `Job ${job.id} timed out after ${timeoutInSeconds} seconds for job eval function job ${job.name}.`,
1067
+ );
1068
+ }
1069
+ console.log(`[EXULU] polling eval function job ${job.name} for state... (attempt ${attempts})`);
1070
+ const jobState: JobState = (await job.getState()) as JobState;
1071
+ console.log(`[EXULU] eval function job ${job.name} state: ${jobState}`);
1072
+ if (jobState === "failed") {
1073
+ throw new Error(`Job ${job.name} (${job.id}) failed with error: ${job.failedReason}.`);
1074
+ }
1075
+ if (jobState === "completed") {
1076
+ console.log(
1077
+ `[EXULU] eval function job ${job.name} completed, getting result from database...`,
1078
+ );
1079
+ const { db } = await postgresClient();
1080
+ const entry = await db.from("job_results").where({ job_id: job.id }).first();
1081
+
1082
+ console.log("[EXULU] eval function job ${job.name} result", entry);
1083
+ result = entry?.result;
1084
+ if (result === undefined || result === null || result === "") {
1085
+ throw new Error(`Eval function ${job.id} result not found in database
1086
+ for job eval function job ${job.name}. Entry data from DB: ${JSON.stringify(entry)}.`);
1087
+ }
1088
+ console.log(`[EXULU] eval function ${job.id} result: ${result}`);
1089
+ break;
1090
+ }
1091
+ // Wait for 2 seconds before polling again
1092
+ await new Promise((resolve) => setTimeout((resolve) => resolve(true), 2000));
1093
+ }
1094
+ return result;
1095
+ };
1096
+
1097
+ export const processUiMessagesFlow = async ({
1098
+ providers,
1099
+ agent,
1100
+ provider,
1101
+ inputMessages,
1102
+ contexts,
1103
+ rerankers,
1104
+ user,
1105
+ tools,
1106
+ config,
1107
+ variables,
1108
+ }: {
1109
+ providers: ExuluProvider[];
1110
+ agent: ExuluAgent;
1111
+ provider: ExuluProvider;
1112
+ inputMessages: UIMessage[];
1113
+ contexts: ExuluContext[];
1114
+ rerankers: ExuluReranker[];
1115
+ user: User;
1116
+ tools: ExuluTool[];
1117
+ config: ExuluConfig;
1118
+ variables?: Record<string, any>;
1119
+ }): Promise<{
1120
+ messages: UIMessage[];
1121
+ metadata: {
1122
+ tokens: {
1123
+ totalTokens: number;
1124
+ reasoningTokens: number;
1125
+ inputTokens: number;
1126
+ outputTokens: number;
1127
+ cachedInputTokens: number;
1128
+ };
1129
+ duration: number;
1130
+ };
1131
+ }> => {
1132
+ console.log("[EXULU] processing UI messages flow for agent.");
1133
+ console.log("[EXULU] input messages", inputMessages);
1134
+
1135
+ // If queue is not defined, execute the eval function directly
1136
+ console.log(
1137
+ "[EXULU] agent tools",
1138
+ agent.tools?.map((x) => x.name + " (" + x.id + ")"),
1139
+ );
1140
+
1141
+ const disabledTools = [];
1142
+ let enabledTools: ExuluTool[] = await getEnabledTools(
1143
+ agent,
1144
+ tools,
1145
+ contexts,
1146
+ rerankers,
1147
+ disabledTools,
1148
+ providers,
1149
+ user,
1150
+ );
1151
+
1152
+ console.log(
1153
+ "[EXULU] enabled tools",
1154
+ enabledTools?.map((x) => x.name + " (" + x.id + ")"),
1155
+ );
1156
+
1157
+ // Get the variable name from user's anthropic_token field
1158
+ const variableName = agent.providerapikey;
1159
+
1160
+ // Look up the variable from the variables table
1161
+ const { db } = await postgresClient();
1162
+
1163
+ let providerapikey: string | undefined;
1164
+
1165
+ if (variableName) {
1166
+ const variable = await db.from("variables").where({ name: variableName }).first();
1167
+ if (!variable) {
1168
+ throw new Error(
1169
+ `Provider API key variable not found for agent ${agent.name} (${agent.id}).`,
1170
+ );
1171
+ }
1172
+
1173
+ // Get the API key from the variable (decrypt if encrypted)
1174
+ providerapikey = variable.value;
1175
+
1176
+ if (!variable.encrypted) {
1177
+ throw new Error(
1178
+ `Provider API key variable not encrypted for agent ${agent.name} (${agent.id}), for security reasons you are only allowed to use encrypted variables for provider API keys.`,
1179
+ );
1180
+ }
1181
+
1182
+ if (variable.encrypted) {
1183
+ const bytes = CryptoJS.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
1184
+ providerapikey = bytes.toString(CryptoJS.enc.Utf8);
1185
+ }
1186
+ }
1187
+
1188
+ // Remove placeholder agent response before sending
1189
+ const messagesWithoutPlaceholder = inputMessages.filter(
1190
+ (message) => (message.metadata as any)?.type !== "placeholder",
1191
+ );
1192
+
1193
+ console.log("[EXULU] messages without placeholder", messagesWithoutPlaceholder);
1194
+
1195
+ // Iterate through the conversation
1196
+ let index = 0;
1197
+ let messageHistory: {
1198
+ messages: UIMessage[];
1199
+ metadata: {
1200
+ tokens: {
1201
+ totalTokens: number;
1202
+ reasoningTokens: number;
1203
+ inputTokens: number;
1204
+ outputTokens: number;
1205
+ cachedInputTokens: number;
1206
+ };
1207
+ duration: number;
1208
+ };
1209
+ } = {
1210
+ messages: [],
1211
+ metadata: {
1212
+ tokens: {
1213
+ totalTokens: 0,
1214
+ reasoningTokens: 0,
1215
+ inputTokens: 0,
1216
+ outputTokens: 0,
1217
+ cachedInputTokens: 0,
1218
+ },
1219
+ duration: 0,
1220
+ },
1221
+ };
1222
+
1223
+ console.log("[EXULU] variables", variables);
1224
+ for (const currentMessage of messagesWithoutPlaceholder) {
1225
+ console.log("[EXULU] running through the conversation");
1226
+ console.log("[EXULU] current index", index);
1227
+ console.log("[EXULU] current message", currentMessage);
1228
+ console.log("[EXULU] message history", messageHistory);
1229
+
1230
+ // Identify {variable_name} in the current message parts
1231
+ // Replace them with the values in variables
1232
+ // If any are missing, throw an error
1233
+ for (const part of currentMessage.parts) {
1234
+ if (part.type === "text") {
1235
+ const text = part.text;
1236
+ const variableNames = [...text.matchAll(/{([^}]+)}/g)].map((match) => match[1]);
1237
+ if (variableNames) {
1238
+ for (const variableName of variableNames) {
1239
+ if (!variableName) {
1240
+ continue;
1241
+ }
1242
+ console.log("[EXULU] variableName", variableName);
1243
+ const variableValue = variables?.[variableName];
1244
+ console.log("[EXULU] variableValue", variableValue);
1245
+ if (variableValue) {
1246
+ part.text = part.text.replaceAll(`{${variableName}}`, variableValue);
1247
+ } else {
1248
+ throw new Error(
1249
+ `Value for variable ${variableName} not provided in variables for processing message flow. Either remove it from the messages, or provide it as an argument.`,
1250
+ );
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+
1257
+ const statistics = {
1258
+ label: agent.name,
1259
+ trigger: "agent" as STATISTICS_LABELS,
1260
+ };
1261
+
1262
+ messageHistory = await new Promise<{
1263
+ messages: UIMessage[];
1264
+ metadata: {
1265
+ tokens: {
1266
+ totalTokens: number;
1267
+ reasoningTokens: number;
1268
+ inputTokens: number;
1269
+ outputTokens: number;
1270
+ cachedInputTokens: number;
1271
+ };
1272
+ duration: number;
1273
+ };
1274
+ }>(async (resolve, reject) => {
1275
+ const startTime = Date.now();
1276
+
1277
+ try {
1278
+ const result = await provider.generateStream({
1279
+ contexts,
1280
+ rerankers,
1281
+ agent: agent,
1282
+ user,
1283
+ approvedTools: tools.map((tool) => "tool-" + sanitizeToolName(tool.name)),
1284
+ instructions: agent.instructions,
1285
+ session: undefined,
1286
+ previousMessages: messageHistory.messages,
1287
+ message: currentMessage,
1288
+ currentTools: enabledTools,
1289
+ allExuluTools: tools,
1290
+ providerapikey,
1291
+ toolConfigs: agent.tools,
1292
+ exuluConfig: config,
1293
+ });
1294
+
1295
+ console.log("[EXULU] consuming stream for agent.");
1296
+ const stream = result.stream.toUIMessageStream({
1297
+ messageMetadata: ({ part }) => {
1298
+ console.log("[EXULU] part", part.type);
1299
+ if (part.type === "finish") {
1300
+ return {
1301
+ totalTokens: part.totalUsage.totalTokens,
1302
+ reasoningTokens: part.totalUsage.reasoningTokens,
1303
+ inputTokens: part.totalUsage.inputTokens,
1304
+ outputTokens: part.totalUsage.outputTokens,
1305
+ cachedInputTokens: part.totalUsage.cachedInputTokens,
1306
+ };
1307
+ }
1308
+ return undefined;
1309
+ },
1310
+ originalMessages: result.originalMessages,
1311
+ sendReasoning: true,
1312
+ sendSources: true,
1313
+ onError: (error) => {
1314
+ console.error("[EXULU] Ui message stream error.", error);
1315
+ reject(new Error(error instanceof Error ? error.message : String(error)));
1316
+ return `Ui message stream error: ${error instanceof Error ? error.message : String(error)}`;
1317
+ },
1318
+ onFinish: async ({ messages }) => {
1319
+ const metadata = messages[messages.length - 1]?.metadata as any;
1320
+ console.log("[EXULU] Stream finished with messages:", messages);
1321
+ console.log("[EXULU] Stream metadata", metadata);
1322
+ await Promise.all([
1323
+ updateStatistic({
1324
+ name: "count",
1325
+ label: statistics.label,
1326
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN as STATISTICS_TYPE,
1327
+ trigger: statistics.trigger,
1328
+ count: 1,
1329
+ user: user.id,
1330
+ role: user?.role?.id,
1331
+ }),
1332
+ ...(metadata?.inputTokens
1333
+ ? [
1334
+ updateStatistic({
1335
+ name: "inputTokens",
1336
+ label: statistics.label,
1337
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN as STATISTICS_TYPE,
1338
+ trigger: statistics.trigger,
1339
+ count: metadata?.inputTokens,
1340
+ user: user.id,
1341
+ role: user?.role?.id,
1342
+ }),
1343
+ ]
1344
+ : []),
1345
+ ...(metadata?.outputTokens
1346
+ ? [
1347
+ updateStatistic({
1348
+ name: "outputTokens",
1349
+ label: statistics.label,
1350
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN as STATISTICS_TYPE,
1351
+ trigger: statistics.trigger,
1352
+ count: metadata?.outputTokens,
1353
+ }),
1354
+ ]
1355
+ : []),
1356
+ ]);
1357
+ resolve({
1358
+ messages,
1359
+ metadata: {
1360
+ tokens: {
1361
+ totalTokens: messageHistory.metadata.tokens.totalTokens + metadata?.totalTokens,
1362
+ reasoningTokens:
1363
+ messageHistory.metadata.tokens.reasoningTokens + metadata?.reasoningTokens,
1364
+ inputTokens: messageHistory.metadata.tokens.inputTokens + metadata?.inputTokens,
1365
+ outputTokens:
1366
+ messageHistory.metadata.tokens.outputTokens + metadata?.outputTokens,
1367
+ cachedInputTokens:
1368
+ messageHistory.metadata.tokens.cachedInputTokens + metadata?.cachedInputTokens,
1369
+ },
1370
+ duration: messageHistory.metadata.duration + (Date.now() - startTime),
1371
+ },
1372
+ });
1373
+ },
1374
+ });
1375
+
1376
+ // Consume the stream to ensure it runs to completion & triggers onFinish
1377
+ for await (const message of stream) {
1378
+ console.log("[EXULU] message", message);
1379
+ }
1380
+ } catch (error: unknown) {
1381
+ console.error(
1382
+ `[EXULU] error generating stream for agent ${agent.name} (${agent.id}).`,
1383
+ error,
1384
+ );
1385
+ reject(new Error(error instanceof Error ? error.message : String(error)));
1386
+ }
1387
+ });
1388
+ index++;
1389
+ }
1390
+ console.log(
1391
+ "[EXULU] finished processing UI messages flow for agent, messages result",
1392
+ messageHistory,
1393
+ );
1394
+ return messageHistory;
1395
+ };
1396
+
1397
+ function getMedian(arr: number[]): number {
1398
+ if (arr.length === 0) return 0; // Handle empty array
1399
+
1400
+ // Step 1: Sort the array
1401
+ const sortedArr = arr.slice().sort((a, b) => a - b);
1402
+
1403
+ const mid = Math.floor(sortedArr.length / 2);
1404
+
1405
+ // Step 2 & 3: Compute median
1406
+ if (sortedArr.length % 2 !== 0) {
1407
+ // Odd length
1408
+ return sortedArr[mid]!;
1409
+ } else {
1410
+ // Even length
1411
+ return (sortedArr[mid - 1]! + sortedArr[mid]!) / 2;
1412
+ }
1413
+ }
1414
+
1415
+ function getSum(arr: number[]): number {
1416
+ if (arr.length === 0) return 0; // Handle empty array
1417
+ return arr.reduce((a, b) => a + b, 0);
1418
+ }
1419
+
1420
+ function getAverage(arr: number[]): number {
1421
+ if (arr.length === 0) return 0; // Handle empty array
1422
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
1423
+ }