@exulu/backend 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +335 -139
- package/dist/index.d.cts +62 -9
- package/dist/index.d.ts +62 -9
- package/dist/index.js +327 -132
- package/package.json +10 -9
- package/types/enums/jobs.ts +11 -0
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
+
// src/index.ts
|
|
9
|
+
import "dotenv/config";
|
|
10
|
+
|
|
8
11
|
// src/redis/client.ts
|
|
9
12
|
import { createClient } from "redis";
|
|
10
13
|
|
|
@@ -114,6 +117,10 @@ var usersSchema = {
|
|
|
114
117
|
name: "firstname",
|
|
115
118
|
type: "text"
|
|
116
119
|
},
|
|
120
|
+
{
|
|
121
|
+
name: "name",
|
|
122
|
+
type: "text"
|
|
123
|
+
},
|
|
117
124
|
{
|
|
118
125
|
name: "lastname",
|
|
119
126
|
type: "text"
|
|
@@ -217,6 +224,70 @@ var statisticsSchema = {
|
|
|
217
224
|
}
|
|
218
225
|
]
|
|
219
226
|
};
|
|
227
|
+
var workflowSchema = {
|
|
228
|
+
name: {
|
|
229
|
+
plural: "workflows",
|
|
230
|
+
singular: "workflow"
|
|
231
|
+
},
|
|
232
|
+
fields: [
|
|
233
|
+
{
|
|
234
|
+
name: "workflow_name",
|
|
235
|
+
type: "text"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "run_id",
|
|
239
|
+
type: "text"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "snapshot",
|
|
243
|
+
type: "text"
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
var threadsSchema = {
|
|
248
|
+
name: {
|
|
249
|
+
plural: "threads",
|
|
250
|
+
singular: "thread"
|
|
251
|
+
},
|
|
252
|
+
fields: [
|
|
253
|
+
{
|
|
254
|
+
name: "resourceId",
|
|
255
|
+
type: "text"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: "title",
|
|
259
|
+
type: "text"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "metadata",
|
|
263
|
+
type: "text"
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
var messagesSchema = {
|
|
268
|
+
name: {
|
|
269
|
+
plural: "messages",
|
|
270
|
+
singular: "message"
|
|
271
|
+
},
|
|
272
|
+
fields: [
|
|
273
|
+
{
|
|
274
|
+
name: "thread_id",
|
|
275
|
+
type: "text"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "content",
|
|
279
|
+
type: "text"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "role",
|
|
283
|
+
type: "text"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "type",
|
|
287
|
+
type: "text"
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
};
|
|
220
291
|
var jobsSchema = {
|
|
221
292
|
name: {
|
|
222
293
|
plural: "jobs",
|
|
@@ -241,7 +312,7 @@ var jobsSchema = {
|
|
|
241
312
|
},
|
|
242
313
|
{
|
|
243
314
|
name: "result",
|
|
244
|
-
type: "
|
|
315
|
+
type: "longText"
|
|
245
316
|
},
|
|
246
317
|
{
|
|
247
318
|
name: "name",
|
|
@@ -251,6 +322,10 @@ var jobsSchema = {
|
|
|
251
322
|
name: "agent",
|
|
252
323
|
type: "text"
|
|
253
324
|
},
|
|
325
|
+
{
|
|
326
|
+
name: "workflow",
|
|
327
|
+
type: "text"
|
|
328
|
+
},
|
|
254
329
|
{
|
|
255
330
|
name: "user",
|
|
256
331
|
type: "text"
|
|
@@ -259,6 +334,10 @@ var jobsSchema = {
|
|
|
259
334
|
name: "item",
|
|
260
335
|
type: "text"
|
|
261
336
|
},
|
|
337
|
+
{
|
|
338
|
+
name: "steps",
|
|
339
|
+
type: "number"
|
|
340
|
+
},
|
|
262
341
|
{
|
|
263
342
|
name: "inputs",
|
|
264
343
|
type: "json"
|
|
@@ -362,14 +441,11 @@ var sanitizeName = (name) => {
|
|
|
362
441
|
return name.toLowerCase().replace(/ /g, "_");
|
|
363
442
|
};
|
|
364
443
|
|
|
365
|
-
// src/postgres/init-db.ts
|
|
366
|
-
import bcrypt2 from "bcryptjs";
|
|
367
|
-
|
|
368
444
|
// src/auth/generate-key.ts
|
|
369
445
|
import bcrypt from "bcryptjs";
|
|
370
446
|
var SALT_ROUNDS = 12;
|
|
371
|
-
async function
|
|
372
|
-
const hash = await bcrypt.hash(
|
|
447
|
+
async function encryptString(string) {
|
|
448
|
+
const hash = await bcrypt.hash(string, SALT_ROUNDS);
|
|
373
449
|
return hash;
|
|
374
450
|
}
|
|
375
451
|
var generateApiKey = async (name, email) => {
|
|
@@ -391,7 +467,7 @@ var generateApiKey = async (name, email) => {
|
|
|
391
467
|
const newKeyName = name;
|
|
392
468
|
const plainKey = `sk_${Math.random().toString(36).substring(2, 15)}_${Math.random().toString(36).substring(2, 15)}`;
|
|
393
469
|
const postFix = `/${newKeyName.toLowerCase().trim().replaceAll(" ", "_")}`;
|
|
394
|
-
const encryptedKey = await
|
|
470
|
+
const encryptedKey = await encryptString(plainKey);
|
|
395
471
|
const existingApiUser = await db2.from("users").where({ email }).first();
|
|
396
472
|
if (!existingApiUser) {
|
|
397
473
|
console.log("[EXULU] Creating default api user.");
|
|
@@ -402,6 +478,7 @@ var generateApiKey = async (name, email) => {
|
|
|
402
478
|
createdAt: /* @__PURE__ */ new Date(),
|
|
403
479
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
404
480
|
type: "api",
|
|
481
|
+
emailVerified: /* @__PURE__ */ new Date(),
|
|
405
482
|
apikey: `${encryptedKey}${postFix}`,
|
|
406
483
|
// password: "admin", todo add this again when we implement password auth / encryption as alternative to OTP
|
|
407
484
|
role: roleId
|
|
@@ -566,11 +643,6 @@ var up = async function(knex) {
|
|
|
566
643
|
});
|
|
567
644
|
}
|
|
568
645
|
};
|
|
569
|
-
var SALT_ROUNDS2 = 12;
|
|
570
|
-
async function encryptApiKey2(apikey) {
|
|
571
|
-
const hash = await bcrypt2.hash(apikey, SALT_ROUNDS2);
|
|
572
|
-
return hash;
|
|
573
|
-
}
|
|
574
646
|
var execute = async () => {
|
|
575
647
|
console.log("[EXULU] Initializing database.");
|
|
576
648
|
const { db: db2 } = await postgresClient();
|
|
@@ -583,33 +655,33 @@ var execute = async () => {
|
|
|
583
655
|
const role = await db2.from("roles").insert({
|
|
584
656
|
name: "admin",
|
|
585
657
|
is_admin: true,
|
|
586
|
-
agents: []
|
|
658
|
+
agents: JSON.stringify([])
|
|
587
659
|
}).returning("id");
|
|
588
660
|
roleId = role[0].id;
|
|
589
661
|
} else {
|
|
590
662
|
roleId = existingRole.id;
|
|
591
663
|
}
|
|
592
|
-
const newKeyName = "exulu_default_key";
|
|
593
|
-
const plainKey = `sk_${Math.random().toString(36).substring(2, 15)}_${Math.random().toString(36).substring(2, 15)}`;
|
|
594
|
-
const postFix = `/${newKeyName.toLowerCase().trim().replaceAll(" ", "_")}`;
|
|
595
|
-
const encryptedKey = await encryptApiKey2(plainKey);
|
|
596
664
|
const existingUser = await db2.from("users").where({ email: "admin@exulu.com" }).first();
|
|
597
665
|
if (!existingUser) {
|
|
666
|
+
const password = await encryptString("admin");
|
|
598
667
|
console.log("[EXULU] Creating default admin user.");
|
|
599
668
|
await db2.from("users").insert({
|
|
600
669
|
name: "exulu",
|
|
601
670
|
email: "admin@exulu.com",
|
|
602
671
|
super_admin: true,
|
|
603
672
|
createdAt: /* @__PURE__ */ new Date(),
|
|
673
|
+
emailVerified: /* @__PURE__ */ new Date(),
|
|
604
674
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
675
|
+
password,
|
|
605
676
|
type: "user",
|
|
606
|
-
// password: "admin", todo add this again when we implement password auth / encryption as alternative to OTP
|
|
607
677
|
role: roleId
|
|
608
678
|
});
|
|
609
679
|
}
|
|
610
680
|
const { key } = await generateApiKey("exulu", "api@exulu.com");
|
|
611
681
|
console.log("[EXULU] Database initialized.");
|
|
612
682
|
console.log("[EXULU] Default api key: ", `${key}`);
|
|
683
|
+
console.log("[EXULU] Default password if using password auth: ", `admin`);
|
|
684
|
+
console.log("[EXULU] Default email if using password auth: ", `admin@exulu.com`);
|
|
613
685
|
return;
|
|
614
686
|
};
|
|
615
687
|
|
|
@@ -620,7 +692,6 @@ import { Agent as MastraAgent } from "@mastra/core";
|
|
|
620
692
|
import { z } from "zod";
|
|
621
693
|
import * as fs from "fs";
|
|
622
694
|
import * as path from "path";
|
|
623
|
-
import "bullmq";
|
|
624
695
|
import { Memory } from "@mastra/memory";
|
|
625
696
|
import { PostgresStore, PgVector } from "@mastra/pg";
|
|
626
697
|
|
|
@@ -656,6 +727,7 @@ var bullmqDecorator = async ({
|
|
|
656
727
|
configuration,
|
|
657
728
|
updater,
|
|
658
729
|
context,
|
|
730
|
+
steps,
|
|
659
731
|
source,
|
|
660
732
|
documents,
|
|
661
733
|
trigger,
|
|
@@ -673,11 +745,13 @@ var bullmqDecorator = async ({
|
|
|
673
745
|
...context && { context },
|
|
674
746
|
...source && { source },
|
|
675
747
|
...documents && { documents },
|
|
748
|
+
...steps && { steps },
|
|
676
749
|
...trigger && { trigger },
|
|
677
750
|
...item && { item },
|
|
678
751
|
agent,
|
|
679
752
|
user,
|
|
680
753
|
inputs,
|
|
754
|
+
label,
|
|
681
755
|
session
|
|
682
756
|
},
|
|
683
757
|
{
|
|
@@ -702,6 +776,7 @@ var bullmqDecorator = async ({
|
|
|
702
776
|
...embedder && { embedder },
|
|
703
777
|
...workflow && { workflow },
|
|
704
778
|
...configuration && { configuration },
|
|
779
|
+
...steps && { steps },
|
|
705
780
|
...updater && { updater },
|
|
706
781
|
...context && { context },
|
|
707
782
|
...source && { source },
|
|
@@ -725,6 +800,17 @@ var bullmqDecorator = async ({
|
|
|
725
800
|
};
|
|
726
801
|
};
|
|
727
802
|
|
|
803
|
+
// types/enums/jobs.ts
|
|
804
|
+
var JOB_STATUS_ENUM = {
|
|
805
|
+
completed: "completed",
|
|
806
|
+
failed: "failed",
|
|
807
|
+
delayed: "delayed",
|
|
808
|
+
active: "active",
|
|
809
|
+
waiting: "waiting",
|
|
810
|
+
paused: "paused",
|
|
811
|
+
stuck: "stuck"
|
|
812
|
+
};
|
|
813
|
+
|
|
728
814
|
// src/registry/classes.ts
|
|
729
815
|
function generateSlug(name) {
|
|
730
816
|
const normalized = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
|
|
@@ -759,6 +845,7 @@ var ExuluAgent = class {
|
|
|
759
845
|
config;
|
|
760
846
|
memory;
|
|
761
847
|
tools;
|
|
848
|
+
agent;
|
|
762
849
|
capabilities;
|
|
763
850
|
constructor({ id, name, description, outputSchema, config, rateLimit, type, capabilities, tools }) {
|
|
764
851
|
this.id = id;
|
|
@@ -771,6 +858,14 @@ var ExuluAgent = class {
|
|
|
771
858
|
this.config = config;
|
|
772
859
|
this.capabilities = capabilities;
|
|
773
860
|
this.slug = `/agents/${generateSlug(this.name)}/run`;
|
|
861
|
+
if (this.type === "agent") {
|
|
862
|
+
this.agent = new MastraAgent({
|
|
863
|
+
name: this.config.name,
|
|
864
|
+
instructions: this.config.instructions,
|
|
865
|
+
model: this.config.model,
|
|
866
|
+
memory: this.memory ? this.memory : void 0
|
|
867
|
+
});
|
|
868
|
+
}
|
|
774
869
|
if (config?.memory) {
|
|
775
870
|
console.log("[EXULU] Initializing memory for agent " + this.name);
|
|
776
871
|
const connectionString = `postgresql://${process.env.POSTGRES_DB_USER}:${process.env.POSTGRES_DB_PASSWORD}@${process.env.POSTGRES_DB_HOST}:${process.env.POSTGRES_DB_PORT}/exulu`;
|
|
@@ -784,7 +879,11 @@ var ExuluAgent = class {
|
|
|
784
879
|
password: process.env.POSTGRES_DB_PASSWORD || "",
|
|
785
880
|
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
|
|
786
881
|
}),
|
|
787
|
-
...config?.memory.vector ? {
|
|
882
|
+
...config?.memory.vector ? {
|
|
883
|
+
vector: new PgVector({
|
|
884
|
+
connectionString
|
|
885
|
+
})
|
|
886
|
+
} : {},
|
|
788
887
|
options: {
|
|
789
888
|
lastMessages: config?.memory.lastMessages || 10,
|
|
790
889
|
semanticRecall: {
|
|
@@ -795,33 +894,17 @@ var ExuluAgent = class {
|
|
|
795
894
|
});
|
|
796
895
|
}
|
|
797
896
|
}
|
|
798
|
-
chat =
|
|
799
|
-
|
|
800
|
-
const agent = await db2.from("agents").select("*").where("id", "=", id).first();
|
|
801
|
-
if (!agent) {
|
|
897
|
+
chat = () => {
|
|
898
|
+
if (!this.agent) {
|
|
802
899
|
throw new Error("Agent not found");
|
|
803
900
|
}
|
|
804
|
-
let tools = {};
|
|
805
|
-
agent.tools?.forEach(({ name }) => {
|
|
806
|
-
const tool = this.tools?.find((t) => t.name === name);
|
|
807
|
-
if (!tool) {
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
return tool;
|
|
811
|
-
});
|
|
812
901
|
updateStatistic({
|
|
813
902
|
name: "count",
|
|
814
903
|
label: this.name,
|
|
815
904
|
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
816
905
|
trigger: "agent"
|
|
817
906
|
});
|
|
818
|
-
return
|
|
819
|
-
name: this.config.name,
|
|
820
|
-
instructions: this.config.instructions,
|
|
821
|
-
model: this.config.model,
|
|
822
|
-
tools,
|
|
823
|
-
memory: this.memory ? this.memory : void 0
|
|
824
|
-
});
|
|
907
|
+
return this.agent;
|
|
825
908
|
};
|
|
826
909
|
};
|
|
827
910
|
var ExuluEmbedder = class {
|
|
@@ -878,35 +961,141 @@ var ExuluWorkflow = class {
|
|
|
878
961
|
enable_batch = false;
|
|
879
962
|
slug = "";
|
|
880
963
|
queue;
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
constructor({ id, name, description, workflow, queue, enable_batch, inputSchema }) {
|
|
964
|
+
steps;
|
|
965
|
+
constructor({ id, name, description, steps, queue, enable_batch }) {
|
|
884
966
|
this.id = id;
|
|
885
967
|
this.name = name;
|
|
886
968
|
this.description = description;
|
|
887
969
|
this.enable_batch = enable_batch;
|
|
888
970
|
this.slug = `/workflows/${generateSlug(this.name)}/run`;
|
|
889
971
|
this.queue = queue;
|
|
890
|
-
this.
|
|
891
|
-
this.workflow = workflow;
|
|
972
|
+
this.steps = steps;
|
|
892
973
|
}
|
|
974
|
+
start = async ({
|
|
975
|
+
inputs: initialInputs,
|
|
976
|
+
user,
|
|
977
|
+
logger,
|
|
978
|
+
job,
|
|
979
|
+
session,
|
|
980
|
+
agent,
|
|
981
|
+
label
|
|
982
|
+
}) => {
|
|
983
|
+
let inputs;
|
|
984
|
+
const { db: db2 } = await postgresClient();
|
|
985
|
+
if (!job?.id) {
|
|
986
|
+
logger.write(`Creating new job for workflow ${this.name} with inputs: ${JSON.stringify(initialInputs)}`, "INFO");
|
|
987
|
+
const result = await db2("jobs").insert({
|
|
988
|
+
status: JOB_STATUS_ENUM.active,
|
|
989
|
+
name: `Job running '${this.name}' for '${label}'`,
|
|
990
|
+
agent,
|
|
991
|
+
workflow: this.id,
|
|
992
|
+
type: "workflow",
|
|
993
|
+
steps: this.steps?.length || 0,
|
|
994
|
+
inputs: initialInputs,
|
|
995
|
+
session,
|
|
996
|
+
user
|
|
997
|
+
}).returning(["id", "status"]);
|
|
998
|
+
job = result[0];
|
|
999
|
+
logger.write(`Created new job for workflow ${this.name}, job id: ${job?.id}`, "INFO");
|
|
1000
|
+
}
|
|
1001
|
+
if (!job) {
|
|
1002
|
+
throw new Error("Job not found, or failed to be created.");
|
|
1003
|
+
}
|
|
1004
|
+
if (job.status !== JOB_STATUS_ENUM.active) {
|
|
1005
|
+
await db2("jobs").update({
|
|
1006
|
+
status: JOB_STATUS_ENUM.active,
|
|
1007
|
+
inputs: initialInputs
|
|
1008
|
+
}).where({ id: job?.id }).returning("id");
|
|
1009
|
+
}
|
|
1010
|
+
const runStep = async (step, inputs2) => {
|
|
1011
|
+
let result;
|
|
1012
|
+
try {
|
|
1013
|
+
result = await step.fn({
|
|
1014
|
+
inputs: inputs2,
|
|
1015
|
+
logger,
|
|
1016
|
+
job,
|
|
1017
|
+
user
|
|
1018
|
+
});
|
|
1019
|
+
return result;
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
logger.write(`Step ${step.name} failed with error: ${error.message}`, "ERROR");
|
|
1022
|
+
if (step.retries && step.retries > 0) {
|
|
1023
|
+
logger.write(`Retrying step ${step.name} with ${step.retries} retries left`, "INFO");
|
|
1024
|
+
step.retries--;
|
|
1025
|
+
let result2 = await runStep(step, inputs2);
|
|
1026
|
+
return result2;
|
|
1027
|
+
}
|
|
1028
|
+
logger.write(`Step ${step.name} failed with error: ${error.message}`, "ERROR");
|
|
1029
|
+
throw error;
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
let final;
|
|
1033
|
+
try {
|
|
1034
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
1035
|
+
const step = this.steps[i];
|
|
1036
|
+
if (!step) {
|
|
1037
|
+
throw new Error("Step not found.");
|
|
1038
|
+
}
|
|
1039
|
+
if (i === 0) {
|
|
1040
|
+
inputs = initialInputs;
|
|
1041
|
+
}
|
|
1042
|
+
logger.write(`Running step ${step.name} with inputs: ${JSON.stringify(inputs)}`, "INFO");
|
|
1043
|
+
let result = await runStep(step, inputs);
|
|
1044
|
+
inputs = result;
|
|
1045
|
+
logger.write(`Step ${step.name} output: ${JSON.stringify(result)}`, "INFO");
|
|
1046
|
+
final = result;
|
|
1047
|
+
}
|
|
1048
|
+
await db2("jobs").update({
|
|
1049
|
+
status: JOB_STATUS_ENUM.completed,
|
|
1050
|
+
result: JSON.stringify(final),
|
|
1051
|
+
finished_at: db2.fn.now()
|
|
1052
|
+
}).where({ id: job?.id }).returning("id");
|
|
1053
|
+
return final;
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
logger.write(`Workflow ${this.name} failed with error: ${error.message} for job ${job?.id}`, "ERROR");
|
|
1056
|
+
await db2("jobs").update({
|
|
1057
|
+
status: JOB_STATUS_ENUM.failed,
|
|
1058
|
+
result: JSON.stringify({
|
|
1059
|
+
error: error.message || error,
|
|
1060
|
+
stack: error.stack || "No stack trace available"
|
|
1061
|
+
})
|
|
1062
|
+
}).where({ id: job?.id }).returning("id");
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
893
1066
|
};
|
|
894
1067
|
var ExuluLogger = class {
|
|
895
1068
|
logPath;
|
|
896
1069
|
job;
|
|
897
1070
|
constructor(job, logsDir) {
|
|
898
1071
|
this.job = job;
|
|
899
|
-
if (
|
|
900
|
-
fs.
|
|
1072
|
+
if (logsDir && job) {
|
|
1073
|
+
if (!fs.existsSync(logsDir)) {
|
|
1074
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
1075
|
+
}
|
|
1076
|
+
this.logPath = path.join(logsDir, `${job.id}_${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.txt`);
|
|
901
1077
|
}
|
|
902
|
-
this.logPath = path.join(logsDir, `${job.id}_${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.txt`);
|
|
903
1078
|
}
|
|
904
1079
|
async write(message, level) {
|
|
905
1080
|
const logMessage = message.endsWith("\n") ? message : message + "\n";
|
|
1081
|
+
if (!this.logPath) {
|
|
1082
|
+
switch (level) {
|
|
1083
|
+
case "INFO":
|
|
1084
|
+
console.log(message);
|
|
1085
|
+
break;
|
|
1086
|
+
case "WARNING":
|
|
1087
|
+
console.warn(message);
|
|
1088
|
+
break;
|
|
1089
|
+
case "ERROR":
|
|
1090
|
+
console.error(message);
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
906
1095
|
try {
|
|
907
1096
|
await fs.promises.appendFile(this.logPath, `[EXULU][${level}] - ${(/* @__PURE__ */ new Date()).toISOString()}: ${logMessage}`);
|
|
908
1097
|
} catch (error) {
|
|
909
|
-
console.error(`Error writing to log file ${this.job.id}:`, error);
|
|
1098
|
+
console.error(`Error writing to log file ${this.job ? this.job.id : "unknown job"}:`, error);
|
|
910
1099
|
throw error;
|
|
911
1100
|
}
|
|
912
1101
|
}
|
|
@@ -1469,7 +1658,7 @@ import "express";
|
|
|
1469
1658
|
import { getToken } from "next-auth/jwt";
|
|
1470
1659
|
|
|
1471
1660
|
// src/auth/auth.ts
|
|
1472
|
-
import
|
|
1661
|
+
import bcrypt2 from "bcryptjs";
|
|
1473
1662
|
var authentication = async ({
|
|
1474
1663
|
apikey,
|
|
1475
1664
|
authtoken,
|
|
@@ -1562,16 +1751,14 @@ var authentication = async ({
|
|
|
1562
1751
|
code: 401
|
|
1563
1752
|
};
|
|
1564
1753
|
}
|
|
1565
|
-
console.log("[EXULU] users", users);
|
|
1566
1754
|
console.log("[EXULU] request_key_name", request_key_name);
|
|
1567
1755
|
console.log("[EXULU] request_key_compare_value", request_key_compare_value);
|
|
1568
1756
|
const filtered = users.filter(({ apikey: apikey2, id }) => apikey2.includes(request_key_name));
|
|
1569
|
-
console.log("[EXULU] filtered", filtered);
|
|
1570
1757
|
for (const user of filtered) {
|
|
1571
1758
|
const user_key_last_slash_index = user.apikey.lastIndexOf("/");
|
|
1572
1759
|
const user_key_compare_value = user.apikey.substring(0, user_key_last_slash_index);
|
|
1573
1760
|
console.log("[EXULU] user_key_compare_value", user_key_compare_value);
|
|
1574
|
-
const isMatch = await
|
|
1761
|
+
const isMatch = await bcrypt2.compare(request_key_compare_value, user_key_compare_value);
|
|
1575
1762
|
console.log("[EXULU] isMatch", isMatch);
|
|
1576
1763
|
if (isMatch) {
|
|
1577
1764
|
await db2.from("users").where({ id: user.id }).update({
|
|
@@ -1793,6 +1980,7 @@ var VectorMethodEnum = {
|
|
|
1793
1980
|
// src/registry/routes.ts
|
|
1794
1981
|
import express from "express";
|
|
1795
1982
|
import { ApolloServer } from "@apollo/server";
|
|
1983
|
+
import * as Papa from "papaparse";
|
|
1796
1984
|
import cors from "cors";
|
|
1797
1985
|
import "reflect-metadata";
|
|
1798
1986
|
|
|
@@ -2129,8 +2317,8 @@ import { expressMiddleware } from "@as-integrations/express5";
|
|
|
2129
2317
|
|
|
2130
2318
|
// src/registry/uppy.ts
|
|
2131
2319
|
import "express";
|
|
2320
|
+
import bodyParser from "body-parser";
|
|
2132
2321
|
import { getToken as getToken2 } from "next-auth/jwt";
|
|
2133
|
-
var bodyParser = __require("body-parser");
|
|
2134
2322
|
var createUppyRoutes = async (app) => {
|
|
2135
2323
|
const {
|
|
2136
2324
|
S3Client,
|
|
@@ -2526,7 +2714,6 @@ var createUppyRoutes = async (app) => {
|
|
|
2526
2714
|
|
|
2527
2715
|
// src/registry/routes.ts
|
|
2528
2716
|
import { InMemoryLRUCache } from "@apollo/utils.keyvaluecache";
|
|
2529
|
-
var Papa = __require("papaparse");
|
|
2530
2717
|
var global_queues = {
|
|
2531
2718
|
logs_cleaner: "logs-cleaner"
|
|
2532
2719
|
};
|
|
@@ -2624,7 +2811,7 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
2624
2811
|
} else {
|
|
2625
2812
|
console.log("===========================", "[EXULU] no redis server configured, not setting up recurring jobs.", "===========================");
|
|
2626
2813
|
}
|
|
2627
|
-
const schema = createSDL([usersSchema, rolesSchema, agentsSchema, jobsSchema]);
|
|
2814
|
+
const schema = createSDL([usersSchema, rolesSchema, agentsSchema, jobsSchema, workflowSchema, threadsSchema, messagesSchema]);
|
|
2628
2815
|
console.log("[EXULU] graphql server");
|
|
2629
2816
|
const server = new ApolloServer({
|
|
2630
2817
|
cache: new InMemoryLRUCache(),
|
|
@@ -2680,6 +2867,7 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
2680
2867
|
description: agent.description,
|
|
2681
2868
|
active: agent.active,
|
|
2682
2869
|
public: agent.public,
|
|
2870
|
+
type: agent.type,
|
|
2683
2871
|
slug: backend?.slug,
|
|
2684
2872
|
rateLimit: backend?.rateLimit,
|
|
2685
2873
|
streaming: backend?.streaming,
|
|
@@ -2889,21 +3077,27 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
2889
3077
|
}
|
|
2890
3078
|
const context = contexts.find((context2) => context2.id === req.params.context);
|
|
2891
3079
|
if (!context) {
|
|
3080
|
+
console.error("[EXULU] context not found in registry.", req.params.context);
|
|
2892
3081
|
res.status(400).json({
|
|
2893
3082
|
message: "Context not found in registry."
|
|
2894
3083
|
});
|
|
2895
3084
|
return;
|
|
2896
3085
|
}
|
|
3086
|
+
console.log("[EXULU] context", context);
|
|
2897
3087
|
const exists = await context.tableExists();
|
|
2898
3088
|
if (!exists) {
|
|
3089
|
+
console.log("[EXULU] context table does not exist, creating it.");
|
|
2899
3090
|
await context.createItemsTable();
|
|
2900
3091
|
}
|
|
3092
|
+
console.log("[EXULU] inserting item", req.body);
|
|
2901
3093
|
const result = await context.insertItem(authenticationResult.user.id, req.body, !!req.body.upsert);
|
|
3094
|
+
console.log("[EXULU] result", result);
|
|
2902
3095
|
res.status(200).json({
|
|
2903
3096
|
message: "Item created successfully.",
|
|
2904
3097
|
id: result
|
|
2905
3098
|
});
|
|
2906
3099
|
} catch (error) {
|
|
3100
|
+
console.error("[EXULU] error upserting item", error);
|
|
2907
3101
|
res.status(500).json({
|
|
2908
3102
|
message: error?.message || "An error occurred while creating the item."
|
|
2909
3103
|
});
|
|
@@ -3248,7 +3442,7 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
3248
3442
|
slug: workflow.slug,
|
|
3249
3443
|
enable_batch: workflow.enable_batch,
|
|
3250
3444
|
queue: workflow.queue?.name,
|
|
3251
|
-
inputSchema: workflow.inputSchema ? zerialize(workflow.inputSchema) : null
|
|
3445
|
+
inputSchema: workflow.steps[0]?.inputSchema ? zerialize(workflow.steps[0].inputSchema) : null
|
|
3252
3446
|
})));
|
|
3253
3447
|
});
|
|
3254
3448
|
console.log("[EXULU] workflow by id");
|
|
@@ -3275,7 +3469,7 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
3275
3469
|
res.status(200).json({
|
|
3276
3470
|
...workflow,
|
|
3277
3471
|
queue: workflow.queue?.name,
|
|
3278
|
-
inputSchema: workflow.inputSchema ? zerialize(workflow.inputSchema) : null,
|
|
3472
|
+
inputSchema: workflow.steps[0]?.inputSchema ? zerialize(workflow.steps[0].inputSchema) : null,
|
|
3279
3473
|
workflow: void 0
|
|
3280
3474
|
});
|
|
3281
3475
|
});
|
|
@@ -3411,10 +3605,6 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
3411
3605
|
note: `Execute workflow ${workflow.name}`
|
|
3412
3606
|
});
|
|
3413
3607
|
app.post(`${workflow.slug}`, async (req, res) => {
|
|
3414
|
-
if (!workflow.queue) {
|
|
3415
|
-
res.status(500).json({ detail: "No queue set for workflow." });
|
|
3416
|
-
return;
|
|
3417
|
-
}
|
|
3418
3608
|
const authenticationResult = await requestValidators.authenticate(req);
|
|
3419
3609
|
if (!authenticationResult.user?.id) {
|
|
3420
3610
|
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
@@ -3431,6 +3621,7 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
3431
3621
|
label: `Job running '${workflow.name}' for '${req.body.label}'`,
|
|
3432
3622
|
agent: req.body.agent,
|
|
3433
3623
|
workflow: workflow.id,
|
|
3624
|
+
steps: workflow.steps?.length || 0,
|
|
3434
3625
|
type: "workflow",
|
|
3435
3626
|
inputs,
|
|
3436
3627
|
session: req.body.session,
|
|
@@ -3449,22 +3640,19 @@ var createExpressRoutes = async (app, agents, embedders, tools, workflows, conte
|
|
|
3449
3640
|
});
|
|
3450
3641
|
return;
|
|
3451
3642
|
}
|
|
3452
|
-
const { runId, start, watch } = workflow.workflow.createRun();
|
|
3453
3643
|
console.log("[EXULU] running workflow with inputs.", inputs);
|
|
3454
|
-
const
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3644
|
+
const logger = new ExuluLogger();
|
|
3645
|
+
const result = await workflow.start({
|
|
3646
|
+
inputs,
|
|
3647
|
+
user: authenticationResult.user.id,
|
|
3648
|
+
logger,
|
|
3649
|
+
session: req.body.session,
|
|
3650
|
+
agent: req.body.agent,
|
|
3651
|
+
label: req.body.label
|
|
3459
3652
|
});
|
|
3460
|
-
const failedSteps = Object.entries(output.results).filter(([_, step]) => step.status === "failed").map(([id, step]) => `${id}: ${step.error}`);
|
|
3461
|
-
if (failedSteps.length > 0) {
|
|
3462
|
-
const message = `Workflow has failed steps: ${failedSteps.join("\n - ")}`;
|
|
3463
|
-
throw new Error(message);
|
|
3464
|
-
}
|
|
3465
3653
|
res.status(200).json({
|
|
3466
3654
|
"job": {},
|
|
3467
|
-
"output":
|
|
3655
|
+
"output": result
|
|
3468
3656
|
});
|
|
3469
3657
|
return;
|
|
3470
3658
|
});
|
|
@@ -3527,43 +3715,40 @@ import { Worker } from "bullmq";
|
|
|
3527
3715
|
// src/registry/utils.ts
|
|
3528
3716
|
import "bullmq";
|
|
3529
3717
|
var bullmq = {
|
|
3530
|
-
validate: (
|
|
3531
|
-
if (!
|
|
3532
|
-
throw new Error(`Missing job data for job ${
|
|
3718
|
+
validate: (bullmqJob) => {
|
|
3719
|
+
if (!bullmqJob.data) {
|
|
3720
|
+
throw new Error(`Missing job data for job ${bullmqJob.id}.`);
|
|
3533
3721
|
}
|
|
3534
|
-
if (!
|
|
3535
|
-
throw new Error(`Missing property "type" in data for job ${
|
|
3722
|
+
if (!bullmqJob.data.type) {
|
|
3723
|
+
throw new Error(`Missing property "type" in data for job ${bullmqJob.id}.`);
|
|
3536
3724
|
}
|
|
3537
|
-
if (!
|
|
3538
|
-
throw new Error(`Missing property "inputs" in data for job ${
|
|
3725
|
+
if (!bullmqJob.data.inputs) {
|
|
3726
|
+
throw new Error(`Missing property "inputs" in data for job ${bullmqJob.id}.`);
|
|
3539
3727
|
}
|
|
3540
|
-
if (
|
|
3541
|
-
throw new Error(`Property "type" in data for job ${
|
|
3728
|
+
if (bullmqJob.data.type !== "embedder" && bullmqJob.data.type !== "workflow") {
|
|
3729
|
+
throw new Error(`Property "type" in data for job ${bullmqJob.id} must be of value "embedder" or "workflow".`);
|
|
3542
3730
|
}
|
|
3543
|
-
if (!
|
|
3544
|
-
throw new Error(`Property "backend" in data for job ${
|
|
3731
|
+
if (!bullmqJob.data.workflow && !bullmqJob.data.embedder) {
|
|
3732
|
+
throw new Error(`Property "backend" in data for job ${bullmqJob.id} missing. Job data: ${JSON.stringify(bullmqJob)}`);
|
|
3545
3733
|
}
|
|
3546
3734
|
},
|
|
3547
3735
|
process: {
|
|
3548
|
-
workflow: async (
|
|
3736
|
+
workflow: async (bullmqJob, exuluJob, workflow, logsDir) => {
|
|
3549
3737
|
if (!workflow) {
|
|
3550
|
-
throw new Error(`Workflow function with id: ${
|
|
3551
|
-
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
logger
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
throw new Error(message);
|
|
3565
|
-
}
|
|
3566
|
-
await logger.write(`Workflow completed. ${JSON.stringify(output.results)}`, "INFO");
|
|
3738
|
+
throw new Error(`Workflow function with id: ${bullmqJob.data.backend} not found in registry.`);
|
|
3739
|
+
}
|
|
3740
|
+
console.log("[EXULU] starting workflow with job inputs.", bullmqJob.data.inputs);
|
|
3741
|
+
const logger = new ExuluLogger(exuluJob, logsDir);
|
|
3742
|
+
const output = await workflow.start({
|
|
3743
|
+
job: exuluJob,
|
|
3744
|
+
inputs: bullmqJob.data.inputs,
|
|
3745
|
+
user: bullmqJob.data.user,
|
|
3746
|
+
logger,
|
|
3747
|
+
session: bullmqJob.data.session,
|
|
3748
|
+
agent: bullmqJob.data.agent,
|
|
3749
|
+
label: bullmqJob.data.label
|
|
3750
|
+
});
|
|
3751
|
+
await logger.write(`Workflow completed. ${JSON.stringify(output)}`, "INFO");
|
|
3567
3752
|
return output;
|
|
3568
3753
|
}
|
|
3569
3754
|
}
|
|
@@ -3590,56 +3775,56 @@ var createWorkers = async (queues2, contexts, embedders, workflows, _logsDir) =>
|
|
|
3590
3775
|
console.log(`[EXULU] creating worker for queue ${queue}.`);
|
|
3591
3776
|
const worker = new Worker(
|
|
3592
3777
|
`${queue}`,
|
|
3593
|
-
async (
|
|
3778
|
+
async (bullmqJob) => {
|
|
3594
3779
|
const { db: db2 } = await postgresClient();
|
|
3595
3780
|
try {
|
|
3596
|
-
bullmq.validate(
|
|
3597
|
-
if (
|
|
3598
|
-
if (!
|
|
3781
|
+
bullmq.validate(bullmqJob);
|
|
3782
|
+
if (bullmqJob.data.type === "embedder") {
|
|
3783
|
+
if (!bullmqJob.data.updater) {
|
|
3599
3784
|
throw new Error("No updater set for embedder job.");
|
|
3600
3785
|
}
|
|
3601
|
-
const context = contexts.find((context2) => context2.id ===
|
|
3786
|
+
const context = contexts.find((context2) => context2.id === bullmqJob.data.context);
|
|
3602
3787
|
if (!context) {
|
|
3603
|
-
throw new Error(`Context ${
|
|
3788
|
+
throw new Error(`Context ${bullmqJob.data.context} not found in the registry.`);
|
|
3604
3789
|
}
|
|
3605
|
-
if (!
|
|
3790
|
+
if (!bullmqJob.data.embedder) {
|
|
3606
3791
|
throw new Error(`No embedder set for embedder job.`);
|
|
3607
3792
|
}
|
|
3608
|
-
const embedder = embedders.find((embedder2) => embedder2.id ===
|
|
3793
|
+
const embedder = embedders.find((embedder2) => embedder2.id === bullmqJob.data.embedder);
|
|
3609
3794
|
if (!embedder) {
|
|
3610
|
-
throw new Error(`Embedder ${
|
|
3795
|
+
throw new Error(`Embedder ${bullmqJob.data.embedder} not found in the registry.`);
|
|
3611
3796
|
}
|
|
3612
|
-
if (!
|
|
3797
|
+
if (!bullmqJob.data.source) {
|
|
3613
3798
|
throw new Error("No source set for embedder job.");
|
|
3614
3799
|
}
|
|
3615
|
-
const source = context.sources.get(
|
|
3800
|
+
const source = context.sources.get(bullmqJob.data.source);
|
|
3616
3801
|
if (!source) {
|
|
3617
|
-
throw new Error(`Source ${
|
|
3802
|
+
throw new Error(`Source ${bullmqJob.data.source} not found in the registry.`);
|
|
3618
3803
|
}
|
|
3619
|
-
if (!
|
|
3804
|
+
if (!bullmqJob.data.updater) {
|
|
3620
3805
|
throw new Error("No updater set for embedder job.");
|
|
3621
3806
|
}
|
|
3622
|
-
const updater = source.updaters.find((updater2) => updater2.id ===
|
|
3807
|
+
const updater = source.updaters.find((updater2) => updater2.id === bullmqJob.data.updater);
|
|
3623
3808
|
if (!updater) {
|
|
3624
|
-
throw new Error(`Updater ${
|
|
3809
|
+
throw new Error(`Updater ${bullmqJob.data.updater} not found in the registry.`);
|
|
3625
3810
|
}
|
|
3626
|
-
if (!
|
|
3811
|
+
if (!bullmqJob.data.documents) {
|
|
3627
3812
|
throw new Error("No input documents set for embedder job.");
|
|
3628
3813
|
}
|
|
3629
|
-
if (!Array.isArray(
|
|
3814
|
+
if (!Array.isArray(bullmqJob.data.documents)) {
|
|
3630
3815
|
throw new Error("Input documents must be an array.");
|
|
3631
3816
|
}
|
|
3632
|
-
const result = await embedder.upsert(
|
|
3817
|
+
const result = await embedder.upsert(bullmqJob.data.context, bullmqJob.data.documents, {
|
|
3633
3818
|
label: context.name,
|
|
3634
|
-
trigger:
|
|
3819
|
+
trigger: bullmqJob.data.trigger || "unknown"
|
|
3635
3820
|
});
|
|
3636
|
-
const mongoRecord = await db2.from("jobs").where({ redis:
|
|
3821
|
+
const mongoRecord = await db2.from("jobs").where({ redis: bullmqJob.id }).first();
|
|
3637
3822
|
if (!mongoRecord) {
|
|
3638
3823
|
throw new Error("Job not found in the database.");
|
|
3639
3824
|
}
|
|
3640
3825
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
3641
3826
|
const duration = (finishedAt.getTime() - new Date(mongoRecord.createdAt).getTime()) / 1e3;
|
|
3642
|
-
await db2.from("jobs").where({ redis:
|
|
3827
|
+
await db2.from("jobs").where({ redis: bullmqJob.id }).update({
|
|
3643
3828
|
status: "completed",
|
|
3644
3829
|
finishedAt,
|
|
3645
3830
|
duration,
|
|
@@ -3647,19 +3832,19 @@ var createWorkers = async (queues2, contexts, embedders, workflows, _logsDir) =>
|
|
|
3647
3832
|
});
|
|
3648
3833
|
return result;
|
|
3649
3834
|
}
|
|
3650
|
-
if (
|
|
3651
|
-
const workflow = workflows.find((workflow2) => workflow2.id ===
|
|
3835
|
+
if (bullmqJob.data.type === "workflow") {
|
|
3836
|
+
const workflow = workflows.find((workflow2) => workflow2.id === bullmqJob.data.workflow);
|
|
3652
3837
|
if (!workflow) {
|
|
3653
|
-
throw new Error(`Workflow ${
|
|
3838
|
+
throw new Error(`Workflow ${bullmqJob.data.workflow} not found in the registry.`);
|
|
3654
3839
|
}
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
if (!mongoRecord) {
|
|
3840
|
+
const exuluJob = await db2.from("jobs").where({ redis: bullmqJob.id }).first();
|
|
3841
|
+
if (!exuluJob) {
|
|
3658
3842
|
throw new Error("Job not found in the database.");
|
|
3659
3843
|
}
|
|
3844
|
+
const result = await bullmq.process.workflow(bullmqJob, exuluJob, workflow, logsDir);
|
|
3660
3845
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
3661
|
-
const duration = (finishedAt.getTime() - new Date(
|
|
3662
|
-
await db2.from("jobs").where({ redis:
|
|
3846
|
+
const duration = (finishedAt.getTime() - new Date(exuluJob.createdAt).getTime()) / 1e3;
|
|
3847
|
+
await db2.from("jobs").where({ redis: bullmqJob.id }).update({
|
|
3663
3848
|
status: "completed",
|
|
3664
3849
|
finishedAt,
|
|
3665
3850
|
duration,
|
|
@@ -3668,7 +3853,7 @@ var createWorkers = async (queues2, contexts, embedders, workflows, _logsDir) =>
|
|
|
3668
3853
|
return result;
|
|
3669
3854
|
}
|
|
3670
3855
|
} catch (error) {
|
|
3671
|
-
await db2.from("jobs").where({ redis:
|
|
3856
|
+
await db2.from("jobs").where({ redis: bullmqJob.id }).update({
|
|
3672
3857
|
status: "failed",
|
|
3673
3858
|
finishedAt: /* @__PURE__ */ new Date(),
|
|
3674
3859
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -3810,12 +3995,19 @@ var ExuluApp = class {
|
|
|
3810
3995
|
};
|
|
3811
3996
|
|
|
3812
3997
|
// src/index.ts
|
|
3998
|
+
import { RecursiveChunker, SentenceChunker } from "chonkie";
|
|
3813
3999
|
var ExuluJobs = {
|
|
3814
4000
|
redis: redisClient,
|
|
3815
4001
|
jobs: {
|
|
3816
4002
|
validate: validateJob
|
|
3817
4003
|
}
|
|
3818
4004
|
};
|
|
4005
|
+
var ExuluChunkers = {
|
|
4006
|
+
chonkie: {
|
|
4007
|
+
sentence: SentenceChunker,
|
|
4008
|
+
recursive: RecursiveChunker
|
|
4009
|
+
}
|
|
4010
|
+
};
|
|
3819
4011
|
var ExuluDatabase = {
|
|
3820
4012
|
init: async () => {
|
|
3821
4013
|
await execute();
|
|
@@ -3825,14 +4017,17 @@ var ExuluDatabase = {
|
|
|
3825
4017
|
}
|
|
3826
4018
|
};
|
|
3827
4019
|
export {
|
|
4020
|
+
JOB_STATUS_ENUM as EXULU_JOB_STATUS_ENUM,
|
|
3828
4021
|
STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM,
|
|
3829
4022
|
ExuluAgent,
|
|
3830
4023
|
ExuluApp,
|
|
3831
4024
|
authentication as ExuluAuthentication,
|
|
4025
|
+
ExuluChunkers,
|
|
3832
4026
|
ExuluContext,
|
|
3833
4027
|
ExuluDatabase,
|
|
3834
4028
|
ExuluEmbedder,
|
|
3835
4029
|
ExuluJobs,
|
|
4030
|
+
ExuluLogger,
|
|
3836
4031
|
queues as ExuluQueues,
|
|
3837
4032
|
ExuluSource,
|
|
3838
4033
|
ExuluTool,
|