@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.
- package/.agents/skills/mintlify/SKILL.md +347 -0
- package/.editorconfig +15 -0
- package/.eslintrc.json +52 -0
- package/.jscpd.json +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc.json +12 -0
- package/CHANGELOG.md +2 -2
- package/README.md +747 -0
- package/SECURITY.md +5 -0
- package/dist/index.cjs +11747 -10227
- package/dist/index.d.cts +725 -667
- package/dist/index.d.ts +725 -667
- package/dist/index.js +12043 -10516
- package/ee/LICENSE.md +62 -0
- package/ee/agentic-retrieval/index.ts +1109 -0
- package/ee/documents/THIRD_PARTY_LICENSES/docling.txt +31 -0
- package/ee/documents/processing/build_pdf_processor.sh +35 -0
- package/ee/documents/processing/chunk_markdown.py +263 -0
- package/ee/documents/processing/doc_processor.ts +635 -0
- package/ee/documents/processing/pdf_processor.spec +115 -0
- package/ee/documents/processing/pdf_to_markdown.py +420 -0
- package/ee/documents/processing/requirements.txt +4 -0
- package/ee/entitlements.ts +49 -0
- package/ee/markdown.ts +686 -0
- package/ee/queues/decorator.ts +140 -0
- package/ee/queues/queues.ts +156 -0
- package/ee/queues/server.ts +6 -0
- package/ee/rbac-resolver.ts +51 -0
- package/ee/rbac-update.ts +111 -0
- package/ee/schemas.ts +348 -0
- package/ee/tokenizer.ts +80 -0
- package/ee/workers.ts +1423 -0
- package/eslint.config.js +88 -0
- package/jest.config.ts +25 -0
- package/license.md +73 -49
- package/mintlify-docs/.mintignore +7 -0
- package/mintlify-docs/AGENTS.md +33 -0
- package/mintlify-docs/CLAUDE.MD +50 -0
- package/mintlify-docs/CONTRIBUTING.md +32 -0
- package/mintlify-docs/LICENSE +21 -0
- package/mintlify-docs/README.md +55 -0
- package/mintlify-docs/ai-tools/claude-code.mdx +43 -0
- package/mintlify-docs/ai-tools/cursor.mdx +39 -0
- package/mintlify-docs/ai-tools/windsurf.mdx +39 -0
- package/mintlify-docs/api-reference/core-types/agent-types.mdx +110 -0
- package/mintlify-docs/api-reference/core-types/analytics-types.mdx +95 -0
- package/mintlify-docs/api-reference/core-types/configuration-types.mdx +83 -0
- package/mintlify-docs/api-reference/core-types/evaluation-types.mdx +106 -0
- package/mintlify-docs/api-reference/core-types/job-types.mdx +135 -0
- package/mintlify-docs/api-reference/core-types/overview.mdx +73 -0
- package/mintlify-docs/api-reference/core-types/prompt-types.mdx +102 -0
- package/mintlify-docs/api-reference/core-types/rbac-types.mdx +163 -0
- package/mintlify-docs/api-reference/core-types/session-types.mdx +77 -0
- package/mintlify-docs/api-reference/core-types/user-management.mdx +112 -0
- package/mintlify-docs/api-reference/core-types/workflow-types.mdx +88 -0
- package/mintlify-docs/api-reference/core-types.mdx +585 -0
- package/mintlify-docs/api-reference/dynamic-types.mdx +851 -0
- package/mintlify-docs/api-reference/endpoint/create.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/delete.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/get.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/webhook.mdx +4 -0
- package/mintlify-docs/api-reference/introduction.mdx +661 -0
- package/mintlify-docs/api-reference/mutations.mdx +1012 -0
- package/mintlify-docs/api-reference/openapi.json +217 -0
- package/mintlify-docs/api-reference/queries.mdx +1154 -0
- package/mintlify-docs/backend/introduction.mdx +218 -0
- package/mintlify-docs/changelog.mdx +387 -0
- package/mintlify-docs/community-edition.mdx +304 -0
- package/mintlify-docs/core/exulu-agent/api-reference.mdx +894 -0
- package/mintlify-docs/core/exulu-agent/configuration.mdx +690 -0
- package/mintlify-docs/core/exulu-agent/introduction.mdx +552 -0
- package/mintlify-docs/core/exulu-app/api-reference.mdx +481 -0
- package/mintlify-docs/core/exulu-app/configuration.mdx +319 -0
- package/mintlify-docs/core/exulu-app/introduction.mdx +117 -0
- package/mintlify-docs/core/exulu-authentication.mdx +810 -0
- package/mintlify-docs/core/exulu-chunkers/api-reference.mdx +1011 -0
- package/mintlify-docs/core/exulu-chunkers/configuration.mdx +596 -0
- package/mintlify-docs/core/exulu-chunkers/introduction.mdx +403 -0
- package/mintlify-docs/core/exulu-context/api-reference.mdx +911 -0
- package/mintlify-docs/core/exulu-context/configuration.mdx +648 -0
- package/mintlify-docs/core/exulu-context/introduction.mdx +394 -0
- package/mintlify-docs/core/exulu-database.mdx +811 -0
- package/mintlify-docs/core/exulu-default-agents.mdx +545 -0
- package/mintlify-docs/core/exulu-eval/api-reference.mdx +772 -0
- package/mintlify-docs/core/exulu-eval/configuration.mdx +680 -0
- package/mintlify-docs/core/exulu-eval/introduction.mdx +459 -0
- package/mintlify-docs/core/exulu-logging.mdx +464 -0
- package/mintlify-docs/core/exulu-otel.mdx +670 -0
- package/mintlify-docs/core/exulu-queues/api-reference.mdx +648 -0
- package/mintlify-docs/core/exulu-queues/configuration.mdx +650 -0
- package/mintlify-docs/core/exulu-queues/introduction.mdx +474 -0
- package/mintlify-docs/core/exulu-reranker/api-reference.mdx +630 -0
- package/mintlify-docs/core/exulu-reranker/configuration.mdx +663 -0
- package/mintlify-docs/core/exulu-reranker/introduction.mdx +516 -0
- package/mintlify-docs/core/exulu-tool/api-reference.mdx +723 -0
- package/mintlify-docs/core/exulu-tool/configuration.mdx +805 -0
- package/mintlify-docs/core/exulu-tool/introduction.mdx +539 -0
- package/mintlify-docs/core/exulu-variables/api-reference.mdx +699 -0
- package/mintlify-docs/core/exulu-variables/configuration.mdx +736 -0
- package/mintlify-docs/core/exulu-variables/introduction.mdx +511 -0
- package/mintlify-docs/development.mdx +94 -0
- package/mintlify-docs/docs.json +248 -0
- package/mintlify-docs/enterprise-edition.mdx +538 -0
- package/mintlify-docs/essentials/code.mdx +35 -0
- package/mintlify-docs/essentials/images.mdx +59 -0
- package/mintlify-docs/essentials/markdown.mdx +88 -0
- package/mintlify-docs/essentials/navigation.mdx +87 -0
- package/mintlify-docs/essentials/reusable-snippets.mdx +110 -0
- package/mintlify-docs/essentials/settings.mdx +318 -0
- package/mintlify-docs/favicon.svg +3 -0
- package/mintlify-docs/frontend/introduction.mdx +39 -0
- package/mintlify-docs/getting-started.mdx +267 -0
- package/mintlify-docs/guides/custom-agent.mdx +608 -0
- package/mintlify-docs/guides/first-agent.mdx +315 -0
- package/mintlify-docs/images/admin_ui.png +0 -0
- package/mintlify-docs/images/contexts.png +0 -0
- package/mintlify-docs/images/create_agents.png +0 -0
- package/mintlify-docs/images/evals.png +0 -0
- package/mintlify-docs/images/graphql.png +0 -0
- package/mintlify-docs/images/graphql_api.png +0 -0
- package/mintlify-docs/images/hero-dark.png +0 -0
- package/mintlify-docs/images/hero-light.png +0 -0
- package/mintlify-docs/images/hero.png +0 -0
- package/mintlify-docs/images/knowledge_sources.png +0 -0
- package/mintlify-docs/images/mcp.png +0 -0
- package/mintlify-docs/images/scaling.png +0 -0
- package/mintlify-docs/index.mdx +411 -0
- package/mintlify-docs/logo/dark.svg +9 -0
- package/mintlify-docs/logo/light.svg +9 -0
- package/mintlify-docs/partners.mdx +558 -0
- package/mintlify-docs/products.mdx +77 -0
- package/mintlify-docs/snippets/snippet-intro.mdx +4 -0
- package/mintlify-docs/styles.css +207 -0
- package/package.json +35 -4
- package/skills-lock.json +10 -0
- package/types/context-processor.ts +45 -0
- package/types/exulu-table-definition.ts +79 -0
- package/types/file-types.ts +18 -0
- package/types/models/agent.ts +10 -12
- package/types/models/exulu-agent-tool-config.ts +11 -0
- package/types/models/rate-limiter-rules.ts +7 -0
- package/types/provider-config.ts +21 -0
- package/types/queue-config.ts +16 -0
- package/types/rbac-rights-modes.ts +1 -0
- package/types/statistics.ts +20 -0
- package/types/workflow.ts +31 -0
- package/changelogs/10.11.2025_03.12.2025.md +0 -316
- package/documentation/logging.md +0 -122
- package/documentation/otel.md +0 -145
- package/types/models/agent-backend.ts +0 -15
- /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
|
+
}
|