@exulu/backend 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3721 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/redis/client.ts
9
+ import { createClient } from "redis";
10
+
11
+ // src/bullmq/server.ts
12
+ var redisServer = {
13
+ host: `${process.env.REDIS_HOST}`,
14
+ port: process.env.REDIS_PORT
15
+ };
16
+
17
+ // src/redis/client.ts
18
+ var redisClient = null;
19
+ (async () => {
20
+ if (!redisClient) {
21
+ const url = `redis://${redisServer.host}:${redisServer.port}`;
22
+ console.log(`[EXULU] connecting to redis.`);
23
+ redisClient = createClient({
24
+ // todo add password
25
+ url
26
+ });
27
+ await redisClient.connect();
28
+ }
29
+ })();
30
+
31
+ // src/bullmq/validators.ts
32
+ var validateJob = (job) => {
33
+ if (!job.data) {
34
+ throw new Error(`Missing job data for job ${job.id}.`);
35
+ }
36
+ if (!job.data.type) {
37
+ throw new Error(`Missing property "type" in data for job ${job.id}.`);
38
+ }
39
+ if (!job.data.function) {
40
+ throw new Error(`Missing property "function" in data for job ${job.id}.`);
41
+ }
42
+ if (!job.data.inputs) {
43
+ throw new Error(`Missing property "inputs" in data for job ${job.id}.`);
44
+ }
45
+ if (job.data.type !== "embedder" && job.data.type !== "workflow") {
46
+ throw new Error(`Property "type" in data for job ${job.id} must be of value "embedder" or "agent".`);
47
+ }
48
+ if (job.data.type === "workflow" && job.data.function !== "execute") {
49
+ throw new Error(`Property "function" in data for job ${job.id} must be of value "execute" when using type "workflow".`);
50
+ }
51
+ if (job.data.type === "embedder" && job.data.function !== "upsert" && job.data.function !== "delete" && job.data.function !== "retrieve") {
52
+ throw new Error(`Property "function" in data for job ${job.id} must be of value "upsert", "delete" or "retrieve" when using type "embedder".`);
53
+ }
54
+ if (!job.data.id) {
55
+ throw new Error(`Property "id" in data for job ${job.id} missing.`);
56
+ }
57
+ return job;
58
+ };
59
+
60
+ // src/postgres/client.ts
61
+ import Knex from "knex";
62
+ import "knex";
63
+ import "pgvector/knex";
64
+ var db = {};
65
+ async function postgresClient() {
66
+ if (!db["exulu"]) {
67
+ const knex = Knex({
68
+ client: "pg",
69
+ connection: {
70
+ host: process.env.POSTGRES_DB_HOST,
71
+ port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
72
+ user: process.env.POSTGRES_DB_USER,
73
+ database: "exulu",
74
+ password: process.env.POSTGRES_DB_PASSWORD,
75
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
76
+ }
77
+ });
78
+ await knex.schema.createExtensionIfNotExists("vector");
79
+ db["exulu"] = knex;
80
+ }
81
+ return {
82
+ db: db["exulu"]
83
+ };
84
+ }
85
+
86
+ // src/postgres/core-schema.ts
87
+ var usersSchema = {
88
+ name: {
89
+ plural: "users",
90
+ singular: "user"
91
+ },
92
+ fields: [
93
+ {
94
+ name: "firstname",
95
+ type: "text"
96
+ },
97
+ {
98
+ name: "lastname",
99
+ type: "text"
100
+ },
101
+ {
102
+ name: "email",
103
+ type: "text",
104
+ index: true
105
+ },
106
+ {
107
+ name: "temporary_token",
108
+ type: "text"
109
+ },
110
+ {
111
+ name: "type",
112
+ type: "text",
113
+ index: true
114
+ },
115
+ {
116
+ name: "profile_image",
117
+ type: "text"
118
+ },
119
+ {
120
+ name: "super_admin",
121
+ type: "boolean",
122
+ default: false
123
+ },
124
+ {
125
+ name: "status",
126
+ type: "text"
127
+ },
128
+ {
129
+ name: "emailVerified",
130
+ type: "text"
131
+ },
132
+ {
133
+ name: "apikey",
134
+ type: "text"
135
+ },
136
+ {
137
+ name: "role",
138
+ type: "reference",
139
+ references: {
140
+ table: "roles",
141
+ field: "id",
142
+ onDelete: "CASCADE"
143
+ }
144
+ },
145
+ {
146
+ name: "last_used",
147
+ type: "date"
148
+ }
149
+ ]
150
+ };
151
+ var rolesSchema = {
152
+ name: {
153
+ plural: "roles",
154
+ singular: "role"
155
+ },
156
+ fields: [
157
+ {
158
+ name: "name",
159
+ type: "text"
160
+ },
161
+ {
162
+ name: "is_admin",
163
+ type: "boolean",
164
+ default: false
165
+ },
166
+ {
167
+ name: "agents",
168
+ type: "json"
169
+ }
170
+ ]
171
+ };
172
+ var statisticsSchema = {
173
+ name: {
174
+ plural: "statistics",
175
+ singular: "statistic"
176
+ },
177
+ fields: [
178
+ {
179
+ name: "name",
180
+ type: "text"
181
+ },
182
+ {
183
+ name: "label",
184
+ type: "text"
185
+ },
186
+ {
187
+ name: "type",
188
+ type: "text"
189
+ },
190
+ {
191
+ name: "total",
192
+ type: "number"
193
+ },
194
+ {
195
+ name: "timeseries",
196
+ type: "json"
197
+ }
198
+ ]
199
+ };
200
+ var jobsSchema = {
201
+ name: {
202
+ plural: "jobs",
203
+ singular: "job"
204
+ },
205
+ fields: [
206
+ {
207
+ name: "redis",
208
+ type: "text"
209
+ },
210
+ {
211
+ name: "session",
212
+ type: "text"
213
+ },
214
+ {
215
+ name: "status",
216
+ type: "text"
217
+ },
218
+ {
219
+ name: "type",
220
+ type: "text"
221
+ },
222
+ {
223
+ name: "result",
224
+ type: "text"
225
+ },
226
+ {
227
+ name: "name",
228
+ type: "text"
229
+ },
230
+ {
231
+ name: "agent",
232
+ type: "text"
233
+ },
234
+ {
235
+ name: "user",
236
+ type: "text"
237
+ },
238
+ {
239
+ name: "item",
240
+ type: "text"
241
+ },
242
+ {
243
+ name: "inputs",
244
+ type: "json"
245
+ },
246
+ {
247
+ name: "finished_at",
248
+ type: "date"
249
+ },
250
+ {
251
+ name: "duration",
252
+ type: "number"
253
+ }
254
+ ]
255
+ };
256
+ var agentsSchema = {
257
+ name: {
258
+ plural: "agents",
259
+ singular: "agent"
260
+ },
261
+ fields: [
262
+ {
263
+ name: "name",
264
+ type: "text"
265
+ },
266
+ {
267
+ name: "description",
268
+ type: "text"
269
+ },
270
+ {
271
+ name: "extensions",
272
+ type: "json"
273
+ },
274
+ {
275
+ name: "backend",
276
+ type: "text"
277
+ },
278
+ {
279
+ name: "type",
280
+ type: "text"
281
+ },
282
+ {
283
+ name: "active",
284
+ type: "boolean",
285
+ default: false
286
+ },
287
+ {
288
+ name: "public",
289
+ type: "boolean",
290
+ default: false
291
+ },
292
+ {
293
+ name: "tools",
294
+ type: "json"
295
+ }
296
+ ]
297
+ };
298
+
299
+ // src/registry/utils/map-types.ts
300
+ var mapType = (t, type, name, defaultValue) => {
301
+ if (type === "text") {
302
+ t.string(name, 255);
303
+ return;
304
+ }
305
+ if (type === "longText") {
306
+ t.text(name);
307
+ return;
308
+ }
309
+ if (type === "shortText") {
310
+ t.string(name, 100);
311
+ return;
312
+ }
313
+ if (type === "number") {
314
+ t.float(name);
315
+ return;
316
+ }
317
+ if (type === "boolean") {
318
+ t.boolean(name).defaultTo(defaultValue || false);
319
+ return;
320
+ }
321
+ if (type === "code") {
322
+ t.text(name);
323
+ return;
324
+ }
325
+ if (type === "json") {
326
+ t.jsonb(name);
327
+ return;
328
+ }
329
+ if (type === "date") {
330
+ t.date(name);
331
+ return;
332
+ }
333
+ if (type === "uuid") {
334
+ t.uuid(name);
335
+ return;
336
+ }
337
+ throw new Error("Invalid type: " + type);
338
+ };
339
+
340
+ // src/registry/utils/sanitize-name.ts
341
+ var sanitizeName = (name) => {
342
+ return name.toLowerCase().replace(/ /g, "_");
343
+ };
344
+
345
+ // src/postgres/init-db.ts
346
+ var up = async function(knex) {
347
+ if (!await knex.schema.hasTable("roles")) {
348
+ await knex.schema.createTable("roles", (table) => {
349
+ table.uuid("id").primary().defaultTo(knex.fn.uuid());
350
+ table.date("createdAt").defaultTo(knex.fn.now());
351
+ table.date("updatedAt").defaultTo(knex.fn.now());
352
+ for (const field of rolesSchema.fields) {
353
+ const { type, name, references, default: defaultValue } = field;
354
+ if (!type || !name) {
355
+ continue;
356
+ }
357
+ if (type === "reference") {
358
+ if (!references) {
359
+ throw new Error("Field with type reference must have a reference definition.");
360
+ }
361
+ table.uuid(name).references(references.field).inTable(references.table);
362
+ return;
363
+ }
364
+ mapType(table, type, sanitizeName(name), defaultValue);
365
+ }
366
+ });
367
+ }
368
+ if (!await knex.schema.hasTable("statistics")) {
369
+ await knex.schema.createTable("statistics", (table) => {
370
+ table.uuid("id").primary().defaultTo(knex.fn.uuid());
371
+ table.date("createdAt").defaultTo(knex.fn.now());
372
+ table.date("updatedAt").defaultTo(knex.fn.now());
373
+ for (const field of statisticsSchema.fields) {
374
+ const { type, name, references, default: defaultValue } = field;
375
+ if (!type || !name) {
376
+ continue;
377
+ }
378
+ if (type === "reference") {
379
+ if (!references) {
380
+ throw new Error("Field with type reference must have a reference definition.");
381
+ }
382
+ table.uuid(name).references(references.field).inTable(references.table);
383
+ return;
384
+ }
385
+ mapType(table, type, sanitizeName(name), defaultValue);
386
+ }
387
+ });
388
+ }
389
+ if (!await knex.schema.hasTable("jobs")) {
390
+ await knex.schema.createTable("jobs", (table) => {
391
+ table.increments("id").primary();
392
+ table.date("createdAt").defaultTo(knex.fn.now());
393
+ table.date("updatedAt").defaultTo(knex.fn.now());
394
+ for (const field of jobsSchema.fields) {
395
+ const { type, name, references, default: defaultValue } = field;
396
+ if (!type || !name) {
397
+ continue;
398
+ }
399
+ if (type === "reference") {
400
+ if (!references) {
401
+ throw new Error("Field with type reference must have a reference definition.");
402
+ }
403
+ table.uuid(name).references(references.field).inTable(references.table);
404
+ return;
405
+ }
406
+ mapType(table, type, sanitizeName(name), defaultValue);
407
+ }
408
+ });
409
+ }
410
+ if (!await knex.schema.hasTable("agents")) {
411
+ await knex.schema.createTable("agents", (table) => {
412
+ table.increments("id").primary();
413
+ table.date("createdAt").defaultTo(knex.fn.now());
414
+ table.date("updatedAt").defaultTo(knex.fn.now());
415
+ for (const field of agentsSchema.fields) {
416
+ const { type, name, references, default: defaultValue } = field;
417
+ if (!type || !name) {
418
+ continue;
419
+ }
420
+ if (type === "reference") {
421
+ if (!references) {
422
+ throw new Error("Field with type reference must have a reference definition.");
423
+ }
424
+ table.uuid(name).references(references.field).inTable(references.table);
425
+ return;
426
+ }
427
+ mapType(table, type, sanitizeName(name), defaultValue);
428
+ }
429
+ });
430
+ }
431
+ if (!await knex.schema.hasTable("verification_token")) {
432
+ await knex.schema.createTable("verification_token", (table) => {
433
+ table.text("identifier").notNullable();
434
+ table.timestamp("expires", { useTz: true }).notNullable();
435
+ table.text("token").notNullable();
436
+ table.primary(["identifier", "token"]);
437
+ });
438
+ }
439
+ if (!await knex.schema.hasTable("users")) {
440
+ await knex.schema.createTable("users", (table) => {
441
+ table.increments("id").primary();
442
+ table.date("createdAt").defaultTo(knex.fn.now());
443
+ table.date("updatedAt").defaultTo(knex.fn.now());
444
+ table.string("name", 255);
445
+ table.string("password", 255);
446
+ table.string("email", 255);
447
+ table.timestamp("emailVerified", { useTz: true });
448
+ table.text("image");
449
+ for (const field of usersSchema.fields) {
450
+ const { type, name, references, default: defaultValue } = field;
451
+ if (name === "id" || name === "name" || name === "email" || name === "emailVerified" || name === "image") {
452
+ continue;
453
+ }
454
+ if (!type || !name) {
455
+ continue;
456
+ }
457
+ if (type === "reference") {
458
+ if (!references) {
459
+ throw new Error("Field with type reference must have a reference definition.");
460
+ }
461
+ table.uuid(name).references(references.field).inTable(references.table);
462
+ return;
463
+ }
464
+ mapType(table, type, sanitizeName(name), defaultValue);
465
+ }
466
+ });
467
+ }
468
+ if (!await knex.schema.hasTable("accounts")) {
469
+ await knex.schema.createTable("accounts", (table) => {
470
+ table.increments("id").primary();
471
+ table.integer("userId").notNullable();
472
+ table.string("type", 255).notNullable();
473
+ table.string("provider", 255).notNullable();
474
+ table.string("providerAccountId", 255).notNullable();
475
+ table.text("refresh_token");
476
+ table.text("access_token");
477
+ table.bigInteger("expires_at");
478
+ table.text("id_token");
479
+ table.text("scope");
480
+ table.text("session_state");
481
+ table.text("token_type");
482
+ });
483
+ }
484
+ if (!await knex.schema.hasTable("sessions")) {
485
+ await knex.schema.createTable("sessions", (table) => {
486
+ table.increments("id").primary();
487
+ table.integer("userId").notNullable();
488
+ table.timestamp("expires", { useTz: true }).notNullable();
489
+ table.string("sessionToken", 255).notNullable();
490
+ });
491
+ }
492
+ };
493
+ var execute = async () => {
494
+ console.log("[EXULU] Initializing database.");
495
+ const { db: db2 } = await postgresClient();
496
+ await up(db2);
497
+ console.log("[EXULU] Inserting default user and admin role.");
498
+ const existingRole = await db2.from("roles").where({ name: "admin" }).first();
499
+ let roleId;
500
+ if (!existingRole) {
501
+ console.log("[EXULU] Creating default admin role.");
502
+ const role = await db2.from("roles").insert({
503
+ name: "admin",
504
+ is_admin: true,
505
+ agents: []
506
+ }).returning("id");
507
+ roleId = role[0].id;
508
+ } else {
509
+ roleId = existingRole.id;
510
+ }
511
+ const existingUser = await db2.from("users").where({ email: "admin@exulu.com" }).first();
512
+ if (!existingUser) {
513
+ console.log("[EXULU] Creating default admin user.");
514
+ await db2.from("users").insert({
515
+ name: "exulu",
516
+ email: "admin@exulu.com",
517
+ super_admin: true,
518
+ createdAt: /* @__PURE__ */ new Date(),
519
+ updatedAt: /* @__PURE__ */ new Date(),
520
+ // password: "admin", todo add this again when we implement password auth / encryption as alternative to OTP
521
+ role: roleId
522
+ });
523
+ }
524
+ console.log("[EXULU] Database initialized.");
525
+ return;
526
+ };
527
+
528
+ // src/registry/classes.ts
529
+ import "zod";
530
+ import "bullmq";
531
+ import { Agent as MastraAgent } from "@mastra/core";
532
+ import { z } from "zod";
533
+ import * as fs from "fs";
534
+ import * as path from "path";
535
+ import "bullmq";
536
+ import { Memory } from "@mastra/memory";
537
+ import { PostgresStore, PgVector } from "@mastra/pg";
538
+
539
+ // types/enums/statistics.ts
540
+ var STATISTICS_TYPE_ENUM = {
541
+ CONTEXT_RETRIEVE: "context.retrieve",
542
+ SOURCE_UPDATE: "source.update",
543
+ EMBEDDER_UPSERT: "embedder.upsert",
544
+ EMBEDDER_GENERATE: "embedder.generate",
545
+ EMBEDDER_DELETE: "embedder.delete",
546
+ WORKFLOW_RUN: "workflow.run",
547
+ CONTEXT_UPSERT: "context.upsert",
548
+ TOOL_CALL: "tool.call",
549
+ AGENT_RUN: "agent.run"
550
+ };
551
+
552
+ // src/registry/classes.ts
553
+ import pgvector2 from "pgvector/knex";
554
+
555
+ // src/registry/decoraters/bullmq.ts
556
+ import "bullmq";
557
+ import { v4 as uuidv4 } from "uuid";
558
+ var bullmqDecorator = async ({
559
+ label,
560
+ type,
561
+ workflow,
562
+ embedder,
563
+ inputs,
564
+ queue,
565
+ user,
566
+ agent,
567
+ session,
568
+ configuration,
569
+ updater,
570
+ context,
571
+ source,
572
+ documents,
573
+ trigger,
574
+ item
575
+ }) => {
576
+ const redisId = uuidv4();
577
+ const job = await queue.add(
578
+ `${embedder || workflow}`,
579
+ {
580
+ type: `${type}`,
581
+ ...embedder && { embedder },
582
+ ...workflow && { workflow },
583
+ ...configuration && { configuration },
584
+ ...updater && { updater },
585
+ ...context && { context },
586
+ ...source && { source },
587
+ ...documents && { documents },
588
+ ...trigger && { trigger },
589
+ ...item && { item },
590
+ agent,
591
+ user,
592
+ inputs,
593
+ session
594
+ },
595
+ {
596
+ jobId: redisId
597
+ }
598
+ );
599
+ const { db: db2 } = await postgresClient();
600
+ const now = /* @__PURE__ */ new Date();
601
+ console.log("[EXULU] scheduling new job", inputs);
602
+ const insertData = {
603
+ name: `${label}`,
604
+ redis: job.id,
605
+ status: "waiting",
606
+ type,
607
+ inputs,
608
+ agent,
609
+ item,
610
+ createdAt: now,
611
+ updatedAt: now,
612
+ user,
613
+ session,
614
+ ...embedder && { embedder },
615
+ ...workflow && { workflow },
616
+ ...configuration && { configuration },
617
+ ...updater && { updater },
618
+ ...context && { context },
619
+ ...source && { source },
620
+ ...documents && { documents: documents.map((doc2) => doc2.id) },
621
+ ...trigger && { trigger }
622
+ };
623
+ await db2("jobs").insert(insertData).onConflict("redis").merge({
624
+ ...insertData,
625
+ updatedAt: now
626
+ // Only updatedAt changes on updates
627
+ });
628
+ const doc = await db2.from("jobs").where({ redis: job.id }).first();
629
+ if (!doc?.id) {
630
+ throw new Error("Failed to get job ID after insert/update");
631
+ }
632
+ console.log("[EXULU] created job", doc?.id);
633
+ return {
634
+ ...job,
635
+ id: doc?.id,
636
+ redis: job.id
637
+ };
638
+ };
639
+
640
+ // src/registry/classes.ts
641
+ function generateSlug(name) {
642
+ const normalized = name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
643
+ const lowercase = normalized.toLowerCase();
644
+ const slug = lowercase.replace(/[\W_]+/g, "-").replace(/^-+|-+$/g, "");
645
+ return slug;
646
+ }
647
+ var ExuluZodFileType = ({
648
+ name,
649
+ label,
650
+ description,
651
+ allowedFileTypes
652
+ }) => {
653
+ return z.object({
654
+ [`exulu_file_${name}`]: z.string().describe(JSON.stringify({
655
+ label,
656
+ isFile: true,
657
+ description,
658
+ allowedFileTypes
659
+ }))
660
+ });
661
+ };
662
+ var ExuluAgent = class {
663
+ id;
664
+ name;
665
+ description = "";
666
+ slug = "";
667
+ streaming = false;
668
+ type;
669
+ outputSchema;
670
+ rateLimit;
671
+ config;
672
+ memory;
673
+ tools;
674
+ capabilities;
675
+ constructor({ id, name, description, outputSchema, config, rateLimit, type, capabilities, tools }) {
676
+ this.id = id;
677
+ this.name = name;
678
+ this.type = type;
679
+ this.description = description;
680
+ this.outputSchema = outputSchema;
681
+ this.rateLimit = rateLimit;
682
+ this.tools = tools;
683
+ this.config = config;
684
+ this.capabilities = capabilities;
685
+ this.slug = `/agents/${generateSlug(this.name)}/run`;
686
+ if (config?.memory) {
687
+ const connectionString = `postgresql://${process.env.POSTGRES_DB_USER}:${process.env.POSTGRES_DB_PASSWORD}@${process.env.POSTGRES_DB_HOST}:${process.env.POSTGRES_DB_PORT}/exulu`;
688
+ this.memory = new Memory({
689
+ storage: new PostgresStore({
690
+ host: process.env.POSTGRES_DB_HOST || "",
691
+ port: parseInt(process.env.POSTGRES_DB_PORT || "5432"),
692
+ user: process.env.POSTGRES_DB_USER || "",
693
+ database: "exulu",
694
+ // putting it into an own database that is not managed by exulu
695
+ password: process.env.POSTGRES_DB_PASSWORD || "",
696
+ ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false
697
+ }),
698
+ ...config?.memory.vector ? { vector: new PgVector(connectionString) } : {},
699
+ options: {
700
+ lastMessages: config?.memory.lastMessages || 10,
701
+ semanticRecall: {
702
+ topK: config?.memory.semanticRecall.topK || 3,
703
+ messageRange: config?.memory.semanticRecall.messageRange || 2
704
+ }
705
+ }
706
+ });
707
+ }
708
+ }
709
+ chat = async (id) => {
710
+ const { db: db2 } = await postgresClient();
711
+ const agent = await db2.from("agents").select("*").where("id", "=", id).first();
712
+ if (!agent) {
713
+ throw new Error("Agent not found");
714
+ }
715
+ let tools = {};
716
+ agent.tools?.forEach(({ name }) => {
717
+ const tool = this.tools?.find((t) => t.name === name);
718
+ if (!tool) {
719
+ return;
720
+ }
721
+ return tool;
722
+ });
723
+ updateStatistic({
724
+ name: "count",
725
+ label: this.name,
726
+ type: STATISTICS_TYPE_ENUM.AGENT_RUN,
727
+ trigger: "agent"
728
+ });
729
+ return new MastraAgent({
730
+ name: this.config.name,
731
+ instructions: this.config.instructions,
732
+ model: this.config.model,
733
+ tools,
734
+ memory: this.memory ? this.memory : void 0
735
+ });
736
+ };
737
+ };
738
+ var ExuluEmbedder = class {
739
+ id;
740
+ name;
741
+ slug = "";
742
+ queue;
743
+ generateEmbeddings;
744
+ vectorDimensions;
745
+ maxChunkSize;
746
+ chunker;
747
+ constructor({ id, name, description, generateEmbeddings, queue, vectorDimensions, maxChunkSize, chunker }) {
748
+ this.id = id;
749
+ this.name = name;
750
+ this.vectorDimensions = vectorDimensions;
751
+ this.maxChunkSize = maxChunkSize;
752
+ this.chunker = chunker;
753
+ this.slug = `/embedders/${generateSlug(this.name)}/run`;
754
+ this.queue = queue;
755
+ this.generateEmbeddings = generateEmbeddings;
756
+ }
757
+ async generateFromQuery(query, statistics) {
758
+ if (statistics) {
759
+ }
760
+ return await this.generateEmbeddings({
761
+ item: {
762
+ id: "placeholder"
763
+ },
764
+ chunks: [{
765
+ content: query,
766
+ index: 1
767
+ }]
768
+ });
769
+ }
770
+ async generateFromDocument(input, statistics) {
771
+ if (statistics) {
772
+ }
773
+ if (!this.chunker) {
774
+ throw new Error("Chunker not found for embedder " + this.name);
775
+ }
776
+ console.log("generating chunks");
777
+ if (!input.id) {
778
+ throw new Error("Item id is required for generating embeddings.");
779
+ }
780
+ const output = await this.chunker(input, this.maxChunkSize);
781
+ console.log("generating embeddings");
782
+ return await this.generateEmbeddings(output);
783
+ }
784
+ };
785
+ var ExuluWorkflow = class {
786
+ id;
787
+ name;
788
+ description = "";
789
+ enable_batch = false;
790
+ slug = "";
791
+ queue;
792
+ workflow;
793
+ inputSchema;
794
+ constructor({ id, name, description, workflow, queue, enable_batch, inputSchema }) {
795
+ this.id = id;
796
+ this.name = name;
797
+ this.description = description;
798
+ this.enable_batch = enable_batch;
799
+ this.slug = `/workflows/${generateSlug(this.name)}/run`;
800
+ this.queue = queue;
801
+ this.inputSchema = inputSchema;
802
+ this.workflow = workflow;
803
+ }
804
+ };
805
+ var ExuluLogger = class {
806
+ logPath;
807
+ job;
808
+ constructor(job, logsDir) {
809
+ this.job = job;
810
+ if (!fs.existsSync(logsDir)) {
811
+ fs.mkdirSync(logsDir, { recursive: true });
812
+ }
813
+ this.logPath = path.join(logsDir, `${job.id}_${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.txt`);
814
+ }
815
+ async write(message, level) {
816
+ const logMessage = message.endsWith("\n") ? message : message + "\n";
817
+ try {
818
+ await fs.promises.appendFile(this.logPath, `[EXULU][${level}] - ${(/* @__PURE__ */ new Date()).toISOString()}: ${logMessage}`);
819
+ } catch (error) {
820
+ console.error(`Error writing to log file ${this.job.id}:`, error);
821
+ throw error;
822
+ }
823
+ }
824
+ };
825
+ var ExuluTool = class {
826
+ id;
827
+ name;
828
+ description;
829
+ inputSchema;
830
+ outputSchema;
831
+ type;
832
+ _execute;
833
+ constructor({ id, name, description, inputSchema, outputSchema, type, execute: execute2 }) {
834
+ this.id = id;
835
+ this.name = name;
836
+ this.description = description;
837
+ this.inputSchema = inputSchema;
838
+ this.outputSchema = outputSchema;
839
+ this.type = type;
840
+ this._execute = execute2;
841
+ }
842
+ execute = async (inputs) => {
843
+ if (!this._execute) {
844
+ throw new Error("Tool has no execute function.");
845
+ }
846
+ updateStatistic({
847
+ name: "count",
848
+ label: this.name,
849
+ type: STATISTICS_TYPE_ENUM.TOOL_CALL,
850
+ trigger: "agent"
851
+ });
852
+ return await this._execute(inputs);
853
+ };
854
+ };
855
+ var ExuluContext = class {
856
+ id;
857
+ name;
858
+ active;
859
+ fields;
860
+ rateLimit;
861
+ description;
862
+ embedder;
863
+ queryRewriter;
864
+ resultReranker;
865
+ // todo typings
866
+ _sources = [];
867
+ configuration;
868
+ constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration }) {
869
+ this.id = id;
870
+ this.name = name;
871
+ this.fields = fields || [];
872
+ this.configuration = configuration || {
873
+ calculateVectors: "manual"
874
+ };
875
+ this.description = description;
876
+ this.embedder = embedder;
877
+ this.active = active;
878
+ this.rateLimit = rateLimit;
879
+ this._sources = [];
880
+ this.queryRewriter = queryRewriter;
881
+ this.resultReranker = resultReranker;
882
+ }
883
+ deleteOne = async (id) => {
884
+ return {};
885
+ };
886
+ deleteAll = async () => {
887
+ return {};
888
+ };
889
+ getTableName = () => {
890
+ return sanitizeName(this.name) + "_items";
891
+ };
892
+ getChunksTableName = () => {
893
+ return sanitizeName(this.name) + "_chunks";
894
+ };
895
+ tableExists = async () => {
896
+ const { db: db2 } = await postgresClient();
897
+ const tableExists = await db2.schema.hasTable(this.getTableName());
898
+ return tableExists;
899
+ };
900
+ async updateItem(user, id, item) {
901
+ if (!id) {
902
+ throw new Error("Id is required for updating an item.");
903
+ }
904
+ const { db: db2 } = await postgresClient();
905
+ Object.keys(item).forEach((key) => {
906
+ if (key === "name" || key === "description" || key === "external_id" || key === "tags" || key === "source" || key === "textLength" || key === "upsert") {
907
+ return;
908
+ }
909
+ console.log("this.fields", this.fields);
910
+ const field = this.fields.find((field2) => field2.name === key);
911
+ if (!field) {
912
+ throw new Error("Trying to uppdate value for field '" + key + "' that does not exist on the context fields definition. Available fields: " + this.fields.map((field2) => sanitizeName(field2.name)).join(", ") + " ,name, description, external_id");
913
+ }
914
+ });
915
+ delete item.id;
916
+ delete item.created_at;
917
+ delete item.upsert;
918
+ item.updated_at = db2.fn.now();
919
+ const result = await db2.from(this.getTableName()).where({ id }).update(item).returning("id");
920
+ if (this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always") {
921
+ if (this.embedder.queue?.name) {
922
+ console.log("[EXULU] embedder is in queue mode, scheduling job.");
923
+ const job = await bullmqDecorator({
924
+ label: `Job running '${this.embedder.name}' for '${item.name} (${item.id}).'`,
925
+ embedder: this.embedder.id,
926
+ type: "embedder",
927
+ inputs: item,
928
+ queue: this.embedder.queue,
929
+ user
930
+ });
931
+ return {
932
+ id: result[0].id,
933
+ job: job.id
934
+ };
935
+ }
936
+ const { id: source, chunks } = await this.embedder.generateFromDocument({
937
+ ...item,
938
+ id
939
+ }, {
940
+ label: this.name,
941
+ trigger: "agent"
942
+ });
943
+ const exists = await db2.schema.hasTable(this.getChunksTableName());
944
+ if (!exists) {
945
+ await this.createChunksTable();
946
+ }
947
+ await db2.from(this.getChunksTableName()).where({ source }).delete();
948
+ await db2.from(this.getChunksTableName()).insert(chunks.map((chunk) => ({
949
+ source,
950
+ content: chunk.content,
951
+ chunk_index: chunk.index,
952
+ embedding: pgvector2.toSql(chunk.vector)
953
+ })));
954
+ }
955
+ return {
956
+ id: result[0].id,
957
+ job: void 0
958
+ };
959
+ }
960
+ async insertItem(user, item, upsert = false) {
961
+ if (!item.name) {
962
+ throw new Error("Name field is required.");
963
+ }
964
+ const { db: db2 } = await postgresClient();
965
+ if (item.external_id) {
966
+ const existingItem = await db2.from(this.getTableName()).where({ external_id: item.external_id }).first();
967
+ if (existingItem && !upsert) {
968
+ throw new Error("Item with external id " + item.external_id + " already exists.");
969
+ }
970
+ if (existingItem && upsert) {
971
+ await this.updateItem(user, existingItem.id, item);
972
+ return existingItem.id;
973
+ }
974
+ }
975
+ if (upsert && item.id) {
976
+ const existingItem = await db2.from(this.getTableName()).where({ id: item.id }).first();
977
+ if (existingItem && upsert) {
978
+ await this.updateItem(user, existingItem.id, item);
979
+ return existingItem.id;
980
+ }
981
+ }
982
+ Object.keys(item).forEach((key) => {
983
+ if (key === "name" || key === "description" || key === "external_id" || key === "tags" || key === "source" || key === "textLength" || key === "upsert") {
984
+ return;
985
+ }
986
+ console.log("this.fields", this.fields);
987
+ const field = this.fields.find((field2) => field2.name === key);
988
+ if (!field) {
989
+ throw new Error("Trying to insert value for field '" + key + "' that does not exist on the context fields definition. Available fields: " + this.fields.map((field2) => sanitizeName(field2.name)).join(", ") + " ,name, description, external_id");
990
+ }
991
+ });
992
+ delete item.id;
993
+ delete item.upsert;
994
+ const result = await db2.from(this.getTableName()).insert({
995
+ ...item,
996
+ id: db2.fn.uuid(),
997
+ created_at: db2.fn.now(),
998
+ updated_at: db2.fn.now()
999
+ }).returning("id");
1000
+ if (this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always") {
1001
+ if (this.embedder.queue?.name) {
1002
+ console.log("[EXULU] embedder is in queue mode, scheduling job.");
1003
+ const job = await bullmqDecorator({
1004
+ label: `Job running '${this.embedder.name}' for '${item.name} (${item.id}).'`,
1005
+ embedder: this.embedder.id,
1006
+ type: "embedder",
1007
+ inputs: item,
1008
+ queue: this.embedder.queue,
1009
+ user
1010
+ });
1011
+ return {
1012
+ id: result[0].id,
1013
+ job: job.id
1014
+ };
1015
+ }
1016
+ console.log("[EXULU] embedder is not in queue mode, calculating vectors directly.");
1017
+ const { id: source, chunks } = await this.embedder.generateFromDocument({
1018
+ ...item,
1019
+ id: result[0].id
1020
+ }, {
1021
+ label: this.name,
1022
+ trigger: "agent"
1023
+ });
1024
+ const exists = await db2.schema.hasTable(this.getChunksTableName());
1025
+ if (!exists) {
1026
+ await this.createChunksTable();
1027
+ }
1028
+ console.log("inserting chunks");
1029
+ await db2.from(this.getChunksTableName()).insert(chunks.map((chunk) => ({
1030
+ source,
1031
+ content: chunk.content,
1032
+ chunk_index: chunk.index,
1033
+ embedding: pgvector2.toSql(chunk.vector)
1034
+ })));
1035
+ }
1036
+ return {
1037
+ id: result[0].id,
1038
+ job: void 0
1039
+ };
1040
+ }
1041
+ getItems = async ({
1042
+ statistics,
1043
+ limit,
1044
+ page,
1045
+ name,
1046
+ archived,
1047
+ query,
1048
+ method
1049
+ }) => {
1050
+ if (!query && limit > 500) {
1051
+ throw new Error("Limit cannot be greater than 500.");
1052
+ }
1053
+ if (query && limit > 50) {
1054
+ throw new Error("Limit cannot be greater than 50 when using a vector search query.");
1055
+ }
1056
+ if (page < 1) page = 1;
1057
+ if (limit < 1) limit = 10;
1058
+ let offset = (page - 1) * limit;
1059
+ const mainTable = this.getTableName();
1060
+ const { db: db2 } = await postgresClient();
1061
+ const columns = await db2(mainTable).columnInfo();
1062
+ const totalQuery = db2.count("* as count").from(mainTable).first();
1063
+ const itemsQuery = db2.select(Object.keys(columns).map((column) => mainTable + "." + column)).from(mainTable).offset(offset).limit(limit);
1064
+ if (typeof name === "string") {
1065
+ itemsQuery.whereILike("name", `%${name}%`);
1066
+ totalQuery.whereILike("name", `%${name}%`);
1067
+ }
1068
+ if (typeof archived === "boolean") {
1069
+ itemsQuery.where("archived", archived);
1070
+ totalQuery.where("archived", archived);
1071
+ }
1072
+ if (!query) {
1073
+ const total = await totalQuery;
1074
+ let items = await itemsQuery;
1075
+ const last = Math.ceil(total.count / limit);
1076
+ return {
1077
+ pagination: {
1078
+ totalCount: parseInt(total.count),
1079
+ currentPage: page,
1080
+ limit,
1081
+ from: offset,
1082
+ pageCount: last || 1,
1083
+ to: offset + items.length,
1084
+ lastPage: last || 1,
1085
+ nextPage: page + 1 > last ? null : page + 1,
1086
+ previousPage: page - 1 || null
1087
+ },
1088
+ filters: {
1089
+ archived,
1090
+ name,
1091
+ query
1092
+ },
1093
+ context: {
1094
+ name: this.name,
1095
+ id: this.id,
1096
+ embedder: this.embedder.name
1097
+ },
1098
+ items
1099
+ };
1100
+ }
1101
+ if (typeof query === "string") {
1102
+ itemsQuery.limit(limit * 5);
1103
+ if (statistics) {
1104
+ updateStatistic({
1105
+ name: "count",
1106
+ label: statistics.label,
1107
+ type: STATISTICS_TYPE_ENUM.CONTEXT_RETRIEVE,
1108
+ trigger: statistics.trigger
1109
+ });
1110
+ }
1111
+ if (this.queryRewriter) {
1112
+ query = await this.queryRewriter(query);
1113
+ }
1114
+ const chunksTable = this.getChunksTableName();
1115
+ itemsQuery.leftJoin(chunksTable, function() {
1116
+ this.on(chunksTable + ".source", "=", mainTable + ".id");
1117
+ });
1118
+ itemsQuery.select(chunksTable + ".id");
1119
+ itemsQuery.select(chunksTable + ".source");
1120
+ itemsQuery.select(chunksTable + ".content");
1121
+ itemsQuery.select(chunksTable + ".chunk_index");
1122
+ itemsQuery.select(chunksTable + ".created_at");
1123
+ itemsQuery.select(chunksTable + ".updated_at");
1124
+ const { chunks } = await this.embedder.generateFromQuery(query);
1125
+ if (!chunks?.[0]?.vector) {
1126
+ throw new Error("No vector generated for query.");
1127
+ }
1128
+ const vector = chunks[0].vector;
1129
+ const vectorStr = `ARRAY[${vector.join(",")}]`;
1130
+ const vectorExpr = `${vectorStr}::vector`;
1131
+ switch (method) {
1132
+ case "l1Distance":
1133
+ itemsQuery.select(db2.raw(`?? <-> ${vectorExpr} as l1_distance`, [`${chunksTable}.embedding`]));
1134
+ itemsQuery.orderByRaw(db2.raw(`?? <-> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
1135
+ break;
1136
+ case "l2Distance":
1137
+ itemsQuery.select(db2.raw(`?? <-> ${vectorExpr} as l2_distance`, [`${chunksTable}.embedding`]));
1138
+ itemsQuery.orderByRaw(db2.raw(`?? <-> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
1139
+ break;
1140
+ case "hammingDistance":
1141
+ itemsQuery.select(db2.raw(`?? <#> ${vectorExpr} as hamming_distance`, [`${chunksTable}.embedding`]));
1142
+ itemsQuery.orderByRaw(db2.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
1143
+ break;
1144
+ case "jaccardDistance":
1145
+ itemsQuery.select(db2.raw(`?? <#> ${vectorExpr} as jaccard_distance`, [`${chunksTable}.embedding`]));
1146
+ itemsQuery.orderByRaw(db2.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
1147
+ break;
1148
+ case "maxInnerProduct":
1149
+ itemsQuery.select(db2.raw(`?? <#> ${vectorExpr} as inner_product`, [`${chunksTable}.embedding`]));
1150
+ itemsQuery.orderByRaw(db2.raw(`?? <#> ${vectorExpr} ASC`, [`${chunksTable}.embedding`]));
1151
+ break;
1152
+ case "cosineDistance":
1153
+ default:
1154
+ itemsQuery.select(db2.raw(`1 - (?? <#> ${vectorExpr}) as cosine_distance`, [`${chunksTable}.embedding`]));
1155
+ itemsQuery.orderByRaw(db2.raw(`1 - (?? <#> ${vectorExpr}) DESC`, [`${chunksTable}.embedding`]));
1156
+ break;
1157
+ }
1158
+ let items = await itemsQuery;
1159
+ const seenSources = /* @__PURE__ */ new Map();
1160
+ items = items.reduce((acc, item) => {
1161
+ if (!seenSources.has(item.source)) {
1162
+ seenSources.set(item.source, {
1163
+ ...Object.fromEntries(
1164
+ Object.keys(item).filter(
1165
+ (key) => key !== "l1_distance" && key !== "l2_distance" && key !== "hamming_distance" && key !== "jaccard_distance" && key !== "inner_product" && key !== "cosine_distance" && key !== "content" && key !== "source" && key !== "chunk_index"
1166
+ ).map((key) => [key, item[key]])
1167
+ ),
1168
+ chunks: [{
1169
+ content: item.content,
1170
+ chunk_index: item.chunk_index,
1171
+ ...method === "l1Distance" && { l1_distance: item.l1_distance },
1172
+ ...method === "l2Distance" && { l2_distance: item.l2_distance },
1173
+ ...method === "hammingDistance" && { hamming_distance: item.hamming_distance },
1174
+ ...method === "jaccardDistance" && { jaccard_distance: item.jaccard_distance },
1175
+ ...method === "maxInnerProduct" && { inner_product: item.inner_product },
1176
+ ...method === "cosineDistance" && { cosine_distance: item.cosine_distance }
1177
+ }]
1178
+ });
1179
+ acc.push(seenSources.get(item.source));
1180
+ } else {
1181
+ seenSources.get(item.source).chunks.push({
1182
+ content: item.content,
1183
+ chunk_index: item.chunk_index,
1184
+ ...method === "l1Distance" && { l1_distance: item.l1_distance },
1185
+ ...method === "l2Distance" && { l2_distance: item.l2_distance },
1186
+ ...method === "hammingDistance" && { hamming_distance: item.hamming_distance },
1187
+ ...method === "jaccardDistance" && { jaccard_distance: item.jaccard_distance },
1188
+ ...method === "maxInnerProduct" && { inner_product: item.inner_product },
1189
+ ...method === "cosineDistance" && { cosine_distance: item.cosine_distance }
1190
+ });
1191
+ }
1192
+ return acc;
1193
+ }, []);
1194
+ if (this.resultReranker && query) {
1195
+ items = await this.resultReranker(items);
1196
+ }
1197
+ return {
1198
+ filters: {
1199
+ archived,
1200
+ name,
1201
+ query
1202
+ },
1203
+ context: {
1204
+ name: this.name,
1205
+ id: this.id,
1206
+ embedder: this.embedder.name
1207
+ },
1208
+ items
1209
+ };
1210
+ }
1211
+ };
1212
+ createItemsTable = async () => {
1213
+ const { db: db2 } = await postgresClient();
1214
+ const tableName = this.getTableName();
1215
+ console.log("[EXULU] Creating table: " + tableName);
1216
+ return await db2.schema.createTable(tableName, (table) => {
1217
+ console.log("[EXULU] Creating fields for table.", this.fields);
1218
+ table.uuid("id").primary().defaultTo(db2.fn.uuid());
1219
+ table.string("name", 100);
1220
+ table.text("description");
1221
+ table.string("tags", 100);
1222
+ table.boolean("archived").defaultTo(false);
1223
+ table.string("external_id", 100);
1224
+ table.integer("textLength");
1225
+ table.string("source", 100);
1226
+ for (const field of this.fields) {
1227
+ const { type, name } = field;
1228
+ if (!type || !name) {
1229
+ continue;
1230
+ }
1231
+ mapType(table, type, sanitizeName(name));
1232
+ }
1233
+ table.timestamps(true, true);
1234
+ });
1235
+ };
1236
+ createChunksTable = async () => {
1237
+ const { db: db2 } = await postgresClient();
1238
+ const tableName = this.getChunksTableName();
1239
+ console.log("[EXULU] Creating table: " + tableName);
1240
+ return await db2.schema.createTable(tableName, (table) => {
1241
+ table.uuid("id").primary().defaultTo(db2.fn.uuid());
1242
+ table.uuid("source").references("id").inTable(this.getTableName());
1243
+ table.text("content");
1244
+ table.integer("chunk_index");
1245
+ table.specificType("embedding", `vector(${this.embedder.vectorDimensions})`);
1246
+ table.timestamps(true, true);
1247
+ });
1248
+ };
1249
+ // Exports the context as a tool that can be used by an agent
1250
+ tool = () => {
1251
+ return new ExuluTool({
1252
+ id: this.id,
1253
+ name: `${this.name} context`,
1254
+ type: "context",
1255
+ inputSchema: z.object({
1256
+ query: z.string()
1257
+ }),
1258
+ outputSchema: z.object({
1259
+ // todo check if result format is still correct based on above getItems function
1260
+ results: z.array(z.object({
1261
+ count: z.number(),
1262
+ results: z.array(z.object({
1263
+ id: z.string(),
1264
+ content: z.string(),
1265
+ metadata: z.record(z.any())
1266
+ })),
1267
+ errors: z.array(z.string()).optional()
1268
+ }))
1269
+ }),
1270
+ description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
1271
+ execute: async ({ context }) => {
1272
+ return await this.getItems({
1273
+ page: 1,
1274
+ limit: 10,
1275
+ query: context.query,
1276
+ statistics: {
1277
+ label: this.name,
1278
+ trigger: "agent"
1279
+ }
1280
+ });
1281
+ }
1282
+ });
1283
+ };
1284
+ sources = {
1285
+ add: (inputs) => {
1286
+ const source = new ExuluSource({
1287
+ ...inputs,
1288
+ context: this.id
1289
+ });
1290
+ this._sources.push(source);
1291
+ return source;
1292
+ },
1293
+ get: (id) => {
1294
+ if (id) {
1295
+ return this._sources.find((source) => source.id === id);
1296
+ }
1297
+ return this._sources ?? [];
1298
+ }
1299
+ };
1300
+ };
1301
+ var ExuluSource = class {
1302
+ id;
1303
+ name;
1304
+ description;
1305
+ updaters;
1306
+ context;
1307
+ constructor({ id, name, description, updaters, context }) {
1308
+ this.id = id;
1309
+ this.name = name;
1310
+ this.description = description;
1311
+ this.context = context;
1312
+ this.updaters = updaters.map((updater) => {
1313
+ if (updater.type === "webhook") {
1314
+ return {
1315
+ ...updater,
1316
+ slug: `/contexts/${context}/sources/${this.id}/${updater.id}/webhook`
1317
+ };
1318
+ }
1319
+ return updater;
1320
+ });
1321
+ }
1322
+ };
1323
+ var updateStatistic = async (statistic) => {
1324
+ const currentDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1325
+ const { db: db2 } = await postgresClient();
1326
+ await db2.from("statistics").update({
1327
+ total: db2.raw("total + ?", [statistic.count ?? 1]),
1328
+ timeseries: db2.raw("CASE WHEN date = ? THEN array_append(timeseries, ?) ELSE timeseries END", [currentDate, { date: currentDate, count: statistic.count ?? 1 }])
1329
+ }).where({
1330
+ name: statistic.name,
1331
+ label: statistic.label,
1332
+ type: statistic.type
1333
+ }).onConflict("name").merge();
1334
+ };
1335
+
1336
+ // src/registry/index.ts
1337
+ import "express";
1338
+
1339
+ // src/registry/routes.ts
1340
+ import "express";
1341
+
1342
+ // src/registry/rate-limiter.ts
1343
+ var rateLimiter = async (key, windowSeconds, limit, points) => {
1344
+ if (!redisClient) {
1345
+ return {
1346
+ status: false,
1347
+ retryAfter: 10
1348
+ // 10 seconds
1349
+ };
1350
+ }
1351
+ const redisKey = `exulu/${key}`;
1352
+ const current = await redisClient.incrBy(redisKey, points);
1353
+ if (current === points) {
1354
+ await redisClient.expire(redisKey, windowSeconds);
1355
+ }
1356
+ if (current > limit) {
1357
+ const ttl = await redisClient.ttl(redisKey);
1358
+ return {
1359
+ status: false,
1360
+ retryAfter: ttl
1361
+ };
1362
+ }
1363
+ return {
1364
+ status: true,
1365
+ retryAfter: null
1366
+ };
1367
+ };
1368
+
1369
+ // src/registry/route-validators/index.ts
1370
+ import "express";
1371
+ import { getToken } from "next-auth/jwt";
1372
+
1373
+ // src/auth/auth.ts
1374
+ import bcrypt from "bcryptjs";
1375
+ var authentication = async ({
1376
+ apikey,
1377
+ authtoken,
1378
+ internalkey,
1379
+ db: db2
1380
+ }) => {
1381
+ if (internalkey) {
1382
+ if (!process.env.INTERNAL_SECRET) {
1383
+ return {
1384
+ error: true,
1385
+ message: `Header "internal" provided, but no INTERNAL_SECRET was provided in the environment variables.`,
1386
+ code: 401
1387
+ };
1388
+ }
1389
+ if (process.env.INTERNAL_SECRET !== internalkey) {
1390
+ return {
1391
+ error: true,
1392
+ message: `Internal key was provided in header but did not match the INTERNAL_SECRET environment variable.`,
1393
+ code: 401
1394
+ };
1395
+ }
1396
+ return {
1397
+ error: false,
1398
+ code: 200,
1399
+ user: {
1400
+ type: "api",
1401
+ id: "XXXX-XXXX-XXXX-XXXX",
1402
+ email: "internal@exulu.com"
1403
+ }
1404
+ };
1405
+ }
1406
+ if (authtoken) {
1407
+ try {
1408
+ console.log("authtoken", authtoken);
1409
+ if (!authtoken?.email) {
1410
+ return {
1411
+ error: true,
1412
+ message: `No email provided in session ${JSON.stringify(authtoken)}`,
1413
+ code: 401
1414
+ };
1415
+ }
1416
+ const user = await db2.from("users").select("*").where("email", authtoken?.email).first();
1417
+ console.log("user", user);
1418
+ if (!user) {
1419
+ return {
1420
+ error: true,
1421
+ message: `No user found for email: ${authtoken.email}`,
1422
+ code: 401
1423
+ };
1424
+ }
1425
+ return {
1426
+ error: false,
1427
+ code: 200,
1428
+ user
1429
+ };
1430
+ } catch (error) {
1431
+ console.error(error);
1432
+ return {
1433
+ error: true,
1434
+ message: "Invalid token.",
1435
+ code: 401
1436
+ };
1437
+ }
1438
+ }
1439
+ if (apikey) {
1440
+ const users = await db2.from("users").select("*").where("type", "api");
1441
+ if (!users || users.length === 0) {
1442
+ return {
1443
+ error: true,
1444
+ message: `No API users found.`,
1445
+ code: 401
1446
+ };
1447
+ }
1448
+ const keyParts = apikey.split("/");
1449
+ const keyName = keyParts.pop();
1450
+ const keyValue = keyParts[0];
1451
+ if (!keyName) {
1452
+ return {
1453
+ error: true,
1454
+ message: "Provided api key does not include postfix with key name ({key}/{name}).",
1455
+ code: 401
1456
+ };
1457
+ }
1458
+ if (!keyValue) {
1459
+ return {
1460
+ error: true,
1461
+ message: "Provided api key is not in the correct format.",
1462
+ code: 401
1463
+ };
1464
+ }
1465
+ const filtered = users.filter(({ apiKey, id }) => apiKey.includes(keyName));
1466
+ for (const user of filtered) {
1467
+ const lastSlashIndex = user.apiKey.lastIndexOf("/");
1468
+ const compareValue = lastSlashIndex !== -1 ? user.apiKey.substring(0, lastSlashIndex) : user.apiKey;
1469
+ const isMatch = await bcrypt.compare(keyValue, compareValue);
1470
+ if (isMatch) {
1471
+ await db2.from("users").where({ id: user.id }).update({
1472
+ lastUsed: /* @__PURE__ */ new Date()
1473
+ }).returning("id");
1474
+ return {
1475
+ error: false,
1476
+ code: 200,
1477
+ user
1478
+ };
1479
+ }
1480
+ }
1481
+ }
1482
+ return {
1483
+ error: true,
1484
+ message: "Either an api key or authorization key must be provided.",
1485
+ code: 401
1486
+ };
1487
+ };
1488
+
1489
+ // src/registry/route-validators/index.ts
1490
+ var requestValidators = {
1491
+ authenticate: async (req) => {
1492
+ const apikey = req.headers["exulu-api-key"] || null;
1493
+ const { db: db2 } = await postgresClient();
1494
+ let authtoken = null;
1495
+ if (typeof apikey !== "string") {
1496
+ const secret = process.env.NEXTAUTH_SECRET;
1497
+ authtoken = await getToken({ req, secret });
1498
+ }
1499
+ return await authentication({
1500
+ authtoken,
1501
+ apikey,
1502
+ db: db2
1503
+ });
1504
+ },
1505
+ workflows: (req) => {
1506
+ const contentType = req.headers["content-type"] || "";
1507
+ if (!contentType.includes("application/json")) {
1508
+ return {
1509
+ error: true,
1510
+ code: 400,
1511
+ message: "Unsupported content type."
1512
+ };
1513
+ }
1514
+ if (!req.body) {
1515
+ return {
1516
+ error: true,
1517
+ code: 400,
1518
+ message: "Missing body."
1519
+ };
1520
+ }
1521
+ if (!req.body.agent) {
1522
+ return {
1523
+ error: true,
1524
+ code: 400,
1525
+ message: "Missing agent in body."
1526
+ };
1527
+ }
1528
+ if (!req.body.session) {
1529
+ return {
1530
+ error: true,
1531
+ code: 400,
1532
+ message: "Missing session in body."
1533
+ };
1534
+ }
1535
+ if (!req.body.inputs) {
1536
+ return {
1537
+ error: true,
1538
+ code: 400,
1539
+ message: "Missing inputs in body."
1540
+ };
1541
+ }
1542
+ if (!req.body.label) {
1543
+ return {
1544
+ error: true,
1545
+ code: 400,
1546
+ message: "Missing label for job in body."
1547
+ };
1548
+ }
1549
+ return {
1550
+ error: false
1551
+ };
1552
+ },
1553
+ embedders: (req, configuration) => {
1554
+ const contentType = req.headers["content-type"] || "";
1555
+ if (!contentType.includes("application/json")) {
1556
+ return {
1557
+ error: true,
1558
+ code: 400,
1559
+ message: "Unsupported content type."
1560
+ };
1561
+ }
1562
+ if (!req.body) {
1563
+ return {
1564
+ error: true,
1565
+ code: 400,
1566
+ message: "Missing body."
1567
+ };
1568
+ }
1569
+ if (!req.body.inputs) {
1570
+ return {
1571
+ error: true,
1572
+ code: 400,
1573
+ message: "Missing inputs."
1574
+ };
1575
+ }
1576
+ if (!req.body.label) {
1577
+ return {
1578
+ error: true,
1579
+ code: 400,
1580
+ message: "Missing label for job in body."
1581
+ };
1582
+ }
1583
+ if (configuration) {
1584
+ for (const key in configuration) {
1585
+ if (!req.body.configuration[key]) {
1586
+ return {
1587
+ error: true,
1588
+ code: 400,
1589
+ message: `Missing ${key} in body.configuration.`
1590
+ };
1591
+ }
1592
+ }
1593
+ }
1594
+ return {
1595
+ error: false
1596
+ };
1597
+ },
1598
+ agents: (req) => {
1599
+ console.log("[EXULU] validating request body and headers.", req.body);
1600
+ const contentType = req.headers["content-type"] || "";
1601
+ if (!contentType.includes("application/json")) {
1602
+ return {
1603
+ error: true,
1604
+ code: 400,
1605
+ message: "Unsupported content type."
1606
+ };
1607
+ }
1608
+ if (!req.body) {
1609
+ return {
1610
+ error: true,
1611
+ code: 400,
1612
+ message: "Missing body."
1613
+ };
1614
+ }
1615
+ if (!req.body.threadId) {
1616
+ return {
1617
+ error: true,
1618
+ code: 400,
1619
+ message: "Missing threadId in body."
1620
+ };
1621
+ }
1622
+ if (!req.body.resourceId) {
1623
+ return {
1624
+ error: true,
1625
+ code: 400,
1626
+ message: "Missing resourceId in body."
1627
+ };
1628
+ }
1629
+ if (!req.body.messages) {
1630
+ return {
1631
+ error: true,
1632
+ code: 400,
1633
+ message: 'Missing "messages" property in body.'
1634
+ };
1635
+ }
1636
+ return {
1637
+ error: false
1638
+ };
1639
+ }
1640
+ };
1641
+
1642
+ // src/registry/routes.ts
1643
+ import { zerialize } from "zodex";
1644
+
1645
+ // src/bullmq/queues.ts
1646
+ import { Queue as Queue3 } from "bullmq";
1647
+ var ExuluQueues = class {
1648
+ queues;
1649
+ constructor() {
1650
+ this.queues = [];
1651
+ }
1652
+ queue(name) {
1653
+ return this.queues.find((x) => x.name === name);
1654
+ }
1655
+ use(name) {
1656
+ const existing = this.queues.find((x) => x.name === name);
1657
+ if (existing) {
1658
+ return existing;
1659
+ }
1660
+ const newQueue = new Queue3(`${name}`, { connection: redisServer });
1661
+ this.queues.push(newQueue);
1662
+ return newQueue;
1663
+ }
1664
+ };
1665
+ var queues = new ExuluQueues();
1666
+
1667
+ // types/models/vector-methods.ts
1668
+ var VectorMethodEnum = {
1669
+ "cosineDistance": "cosineDistance",
1670
+ "l1Distance": "l1Distance",
1671
+ "l2Distance": "l2Distance",
1672
+ "hammingDistance": "hammingDistance",
1673
+ "jaccardDistance": "jaccardDistance",
1674
+ "maxInnerProduct": "maxInnerProduct"
1675
+ };
1676
+
1677
+ // src/registry/routes.ts
1678
+ import express from "express";
1679
+ import { ApolloServer } from "@apollo/server";
1680
+ import cors from "cors";
1681
+ import "reflect-metadata";
1682
+
1683
+ // src/registry/utils/graphql.ts
1684
+ import { makeExecutableSchema } from "@graphql-tools/schema";
1685
+ import GraphQLJSON from "graphql-type-json";
1686
+ var map = (field) => {
1687
+ let type;
1688
+ switch (field.type) {
1689
+ case "text":
1690
+ case "shortText":
1691
+ case "longText":
1692
+ case "code":
1693
+ type = "String";
1694
+ break;
1695
+ case "number":
1696
+ type = "Float";
1697
+ break;
1698
+ case "boolean":
1699
+ type = "Boolean";
1700
+ break;
1701
+ case "json":
1702
+ type = "JSON";
1703
+ break;
1704
+ case "date":
1705
+ type = "String";
1706
+ break;
1707
+ default:
1708
+ type = "String";
1709
+ }
1710
+ return type;
1711
+ };
1712
+ function createTypeDefs(table) {
1713
+ const fields = table.fields.map((field) => {
1714
+ let type;
1715
+ type = map(field);
1716
+ const required = field.required ? "!" : "";
1717
+ return ` ${field.name}: ${type}${required}`;
1718
+ });
1719
+ const typeDef = `
1720
+ type ${table.name.singular} {
1721
+ ${fields.join("\n")}
1722
+ id: ID!
1723
+ createdAt: String!
1724
+ updatedAt: String!
1725
+ }
1726
+ `;
1727
+ const inputDef = `
1728
+ input ${table.name.singular}Input {
1729
+ ${table.fields.map((f) => ` ${f.name}: ${map(f)}`).join("\n")}
1730
+ }
1731
+ `;
1732
+ return typeDef + inputDef;
1733
+ }
1734
+ function createFilterTypeDefs(table) {
1735
+ const fieldFilters = table.fields.map((field) => {
1736
+ let type;
1737
+ type = map(field);
1738
+ return `
1739
+ ${field.name}: FilterOperator${type}`;
1740
+ });
1741
+ const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
1742
+ const operatorTypes = `
1743
+ input FilterOperatorString {
1744
+ eq: String
1745
+ ne: String
1746
+ in: [String]
1747
+ contains: String
1748
+ }
1749
+
1750
+ input FilterOperatorFloat {
1751
+ eq: Float
1752
+ ne: Float
1753
+ in: [Float]
1754
+ }
1755
+
1756
+ input FilterOperatorBoolean {
1757
+ eq: Boolean
1758
+ ne: Boolean
1759
+ in: [Boolean]
1760
+ }
1761
+
1762
+ input FilterOperatorJSON {
1763
+ eq: JSON
1764
+ ne: JSON
1765
+ in: [JSON]
1766
+ }
1767
+
1768
+ input SortBy {
1769
+ field: String!
1770
+ direction: SortDirection!
1771
+ }
1772
+
1773
+ enum SortDirection {
1774
+ ASC
1775
+ DESC
1776
+ }
1777
+
1778
+ input Filter${tableNameSingularUpperCaseFirst} {
1779
+ ${fieldFilters.join("\n")}
1780
+ }`;
1781
+ return operatorTypes;
1782
+ }
1783
+ var getRequestedFields = (info) => {
1784
+ const selections = info.operation.selectionSet.selections[0].selectionSet.selections;
1785
+ const itemsSelection = selections.find((s) => s.name.value === "items");
1786
+ const fields = itemsSelection ? Object.keys(itemsSelection.selectionSet.selections.reduce((acc, field) => {
1787
+ acc[field.name.value] = true;
1788
+ return acc;
1789
+ }, {})) : Object.keys(selections.reduce((acc, field) => {
1790
+ acc[field.name.value] = true;
1791
+ return acc;
1792
+ }, {}));
1793
+ return fields.filter((field) => field !== "pageInfo" && field !== "items");
1794
+ };
1795
+ function createMutations(table) {
1796
+ const tableNamePlural = table.name.plural.toLowerCase();
1797
+ const tableNameSingular = table.name.singular.toLowerCase();
1798
+ return {
1799
+ [`${tableNamePlural}CreateOne`]: async (_, args, context, info) => {
1800
+ const { db: db2 } = context;
1801
+ const requestedFields = getRequestedFields(info);
1802
+ const results = await db2(tableNamePlural).insert({
1803
+ ...args.input,
1804
+ createdAt: /* @__PURE__ */ new Date(),
1805
+ updatedAt: /* @__PURE__ */ new Date()
1806
+ }).returning(requestedFields);
1807
+ console.log("requestedFields", requestedFields);
1808
+ return results[0];
1809
+ },
1810
+ [`${tableNamePlural}UpdateOne`]: async (_, args, context, info) => {
1811
+ const { db: db2 } = context;
1812
+ const { where, input } = args;
1813
+ await db2(tableNamePlural).where(where).update({
1814
+ ...input,
1815
+ updatedAt: /* @__PURE__ */ new Date()
1816
+ });
1817
+ const requestedFields = getRequestedFields(info);
1818
+ const result = await db2.from(tableNamePlural).select(requestedFields).where(where).first();
1819
+ return result;
1820
+ },
1821
+ [`${tableNamePlural}UpdateOneById`]: async (_, args, context, info) => {
1822
+ const { id, input } = args;
1823
+ const { db: db2 } = context;
1824
+ await db2(tableNamePlural).where({ id }).update({
1825
+ ...input,
1826
+ updatedAt: /* @__PURE__ */ new Date()
1827
+ });
1828
+ const requestedFields = getRequestedFields(info);
1829
+ const result = await db2.from(tableNamePlural).select(requestedFields).where({ id }).first();
1830
+ return result;
1831
+ },
1832
+ [`${tableNamePlural}RemoveOne`]: async (_, args, context, info) => {
1833
+ const { db: db2 } = context;
1834
+ const { where } = args;
1835
+ const requestedFields = getRequestedFields(info);
1836
+ const result = await db2.from(tableNamePlural).select(requestedFields).where(where).first();
1837
+ await db2(tableNamePlural).where(where).del();
1838
+ return result;
1839
+ },
1840
+ [`${tableNamePlural}RemoveOneById`]: async (_, args, context, info) => {
1841
+ const { id } = args;
1842
+ const { db: db2 } = context;
1843
+ const requestedFields = getRequestedFields(info);
1844
+ const result = await db2.from(tableNamePlural).select(requestedFields).where({ id }).first();
1845
+ await db2(tableNamePlural).where({ id }).del();
1846
+ return result;
1847
+ }
1848
+ };
1849
+ }
1850
+ function createQueries(table) {
1851
+ const tableNamePlural = table.name.plural.toLowerCase();
1852
+ const tableNameSingular = table.name.singular.toLowerCase();
1853
+ const applyFilters = (query, filters) => {
1854
+ filters.forEach((filter) => {
1855
+ Object.entries(filter).forEach(([fieldName, operators]) => {
1856
+ if (operators) {
1857
+ if (operators.eq !== void 0) {
1858
+ query = query.where(fieldName, operators.eq);
1859
+ }
1860
+ if (operators.ne !== void 0) {
1861
+ query = query.whereRaw(`?? IS DISTINCT FROM ?`, [fieldName, operators.ne]);
1862
+ }
1863
+ if (operators.in !== void 0) {
1864
+ query = query.whereIn(fieldName, operators.in);
1865
+ }
1866
+ if (operators.contains !== void 0) {
1867
+ query = query.where(fieldName, "like", `%${operators.contains}%`);
1868
+ }
1869
+ }
1870
+ });
1871
+ });
1872
+ return query;
1873
+ };
1874
+ const applySorting = (query, sort) => {
1875
+ if (sort) {
1876
+ query = query.orderBy(sort.field, sort.direction.toLowerCase());
1877
+ }
1878
+ return query;
1879
+ };
1880
+ return {
1881
+ [`${tableNameSingular}ById`]: async (_, args, context, info) => {
1882
+ const { db: db2 } = context;
1883
+ const requestedFields = getRequestedFields(info);
1884
+ const result = await db2.from(tableNamePlural).select(requestedFields).where({ id: args.id }).first();
1885
+ return result;
1886
+ },
1887
+ [`${tableNameSingular}One`]: async (_, args, context, info) => {
1888
+ const { filters = [], sort } = args;
1889
+ const { db: db2 } = context;
1890
+ const requestedFields = getRequestedFields(info);
1891
+ let query = db2.from(tableNamePlural).select(requestedFields);
1892
+ query = applyFilters(query, filters);
1893
+ query = applySorting(query, sort);
1894
+ const result = await query.first();
1895
+ return result;
1896
+ },
1897
+ [`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
1898
+ const { limit = 10, page = 0, filters = [], sort } = args;
1899
+ const { db: db2 } = context;
1900
+ console.log("page", page);
1901
+ let baseQuery = db2(tableNamePlural);
1902
+ baseQuery = applyFilters(baseQuery, filters);
1903
+ const [{ count }] = await baseQuery.clone().count("* as count");
1904
+ const itemCount = Number(count);
1905
+ const pageCount = Math.ceil(itemCount / limit);
1906
+ const currentPage = page;
1907
+ const hasPreviousPage = currentPage > 1;
1908
+ const hasNextPage = currentPage < pageCount - 1;
1909
+ let dataQuery = baseQuery.clone();
1910
+ const requestedFields = getRequestedFields(info);
1911
+ dataQuery = applySorting(dataQuery, sort);
1912
+ if (page > 1) {
1913
+ dataQuery = dataQuery.offset((page - 1) * limit);
1914
+ }
1915
+ const items = await dataQuery.select(requestedFields).limit(limit);
1916
+ console.log("items", items);
1917
+ console.log("query", dataQuery.toQuery());
1918
+ return {
1919
+ pageInfo: {
1920
+ pageCount,
1921
+ itemCount,
1922
+ currentPage,
1923
+ hasPreviousPage,
1924
+ hasNextPage
1925
+ },
1926
+ items
1927
+ };
1928
+ }
1929
+ };
1930
+ }
1931
+ function createSDL(tables) {
1932
+ let typeDefs = `
1933
+ scalar JSON
1934
+
1935
+ type Query {
1936
+ `;
1937
+ let mutationDefs = `
1938
+ type Mutation {
1939
+ `;
1940
+ let modelDefs = "";
1941
+ const resolvers = { JSON: GraphQLJSON, Query: {}, Mutation: {} };
1942
+ for (const table of tables) {
1943
+ const tableNamePlural = table.name.plural.toLowerCase();
1944
+ const tableNameSingular = table.name.singular.toLowerCase();
1945
+ const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
1946
+ typeDefs += `
1947
+ ${tableNameSingular}ById(id: ID!): ${tableNameSingular}
1948
+ ${tableNamePlural}Pagination(limit: Int, page: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingularUpperCaseFirst}PaginationResult
1949
+ ${tableNameSingular}One(filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}
1950
+ `;
1951
+ mutationDefs += `
1952
+ ${tableNamePlural}CreateOne(input: ${tableNameSingular}Input!): ${tableNameSingular}
1953
+ ${tableNamePlural}UpdateOne(where: JSON!, input: ${tableNameSingular}Input!): ${tableNameSingular}
1954
+ ${tableNamePlural}UpdateOneById(id: ID!, input: ${tableNameSingular}Input!): ${tableNameSingular}
1955
+ ${tableNamePlural}RemoveOne(where: JSON!): ${tableNameSingular}
1956
+ ${tableNamePlural}RemoveOneById(id: ID!): ${tableNameSingular}
1957
+ `;
1958
+ modelDefs += createTypeDefs(table);
1959
+ modelDefs += createFilterTypeDefs(table);
1960
+ modelDefs += `
1961
+ type ${tableNameSingularUpperCaseFirst}PaginationResult {
1962
+ pageInfo: PageInfo!
1963
+ items: [${tableNameSingular}]!
1964
+ }
1965
+
1966
+ type PageInfo {
1967
+ pageCount: Int!
1968
+ itemCount: Int!
1969
+ currentPage: Int!
1970
+ hasPreviousPage: Boolean!
1971
+ hasNextPage: Boolean!
1972
+ }
1973
+ `;
1974
+ Object.assign(resolvers.Query, createQueries(table));
1975
+ Object.assign(resolvers.Mutation, createMutations(table));
1976
+ }
1977
+ typeDefs += "}\n";
1978
+ mutationDefs += "}\n";
1979
+ const fullSDL = typeDefs + mutationDefs + modelDefs;
1980
+ const schema = makeExecutableSchema({
1981
+ typeDefs: fullSDL,
1982
+ resolvers
1983
+ });
1984
+ console.log("\n\u{1F4CA} GraphQL Schema Overview\n");
1985
+ const queriesTable = Object.keys(resolvers.Query).map((query) => ({
1986
+ "Operation Type": "Query",
1987
+ "Name": query,
1988
+ "Description": "Retrieves data"
1989
+ }));
1990
+ const mutationsTable = Object.keys(resolvers.Mutation).map((mutation) => ({
1991
+ "Operation Type": "Mutation",
1992
+ "Name": mutation,
1993
+ "Description": "Modifies data"
1994
+ }));
1995
+ const typesTable = tables.flatMap(
1996
+ (table) => table.fields.map((field) => ({
1997
+ "Type": table.name.singular,
1998
+ "Field": field.name,
1999
+ "Field Type": field.type,
2000
+ "Required": field.required ? "Yes" : "No"
2001
+ }))
2002
+ );
2003
+ console.log("\u{1F50D} Operations:");
2004
+ console.table([...queriesTable, ...mutationsTable]);
2005
+ console.log("\n\u{1F4DD} Types and Fields:");
2006
+ console.table(typesTable);
2007
+ console.log("\n");
2008
+ return schema;
2009
+ }
2010
+
2011
+ // src/registry/routes.ts
2012
+ import { expressMiddleware } from "@as-integrations/express5";
2013
+
2014
+ // src/registry/uppy.ts
2015
+ import "express";
2016
+ import { getToken as getToken2 } from "next-auth/jwt";
2017
+ var bodyParser = __require("body-parser");
2018
+ var createUppyRoutes = async (app) => {
2019
+ const {
2020
+ S3Client,
2021
+ AbortMultipartUploadCommand,
2022
+ CompleteMultipartUploadCommand,
2023
+ CreateMultipartUploadCommand,
2024
+ GetObjectCommand,
2025
+ ListPartsCommand,
2026
+ PutObjectCommand,
2027
+ UploadPartCommand,
2028
+ ListObjectsV2Command
2029
+ } = __require("@aws-sdk/client-s3");
2030
+ const { getSignedUrl } = __require("@aws-sdk/s3-request-presigner");
2031
+ const {
2032
+ STSClient,
2033
+ GetFederationTokenCommand
2034
+ } = __require("@aws-sdk/client-sts");
2035
+ const policy = {
2036
+ Version: "2012-10-17",
2037
+ Statement: [
2038
+ {
2039
+ Effect: "Allow",
2040
+ Action: [
2041
+ "s3:PutObject"
2042
+ ],
2043
+ Resource: [
2044
+ `arn:aws:s3:::${process.env.COMPANION_S3_BUCKET}/*`,
2045
+ `arn:aws:s3:::${process.env.COMPANION_S3_BUCKET}`
2046
+ ]
2047
+ }
2048
+ ]
2049
+ };
2050
+ let s3Client;
2051
+ let stsClient;
2052
+ const expiresIn = 60 * 60 * 24 * 1;
2053
+ function getS3Client() {
2054
+ s3Client ??= new S3Client({
2055
+ region: process.env.COMPANION_S3_REGION,
2056
+ ...process.env.COMPANION_S3_ENDPOINT && {
2057
+ forcePathStyle: true,
2058
+ endpoint: process.env.COMPANION_S3_ENDPOINT
2059
+ },
2060
+ credentials: {
2061
+ accessKeyId: process.env.COMPANION_S3_KEY,
2062
+ secretAccessKey: process.env.COMPANION_S3_SECRET
2063
+ }
2064
+ });
2065
+ return s3Client;
2066
+ }
2067
+ function getSTSClient() {
2068
+ stsClient ??= new STSClient({
2069
+ region: process.env.COMPANION_S3_REGION,
2070
+ ...process.env.COMPANION_S3_ENDPOINT && { endpoint: process.env.COMPANION_S3_ENDPOINT },
2071
+ credentials: {
2072
+ accessKeyId: process.env.COMPANION_S3_KEY,
2073
+ secretAccessKey: process.env.COMPANION_S3_SECRET
2074
+ }
2075
+ });
2076
+ return stsClient;
2077
+ }
2078
+ app.use(bodyParser.urlencoded({ extended: true }), bodyParser.json());
2079
+ app.get("/", (req, res) => {
2080
+ res.json("Exulu upload server.");
2081
+ });
2082
+ app.get("/s3/list", async (req, res, next) => {
2083
+ const apikey = req.headers["exulu-api-key"] || null;
2084
+ let authtoken = null;
2085
+ if (typeof apikey !== "string") {
2086
+ const secret = process.env.NEXTAUTH_SECRET;
2087
+ authtoken = await getToken2({ req, secret });
2088
+ }
2089
+ const { db: db2 } = await postgresClient();
2090
+ const authenticationResult = await authentication({
2091
+ authtoken,
2092
+ apikey,
2093
+ db: db2
2094
+ });
2095
+ if (!authenticationResult.user?.id) {
2096
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2097
+ return;
2098
+ }
2099
+ const { prefix = "" } = req.query;
2100
+ if (typeof prefix !== "string") {
2101
+ res.status(400).json({ error: "Invalid prefix parameter. Must be a string." });
2102
+ return;
2103
+ }
2104
+ if (authenticationResult.user.type !== "api" && !prefix.includes(authenticationResult.user.id)) {
2105
+ res.status(405).json({ error: "Not allowed to list files in this folder based on authenticated user." });
2106
+ return;
2107
+ }
2108
+ try {
2109
+ const command = new ListObjectsV2Command({
2110
+ Bucket: process.env.COMPANION_S3_BUCKET,
2111
+ Prefix: prefix,
2112
+ MaxKeys: 1e3
2113
+ // Adjust this value based on your needs
2114
+ });
2115
+ const data = await getS3Client().send(command);
2116
+ const files = data.Contents?.map((item) => ({
2117
+ key: item.Key,
2118
+ size: item.Size,
2119
+ lastModified: item.LastModified
2120
+ })) || [];
2121
+ res.setHeader("Access-Control-Allow-Origin", "*");
2122
+ res.status(200).json({
2123
+ files,
2124
+ isTruncated: data.IsTruncated,
2125
+ nextContinuationToken: data.NextContinuationToken
2126
+ });
2127
+ } catch (err) {
2128
+ next(err);
2129
+ }
2130
+ });
2131
+ app.get("/s3/download", async (req, res, next) => {
2132
+ const apikey = req.headers["exulu-api-key"] || null;
2133
+ const internalkey = req.headers["internal-key"] || null;
2134
+ const { db: db2 } = await postgresClient();
2135
+ let authtoken = null;
2136
+ if (typeof apikey !== "string" && typeof internalkey !== "string") {
2137
+ const secret = process.env.NEXTAUTH_SECRET;
2138
+ authtoken = await getToken2({ req, secret });
2139
+ }
2140
+ const authenticationResult = await authentication({
2141
+ authtoken,
2142
+ apikey,
2143
+ internalkey,
2144
+ db: db2
2145
+ });
2146
+ if (!authenticationResult.user?.id) {
2147
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2148
+ return;
2149
+ }
2150
+ const { key } = req.query;
2151
+ if (typeof key !== "string" || key.trim() === "") {
2152
+ res.status(400).json({ error: "Missing or invalid `key` query parameter." });
2153
+ return;
2154
+ }
2155
+ if (authenticationResult.user.type !== "api" && !key.includes(authenticationResult.user.id)) {
2156
+ res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
2157
+ return;
2158
+ }
2159
+ try {
2160
+ const url = await getSignedUrl(
2161
+ getS3Client(),
2162
+ new GetObjectCommand({
2163
+ Bucket: process.env.COMPANION_S3_BUCKET,
2164
+ Key: key
2165
+ }),
2166
+ { expiresIn }
2167
+ );
2168
+ res.setHeader("Access-Control-Allow-Origin", "*");
2169
+ res.json({ url, method: "GET", expiresIn });
2170
+ } catch (err) {
2171
+ next(err);
2172
+ }
2173
+ });
2174
+ app.get("/s3/sts", (req, res, next) => {
2175
+ getSTSClient().send(new GetFederationTokenCommand({
2176
+ Name: "Exulu",
2177
+ // The duration, in seconds, of the role session. The value specified
2178
+ // can range from 900 seconds (15 minutes) up to the maximum session
2179
+ // duration set for the role.
2180
+ DurationSeconds: expiresIn,
2181
+ Policy: JSON.stringify(policy)
2182
+ })).then((response) => {
2183
+ res.setHeader("Access-Control-Allow-Origin", "*");
2184
+ res.setHeader("Cache-Control", `public,max-age=${expiresIn}`);
2185
+ res.json({
2186
+ credentials: response.Credentials,
2187
+ bucket: process.env.COMPANION_S3_BUCKET,
2188
+ region: process.env.COMPANION_S3_REGION
2189
+ });
2190
+ }, next);
2191
+ });
2192
+ const validateFileParameters = (filename, contentType) => {
2193
+ if (!filename || !contentType) {
2194
+ throw new Error("Missing required parameters: filename and content type are required");
2195
+ }
2196
+ };
2197
+ const extractFileParameters = (req) => {
2198
+ const isPostRequest = req.method === "POST";
2199
+ const params = isPostRequest ? req.body : req.query;
2200
+ return {
2201
+ filename: params.filename,
2202
+ contentType: params.type
2203
+ };
2204
+ };
2205
+ const generateS3Key = (filename) => `${crypto.randomUUID()}-${filename}`;
2206
+ const signOnServer = async (req, res, next) => {
2207
+ const apikey = req.headers["exulu-api-key"] || null;
2208
+ const { db: db2 } = await postgresClient();
2209
+ let authtoken = null;
2210
+ if (typeof apikey !== "string") {
2211
+ const secret = process.env.NEXTAUTH_SECRET;
2212
+ authtoken = await getToken2({ req, secret });
2213
+ }
2214
+ const authenticationResult = await authentication({
2215
+ authtoken,
2216
+ apikey,
2217
+ db: db2
2218
+ });
2219
+ if (!authenticationResult.user?.id) {
2220
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2221
+ return;
2222
+ }
2223
+ const { filename, contentType } = extractFileParameters(req);
2224
+ validateFileParameters(filename, contentType);
2225
+ const key = generateS3Key(filename);
2226
+ let folder = "";
2227
+ if (authenticationResult.user.type === "api") {
2228
+ folder = `api/`;
2229
+ } else {
2230
+ folder = `${authenticationResult.user.id}/`;
2231
+ }
2232
+ getSignedUrl(
2233
+ getS3Client(),
2234
+ new PutObjectCommand({
2235
+ Bucket: process.env.COMPANION_S3_BUCKET,
2236
+ Key: folder + key,
2237
+ ContentType: contentType
2238
+ }),
2239
+ { expiresIn }
2240
+ ).then((url) => {
2241
+ res.setHeader("Access-Control-Allow-Origin", "*");
2242
+ res.json({
2243
+ url,
2244
+ method: "PUT"
2245
+ });
2246
+ res.end();
2247
+ }, next);
2248
+ };
2249
+ app.get("/s3/params", async (req, res, next) => {
2250
+ return await signOnServer(req, res, next);
2251
+ });
2252
+ app.post("/s3/sign", async (req, res, next) => {
2253
+ return await signOnServer(req, res, next);
2254
+ });
2255
+ app.post("/s3/multipart", async (req, res, next) => {
2256
+ const apikey = req.headers["exulu-api-key"] || null;
2257
+ const { db: db2 } = await postgresClient();
2258
+ let authtoken = null;
2259
+ if (typeof apikey !== "string") {
2260
+ const secret = process.env.NEXTAUTH_SECRET;
2261
+ authtoken = await getToken2({ req, secret });
2262
+ }
2263
+ const authenticationResult = await authentication({
2264
+ authtoken,
2265
+ apikey,
2266
+ db: db2
2267
+ });
2268
+ if (!authenticationResult.user?.id) {
2269
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2270
+ return;
2271
+ }
2272
+ const client = getS3Client();
2273
+ const { type, metadata, filename } = req.body;
2274
+ if (typeof filename !== "string") {
2275
+ return res.status(400).json({ error: "s3: content filename must be a string" });
2276
+ }
2277
+ if (typeof type !== "string") {
2278
+ return res.status(400).json({ error: "s3: content type must be a string" });
2279
+ }
2280
+ const key = `${crypto.randomUUID()}-${filename}`;
2281
+ let folder = "";
2282
+ if (authenticationResult.user.type === "api") {
2283
+ folder = `api/`;
2284
+ } else {
2285
+ folder = `${authenticationResult.user.id}/`;
2286
+ }
2287
+ const params = {
2288
+ Bucket: process.env.COMPANION_S3_BUCKET,
2289
+ Key: folder + key,
2290
+ ContentType: type,
2291
+ Metadata: metadata
2292
+ };
2293
+ const command = new CreateMultipartUploadCommand(params);
2294
+ return client.send(command, (err, data) => {
2295
+ if (err) {
2296
+ next(err);
2297
+ return;
2298
+ }
2299
+ res.setHeader("Access-Control-Allow-Origin", "*");
2300
+ res.json({
2301
+ key: data.Key,
2302
+ uploadId: data.UploadId
2303
+ });
2304
+ });
2305
+ });
2306
+ function validatePartNumber(partNumber) {
2307
+ partNumber = Number(partNumber);
2308
+ return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 1e4;
2309
+ }
2310
+ app.get("/s3/multipart/:uploadId/:partNumber", (req, res, next) => {
2311
+ const { uploadId, partNumber } = req.params;
2312
+ const { key } = req.query;
2313
+ if (!validatePartNumber(partNumber)) {
2314
+ return res.status(400).json({ error: "s3: the part number must be an integer between 1 and 10000." });
2315
+ }
2316
+ if (typeof key !== "string") {
2317
+ return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' });
2318
+ }
2319
+ return getSignedUrl(getS3Client(), new UploadPartCommand({
2320
+ Bucket: process.env.COMPANION_S3_BUCKET,
2321
+ Key: key,
2322
+ UploadId: uploadId,
2323
+ PartNumber: partNumber,
2324
+ Body: ""
2325
+ }), { expiresIn }).then((url) => {
2326
+ res.setHeader("Access-Control-Allow-Origin", "*");
2327
+ res.json({ url, expires: expiresIn });
2328
+ }, next);
2329
+ });
2330
+ app.get("/s3/multipart/:uploadId", (req, res, next) => {
2331
+ const client = getS3Client();
2332
+ const { uploadId } = req.params;
2333
+ const { key } = req.query;
2334
+ if (typeof key !== "string") {
2335
+ res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' });
2336
+ return;
2337
+ }
2338
+ const parts = [];
2339
+ function listPartsPage(startAt) {
2340
+ client.send(new ListPartsCommand({
2341
+ Bucket: process.env.COMPANION_S3_BUCKET,
2342
+ Key: key,
2343
+ UploadId: uploadId,
2344
+ PartNumberMarker: startAt
2345
+ }), (err, data) => {
2346
+ if (err) {
2347
+ next(err);
2348
+ return;
2349
+ }
2350
+ parts.push(...data.Parts);
2351
+ if (data.IsTruncated) {
2352
+ listPartsPage(data.NextPartNumberMarker);
2353
+ } else {
2354
+ res.json(parts);
2355
+ }
2356
+ });
2357
+ }
2358
+ listPartsPage(0);
2359
+ });
2360
+ function isValidPart(part) {
2361
+ return part && typeof part === "object" && Number(part.PartNumber) && typeof part.ETag === "string";
2362
+ }
2363
+ app.post("/s3/multipart/:uploadId/complete", (req, res, next) => {
2364
+ const client = getS3Client();
2365
+ const { uploadId } = req.params;
2366
+ const { key } = req.query;
2367
+ const { parts } = req.body;
2368
+ if (typeof key !== "string") {
2369
+ return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' });
2370
+ }
2371
+ if (!Array.isArray(parts) || !parts.every(isValidPart)) {
2372
+ return res.status(400).json({ error: "s3: `parts` must be an array of {ETag, PartNumber} objects." });
2373
+ }
2374
+ return client.send(new CompleteMultipartUploadCommand({
2375
+ Bucket: process.env.COMPANION_S3_BUCKET,
2376
+ Key: key,
2377
+ UploadId: uploadId,
2378
+ MultipartUpload: {
2379
+ Parts: parts
2380
+ }
2381
+ }), (err, data) => {
2382
+ if (err) {
2383
+ next(err);
2384
+ return;
2385
+ }
2386
+ res.setHeader("Access-Control-Allow-Origin", "*");
2387
+ res.json({
2388
+ location: data.Location
2389
+ });
2390
+ });
2391
+ });
2392
+ app.delete("/s3/multipart/:uploadId", (req, res, next) => {
2393
+ const client = getS3Client();
2394
+ const { uploadId } = req.params;
2395
+ const { key } = req.query;
2396
+ if (typeof key !== "string") {
2397
+ return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' });
2398
+ }
2399
+ return client.send(new AbortMultipartUploadCommand({
2400
+ Bucket: process.env.COMPANION_S3_BUCKET,
2401
+ Key: key,
2402
+ UploadId: uploadId
2403
+ }), (err) => {
2404
+ if (err) {
2405
+ next(err);
2406
+ return;
2407
+ }
2408
+ res.json({});
2409
+ });
2410
+ });
2411
+ return app;
2412
+ };
2413
+
2414
+ // src/registry/routes.ts
2415
+ var Papa = __require("papaparse");
2416
+ var global_queues = {
2417
+ logs_cleaner: "logs-cleaner"
2418
+ };
2419
+ var createRecurringJobs = async () => {
2420
+ const recurringJobSchedulersLogs = [];
2421
+ const queue = queues.use(global_queues.logs_cleaner);
2422
+ recurringJobSchedulersLogs.push({
2423
+ name: global_queues.logs_cleaner,
2424
+ pattern: "0 10 * * * *",
2425
+ ttld: "30 days",
2426
+ opts: {
2427
+ backoff: 3,
2428
+ attempts: 5,
2429
+ removeOnFail: 1e3
2430
+ }
2431
+ });
2432
+ await queue.upsertJobScheduler(
2433
+ "logs-cleaner-scheduler",
2434
+ { pattern: "0 10 * * * *" },
2435
+ // every 10 minutes
2436
+ {
2437
+ name: global_queues.logs_cleaner,
2438
+ data: { ttld: 30 },
2439
+ // time to live in days
2440
+ opts: {
2441
+ backoff: 3,
2442
+ attempts: 5,
2443
+ removeOnFail: 1e3
2444
+ }
2445
+ }
2446
+ );
2447
+ console.log("Recurring job schedulers:");
2448
+ console.table(recurringJobSchedulersLogs);
2449
+ return queue;
2450
+ };
2451
+ var createExpressRoutes = async (app, agents, embedders, tools, workflows, contexts) => {
2452
+ const routeLogs = [];
2453
+ var corsOptions = {
2454
+ origin: "*",
2455
+ optionsSuccessStatus: 200
2456
+ // some legacy browsers (IE11, various SmartTVs) choke on 204
2457
+ };
2458
+ app.use(cors(corsOptions));
2459
+ console.log(`
2460
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557
2461
+ \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
2462
+ \u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
2463
+ \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551
2464
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u255D \u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
2465
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
2466
+ `);
2467
+ console.log("Agents:");
2468
+ console.table(agents.map((agent) => {
2469
+ return {
2470
+ id: agent.id,
2471
+ name: agent.name,
2472
+ description: agent.description,
2473
+ slug: "/agents/" + agent.id,
2474
+ active: true
2475
+ };
2476
+ }));
2477
+ console.log("Contexts:");
2478
+ console.table(contexts.map((context) => {
2479
+ const sources = context.sources.get();
2480
+ return {
2481
+ id: context.id,
2482
+ name: context.name,
2483
+ description: context.description,
2484
+ embedder: context.embedder.name,
2485
+ slug: "/contexts/" + context.id,
2486
+ active: context.active,
2487
+ sources: Array.isArray(sources) ? sources.length : 0,
2488
+ sources_details: Array.isArray(sources) ? sources.map((source) => `${source.name} (${source.id})`).join(", ") : "No sources"
2489
+ };
2490
+ }));
2491
+ routeLogs.push(
2492
+ { route: "/agents", method: "GET", note: "List all agents" },
2493
+ { route: "/agents/:id", method: "GET", note: "Get specific agent" },
2494
+ { route: "/workflows", method: "GET", note: "List all workflows" },
2495
+ { route: "/workflows/:id", method: "GET", note: "Get specific workflow" },
2496
+ { route: "/contexts", method: "GET", note: "List all contexts" },
2497
+ { route: "/contexts/:id", method: "GET", note: "Get specific context" },
2498
+ { route: "/contexts/statistics", method: "GET", note: "Get context statistics" },
2499
+ { route: "/tools", method: "GET", note: "List all tools" },
2500
+ { route: "/tools/:id", method: "GET", note: "Get specific tool" },
2501
+ { route: "/statistics/timeseries", method: "POST", note: "Get time series statistics" },
2502
+ { route: "/statistics/totals", method: "POST", note: "Get totals statistics" },
2503
+ { route: "/items/:context", method: "POST", note: "Create new item in context" },
2504
+ { route: "/items/:context", method: "GET", note: "Get items from context" },
2505
+ { route: "/items/export/:context", method: "GET", note: "Export items from context" },
2506
+ { route: "/graphql", method: "POST", note: "GraphQL endpoint" }
2507
+ );
2508
+ await createRecurringJobs();
2509
+ const schema = createSDL([usersSchema, rolesSchema, agentsSchema, jobsSchema]);
2510
+ const server = new ApolloServer({ schema, introspection: true });
2511
+ await server.start();
2512
+ app.use(
2513
+ "/graphql",
2514
+ cors(),
2515
+ express.json(),
2516
+ expressMiddleware(server, {
2517
+ context: async ({ req }) => {
2518
+ const authenticationResult = await requestValidators.authenticate(req);
2519
+ if (!authenticationResult.user?.id) {
2520
+ throw new Error(authenticationResult.message);
2521
+ }
2522
+ const { db: db2 } = await postgresClient();
2523
+ return {
2524
+ req,
2525
+ db: db2
2526
+ };
2527
+ }
2528
+ })
2529
+ );
2530
+ app.get(`/agents`, async (req, res) => {
2531
+ res.status(200).json(agents);
2532
+ });
2533
+ app.get(`/agents/:id`, async (req, res) => {
2534
+ const { db: db2 } = await postgresClient();
2535
+ const id = req.params.id;
2536
+ if (!id) {
2537
+ res.status(400).json({
2538
+ message: "Missing id in request."
2539
+ });
2540
+ return;
2541
+ }
2542
+ const agent = await db2.from("agents").where({ id }).first();
2543
+ if (!agent) {
2544
+ res.status(400).json({
2545
+ message: "Agent not found in database."
2546
+ });
2547
+ return;
2548
+ }
2549
+ console.log("[EXULU] agent", agent);
2550
+ const backend = agents.find((a) => a.id === agent.backend);
2551
+ res.status(200).json({
2552
+ ...{
2553
+ name: agent.name,
2554
+ id: agent.id,
2555
+ description: agent.description,
2556
+ active: agent.active,
2557
+ public: agent.public,
2558
+ slug: backend?.slug,
2559
+ rateLimit: backend?.rateLimit,
2560
+ streaming: backend?.streaming,
2561
+ capabilities: backend?.capabilities,
2562
+ // todo add contexts
2563
+ availableTools: backend?.tools,
2564
+ enabledTools: agent.tools
2565
+ }
2566
+ });
2567
+ });
2568
+ console.log("tools", tools);
2569
+ app.get("/tools", async (req, res) => {
2570
+ res.status(200).json(tools.map((tool) => ({
2571
+ id: tool.id,
2572
+ name: tool.name,
2573
+ description: tool.description,
2574
+ type: tool.type || "tool",
2575
+ inputSchema: tool.inputSchema ? zerialize(tool.inputSchema) : null,
2576
+ outputSchema: tool.outputSchema ? zerialize(tool.outputSchema) : null
2577
+ })));
2578
+ });
2579
+ app.get("/tools/:id", async (req, res) => {
2580
+ const id = req.params.id;
2581
+ if (!id) {
2582
+ res.status(400).json({
2583
+ message: "Missing id in request."
2584
+ });
2585
+ return;
2586
+ }
2587
+ const tool = tools.find((tool2) => tool2.id === id);
2588
+ if (!tool) {
2589
+ res.status(400).json({
2590
+ message: "Tool not found."
2591
+ });
2592
+ return;
2593
+ }
2594
+ res.status(200).json(tool);
2595
+ });
2596
+ const deleteItem = async ({
2597
+ id,
2598
+ external_id,
2599
+ contextId
2600
+ }) => {
2601
+ if (!contextId) {
2602
+ throw new Error("Missing context in request.");
2603
+ }
2604
+ if (!id && !external_id) {
2605
+ throw new Error("Missing id or external_id in request.");
2606
+ }
2607
+ const { db: db2 } = await postgresClient();
2608
+ const context = contexts.find((context2) => context2.id === contextId);
2609
+ if (!context) {
2610
+ throw new Error("Context not found in registry.");
2611
+ }
2612
+ const exists = await context.tableExists();
2613
+ if (!exists) {
2614
+ throw new Error("Table with name " + context.getTableName() + " does not exist.");
2615
+ }
2616
+ const mutation = db2.from(context.getTableName()).delete().returning("id");
2617
+ if (id) {
2618
+ mutation.where({ id });
2619
+ }
2620
+ if (external_id) {
2621
+ mutation.where({ external_id });
2622
+ }
2623
+ const result = await mutation;
2624
+ return result;
2625
+ };
2626
+ app.delete("/items/:context/:id", async (req, res) => {
2627
+ if (!req.params.context) {
2628
+ res.status(400).json({
2629
+ message: "Missing context in request."
2630
+ });
2631
+ return;
2632
+ }
2633
+ const result = await deleteItem({
2634
+ id: req.params.id,
2635
+ contextId: req.params.context
2636
+ });
2637
+ res.status(200).json(result);
2638
+ });
2639
+ app.delete("/items/:context/external/:id", async (req, res) => {
2640
+ if (!req.params.context) {
2641
+ res.status(400).json({
2642
+ message: "Missing context in request."
2643
+ });
2644
+ return;
2645
+ }
2646
+ const result = await deleteItem({
2647
+ external_id: req.params.id,
2648
+ contextId: req.params.context
2649
+ });
2650
+ res.status(200).json(result);
2651
+ });
2652
+ app.get("/items/:context/:id", async (req, res) => {
2653
+ if (!req.params.context) {
2654
+ res.status(400).json({
2655
+ message: "Missing context in request."
2656
+ });
2657
+ return;
2658
+ }
2659
+ if (!req.params.id) {
2660
+ res.status(400).json({
2661
+ message: "Missing id in request."
2662
+ });
2663
+ return;
2664
+ }
2665
+ const { db: db2 } = await postgresClient();
2666
+ const context = contexts.find((context2) => context2.id === req.params.context);
2667
+ if (!context) {
2668
+ res.status(400).json({
2669
+ message: "Context not found in registry."
2670
+ });
2671
+ return;
2672
+ }
2673
+ const item = await db2.from(context.getTableName()).where({ id: req.params.id }).select("*").first();
2674
+ if (!item) {
2675
+ res.status(404).json({
2676
+ message: "Item not found."
2677
+ });
2678
+ return;
2679
+ }
2680
+ console.log("[EXULU] chunks table name.", context.getChunksTableName());
2681
+ const chunks = await db2.from(context.getChunksTableName()).where({ source: req.params.id }).select("id", "content", "source", "embedding", "chunk_index", "created_at", "updated_at");
2682
+ console.log("[EXULU] chunks", chunks);
2683
+ res.status(200).json({
2684
+ ...item,
2685
+ chunks: chunks.map((chunk) => ({
2686
+ id: chunk.id,
2687
+ content: chunk.content,
2688
+ source: chunk.source,
2689
+ index: chunk.chunk_index,
2690
+ embedding: chunk.embedding?.length > 0 ? JSON.parse(chunk.embedding)?.length : null,
2691
+ createdAt: chunk.created_at,
2692
+ updatedAt: chunk.updated_at
2693
+ }))
2694
+ });
2695
+ });
2696
+ app.post("/items/:context/:id", async (req, res) => {
2697
+ if (!req.params.context) {
2698
+ res.status(400).json({
2699
+ message: "Missing context in request."
2700
+ });
2701
+ return;
2702
+ }
2703
+ if (!req.params.id) {
2704
+ res.status(400).json({
2705
+ message: "Missing id in request."
2706
+ });
2707
+ return;
2708
+ }
2709
+ const authenticationResult = await requestValidators.authenticate(req);
2710
+ if (!authenticationResult.user?.id) {
2711
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2712
+ return;
2713
+ }
2714
+ const context = contexts.find((context2) => context2.id === req.params.context);
2715
+ if (!context) {
2716
+ res.status(400).json({
2717
+ message: "Context not found in registry."
2718
+ });
2719
+ return;
2720
+ }
2721
+ const exists = await context.tableExists();
2722
+ if (!exists) {
2723
+ await context.createItemsTable();
2724
+ }
2725
+ const result = await context.updateItem(authenticationResult.user.id, req.params.id, req.body);
2726
+ res.status(200).json({
2727
+ message: "Item updated successfully.",
2728
+ id: result
2729
+ });
2730
+ });
2731
+ app.post("/items/:context", async (req, res) => {
2732
+ try {
2733
+ if (!req.params.context) {
2734
+ res.status(400).json({
2735
+ message: "Missing context in request."
2736
+ });
2737
+ return;
2738
+ }
2739
+ const authenticationResult = await requestValidators.authenticate(req);
2740
+ if (!authenticationResult.user?.id) {
2741
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2742
+ return;
2743
+ }
2744
+ const context = contexts.find((context2) => context2.id === req.params.context);
2745
+ if (!context) {
2746
+ res.status(400).json({
2747
+ message: "Context not found in registry."
2748
+ });
2749
+ return;
2750
+ }
2751
+ const exists = await context.tableExists();
2752
+ if (!exists) {
2753
+ await context.createItemsTable();
2754
+ }
2755
+ const result = await context.insertItem(authenticationResult.user.id, req.body, !!req.body.upsert);
2756
+ res.status(200).json({
2757
+ message: "Item created successfully.",
2758
+ id: result
2759
+ });
2760
+ } catch (error) {
2761
+ res.status(500).json({
2762
+ message: error?.message || "An error occurred while creating the item."
2763
+ });
2764
+ }
2765
+ });
2766
+ app.get("/items/:context", async (req, res) => {
2767
+ if (!req.params.context) {
2768
+ res.status(400).json({
2769
+ message: "Missing context in request."
2770
+ });
2771
+ return;
2772
+ }
2773
+ let limit = req.query.limit ? parseInt(req.query.limit) : 10;
2774
+ let page = req.query.page ? parseInt(req.query.page) : 1;
2775
+ const authenticationResult = await requestValidators.authenticate(req);
2776
+ if (!authenticationResult.user?.id) {
2777
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2778
+ return;
2779
+ }
2780
+ const context = contexts.find((context2) => context2.id === req.params.context);
2781
+ if (!context) {
2782
+ res.status(400).json({
2783
+ message: "Context not found in registry."
2784
+ });
2785
+ return;
2786
+ }
2787
+ const exists = await context.tableExists();
2788
+ if (!exists) {
2789
+ await context.createItemsTable();
2790
+ }
2791
+ if (req.query.method && !Object.values(VectorMethodEnum).includes(req.query.method)) {
2792
+ res.status(400).json({
2793
+ message: "Invalid vector lookup method, must be one of: " + Object.values(VectorMethodEnum).join(", ")
2794
+ });
2795
+ return;
2796
+ }
2797
+ const result = await context.getItems({
2798
+ page,
2799
+ limit,
2800
+ archived: req.query.archived === "true",
2801
+ name: typeof req.query.name === "string" ? req.query.name : void 0,
2802
+ method: req.query.method ? req.query.method : void 0,
2803
+ query: req.query.query ? req.query.query : void 0,
2804
+ statistics: {
2805
+ label: context.name,
2806
+ trigger: "api"
2807
+ }
2808
+ });
2809
+ res.status(200).json(result);
2810
+ });
2811
+ routeLogs.push({
2812
+ route: "/items/:context",
2813
+ method: "DELETE",
2814
+ note: `Delete all embeddings for a context.`
2815
+ });
2816
+ app.delete(`items/:context`, async (req, res) => {
2817
+ if (!req.params.context) {
2818
+ res.status(400).json({
2819
+ message: "Missing context in request."
2820
+ });
2821
+ return;
2822
+ }
2823
+ const context = contexts.find((context2) => context2.id === req.params.context);
2824
+ if (!context) {
2825
+ res.status(400).json({
2826
+ message: "Context not found in registry."
2827
+ });
2828
+ return;
2829
+ }
2830
+ const authenticationResult = await requestValidators.authenticate(req);
2831
+ if (!authenticationResult.user?.id) {
2832
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2833
+ return;
2834
+ }
2835
+ await context.deleteAll();
2836
+ res.status(200).json({
2837
+ message: "All embeddings deleted."
2838
+ });
2839
+ });
2840
+ routeLogs.push({
2841
+ route: `/items/:context/:id`,
2842
+ method: "DELETE",
2843
+ note: `Delete specific embedding for a context.`
2844
+ });
2845
+ app.delete(`items/:context/:id`, async (req, res) => {
2846
+ const id = req.params.id;
2847
+ if (!req.params.context) {
2848
+ res.status(400).json({
2849
+ message: "Missing context in request."
2850
+ });
2851
+ return;
2852
+ }
2853
+ const context = contexts.find((context2) => context2.id === req.params.context);
2854
+ if (!context) {
2855
+ res.status(400).json({
2856
+ message: "Context not found in registry."
2857
+ });
2858
+ return;
2859
+ }
2860
+ if (!id) {
2861
+ res.status(400).json({
2862
+ message: "Missing id in request."
2863
+ });
2864
+ return;
2865
+ }
2866
+ const authenticationResult = await requestValidators.authenticate(req);
2867
+ if (!authenticationResult.user?.id) {
2868
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2869
+ return;
2870
+ }
2871
+ await context.deleteOne(id);
2872
+ res.status(200).json({
2873
+ message: "Embedding deleted."
2874
+ });
2875
+ });
2876
+ app.post("/statistics/timeseries", async (req, res) => {
2877
+ const authenticationResult = await requestValidators.authenticate(req);
2878
+ if (!authenticationResult.user?.id) {
2879
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2880
+ return;
2881
+ }
2882
+ const { db: db2 } = await postgresClient();
2883
+ const type = req.body.type;
2884
+ if (!Object.values(STATISTICS_TYPE_ENUM).includes(type)) {
2885
+ res.status(400).json({
2886
+ message: "Invalid type, must be one of: " + Object.values(STATISTICS_TYPE_ENUM).join(", ")
2887
+ });
2888
+ return;
2889
+ }
2890
+ let from = new Date(req.body.from);
2891
+ let to = new Date(req.body.to);
2892
+ if (!from || !to) {
2893
+ from = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
2894
+ to = /* @__PURE__ */ new Date();
2895
+ }
2896
+ const query = db2.from("statistics").select("*");
2897
+ query.where("name", "count");
2898
+ query.andWhere("type", type);
2899
+ query.andWhere("createdAt", ">=", from);
2900
+ query.andWhere("createdAt", "<=", to);
2901
+ const results = await query;
2902
+ const dates = [];
2903
+ for (let i = 0; i < (to.getTime() - from.getTime()) / (1e3 * 60 * 60 * 24); i++) {
2904
+ dates.push(new Date(from.getTime() + i * (1e3 * 60 * 60 * 24)));
2905
+ }
2906
+ const data = dates.map((date) => {
2907
+ const result = results.find((result2) => result2.date === date);
2908
+ if (result) {
2909
+ return result;
2910
+ }
2911
+ return {
2912
+ date,
2913
+ count: 0
2914
+ };
2915
+ });
2916
+ res.status(200).json({
2917
+ data,
2918
+ filter: {
2919
+ from,
2920
+ to
2921
+ }
2922
+ });
2923
+ });
2924
+ app.post("/statistics/totals", async (req, res) => {
2925
+ const authenticationResult = await requestValidators.authenticate(req);
2926
+ if (!authenticationResult.user?.id) {
2927
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2928
+ return;
2929
+ }
2930
+ const { db: db2 } = await postgresClient();
2931
+ let from = new Date(req.body.from);
2932
+ let to = new Date(req.body.to);
2933
+ if (!from || !to) {
2934
+ from = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3);
2935
+ to = /* @__PURE__ */ new Date();
2936
+ }
2937
+ let promises2 = Object.values(STATISTICS_TYPE_ENUM).map(async (type) => {
2938
+ const result = await db2.from("statistics").where("name", "count").andWhere("type", type).andWhere("createdAt", ">=", from).andWhere("createdAt", "<=", to).sum("total as total");
2939
+ return {
2940
+ [type]: result[0]?.total || 0
2941
+ };
2942
+ });
2943
+ const results = await Promise.all(promises2);
2944
+ res.status(200).json({
2945
+ data: { ...Object.assign({}, ...results) },
2946
+ filter: {
2947
+ from,
2948
+ to
2949
+ }
2950
+ });
2951
+ });
2952
+ app.get("/contexts/statistics", async (req, res) => {
2953
+ const authenticationResult = await requestValidators.authenticate(req);
2954
+ if (!authenticationResult.user?.id) {
2955
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2956
+ return;
2957
+ }
2958
+ const { db: db2 } = await postgresClient();
2959
+ const statistics = await db2("statistics").where("name", "count").andWhere("type", "context.retrieve").sum("total as total").first();
2960
+ const response = await db2("jobs").select(db2.raw(`to_char("createdAt", 'YYYY-MM-DD') as date`)).count("* as count").where("type", "embedder").groupByRaw(`to_char("createdAt", 'YYYY-MM-DD')`).then((rows) => ({
2961
+ jobs: rows
2962
+ }));
2963
+ console.log({ response });
2964
+ let jobs = [];
2965
+ if (response[0]) {
2966
+ jobs = response[0].jobs.map((job) => ({
2967
+ date: job.id,
2968
+ count: job.count
2969
+ }));
2970
+ }
2971
+ const embeddingsCountResult = await db2("jobs").where("type", "embedder").count("* as count").first();
2972
+ res.status(200).json({
2973
+ active: contexts.filter((context) => context.active).length,
2974
+ inactive: contexts.filter((context) => !context.active).length,
2975
+ sources: contexts.reduce((acc, context) => acc + context.sources.get().length, 0),
2976
+ queries: statistics?.total || 0,
2977
+ jobs,
2978
+ totals: {
2979
+ embeddings: embeddingsCountResult?.count || 0
2980
+ }
2981
+ });
2982
+ });
2983
+ app.get(`/contexts/:id`, async (req, res) => {
2984
+ const authenticationResult = await requestValidators.authenticate(req);
2985
+ if (!authenticationResult.user?.id) {
2986
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
2987
+ return;
2988
+ }
2989
+ const id = req.params.id;
2990
+ if (!id) {
2991
+ res.status(400).json({
2992
+ message: "Missing id in request."
2993
+ });
2994
+ return;
2995
+ }
2996
+ const context = contexts.find((context2) => context2.id === id);
2997
+ if (!context) {
2998
+ res.status(400).json({
2999
+ message: "Context not found."
3000
+ });
3001
+ return;
3002
+ }
3003
+ res.status(200).json({
3004
+ ...{
3005
+ id: context.id,
3006
+ name: context.name,
3007
+ description: context.description,
3008
+ embedder: context.embedder.name,
3009
+ slug: "/contexts/" + context.id,
3010
+ active: context.active,
3011
+ fields: context.fields,
3012
+ sources: context.sources.get().map((source) => ({
3013
+ id: source.id,
3014
+ name: source.name,
3015
+ description: source.description,
3016
+ updaters: source.updaters.map((updater) => ({
3017
+ id: updater.id,
3018
+ slug: updater.slug,
3019
+ type: updater.type,
3020
+ configuration: updater.configuration
3021
+ }))
3022
+ }))
3023
+ },
3024
+ agents: []
3025
+ // todo
3026
+ });
3027
+ });
3028
+ app.get(`/items/export/:context`, async (req, res) => {
3029
+ if (!req.params.context) {
3030
+ res.status(400).json({
3031
+ message: "Missing context in request."
3032
+ });
3033
+ return;
3034
+ }
3035
+ const authenticationResult = await requestValidators.authenticate(req);
3036
+ if (!authenticationResult.user?.id) {
3037
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3038
+ return;
3039
+ }
3040
+ const context = contexts.find((context2) => context2.id === req.params.context);
3041
+ if (!context) {
3042
+ res.status(400).json({
3043
+ message: "Context not found."
3044
+ });
3045
+ return;
3046
+ }
3047
+ const items = await context.getItems({
3048
+ page: 1,
3049
+ // todo add pagination
3050
+ limit: 500
3051
+ });
3052
+ const csv = Papa.unparse(items);
3053
+ const ISOTime = (/* @__PURE__ */ new Date()).toISOString();
3054
+ res.status(200).attachment(`${context.name}-items-export-${ISOTime}.csv`).send(csv);
3055
+ });
3056
+ app.get(`/contexts`, async (req, res) => {
3057
+ console.log("contexts!!");
3058
+ const authenticationResult = await requestValidators.authenticate(req);
3059
+ if (!authenticationResult.user?.id) {
3060
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3061
+ return;
3062
+ }
3063
+ console.log("contexts", contexts?.length);
3064
+ res.status(200).json(contexts.map((context) => ({
3065
+ id: context.id,
3066
+ name: context.name,
3067
+ description: context.description,
3068
+ embedder: context.embedder.name,
3069
+ slug: "/contexts/" + context.id,
3070
+ active: context.active,
3071
+ fields: context.fields,
3072
+ sources: context.sources.get().map((source) => ({
3073
+ id: source.id,
3074
+ name: source.name,
3075
+ description: source.description,
3076
+ updaters: source.updaters.map((updater) => ({
3077
+ id: updater.id,
3078
+ slug: updater.slug,
3079
+ type: updater.type,
3080
+ configuration: updater.configuration
3081
+ }))
3082
+ }))
3083
+ })));
3084
+ });
3085
+ app.get(`/workflows`, async (req, res) => {
3086
+ const authenticationResult = await requestValidators.authenticate(req);
3087
+ if (!authenticationResult.user?.id) {
3088
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3089
+ return;
3090
+ }
3091
+ res.status(200).json(workflows.map((workflow) => ({
3092
+ id: workflow.id,
3093
+ name: workflow.name,
3094
+ slug: workflow.slug,
3095
+ enable_batch: workflow.enable_batch,
3096
+ queue: workflow.queue?.name,
3097
+ inputSchema: workflow.inputSchema ? zerialize(workflow.inputSchema) : null
3098
+ })));
3099
+ });
3100
+ app.get(`/workflows/:id`, async (req, res) => {
3101
+ const authenticationResult = await requestValidators.authenticate(req);
3102
+ if (!authenticationResult.user?.id) {
3103
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3104
+ return;
3105
+ }
3106
+ const id = req.params.id;
3107
+ if (!id) {
3108
+ res.status(400).json({
3109
+ message: "Missing id in request."
3110
+ });
3111
+ return;
3112
+ }
3113
+ const workflow = workflows.find((workflow2) => workflow2.id === id);
3114
+ if (!workflow) {
3115
+ res.status(400).json({
3116
+ message: "Workflow not found."
3117
+ });
3118
+ return;
3119
+ }
3120
+ res.status(200).json({
3121
+ ...workflow,
3122
+ queue: workflow.queue?.name,
3123
+ inputSchema: workflow.inputSchema ? zerialize(workflow.inputSchema) : null,
3124
+ workflow: void 0
3125
+ });
3126
+ });
3127
+ contexts.forEach((context) => {
3128
+ const sources = context.sources.get();
3129
+ if (!Array.isArray(sources)) {
3130
+ return;
3131
+ }
3132
+ sources.forEach((source) => {
3133
+ source.updaters.forEach((updater) => {
3134
+ if (!updater.slug) return;
3135
+ if (updater.type === "webhook" || updater.type === "manual") {
3136
+ routeLogs.push({
3137
+ route: `${updater.slug}/${updater.type}/:context`,
3138
+ method: "POST",
3139
+ note: `Webhook updater for ${context.name}`
3140
+ });
3141
+ app.post(`${updater.slug}/${updater.type}/:context`, async (req, res) => {
3142
+ const { context: id } = req.params;
3143
+ if (!id) {
3144
+ res.status(400).json({
3145
+ message: "Missing context id in request."
3146
+ });
3147
+ return;
3148
+ }
3149
+ const context2 = contexts.find((context3) => context3.id === id);
3150
+ if (!context2) {
3151
+ res.status(400).json({
3152
+ message: `Context for provided id: ${id} not found.`
3153
+ });
3154
+ return;
3155
+ }
3156
+ if (!context2.embedder.queue) {
3157
+ res.status(500).json({ detail: "No queue set for embedder." });
3158
+ return;
3159
+ }
3160
+ const authenticationResult = await requestValidators.authenticate(req);
3161
+ if (!authenticationResult.user?.id) {
3162
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3163
+ return;
3164
+ }
3165
+ const requestValidationResult = requestValidators.embedders(req, updater.configuration);
3166
+ if (requestValidationResult.error) {
3167
+ res.status(requestValidationResult.code || 500).json({ detail: `${requestValidationResult.message}` });
3168
+ return;
3169
+ }
3170
+ const documents = await updater.fn(req.body.configuration);
3171
+ const batches = [];
3172
+ for (let i = 0; i < documents.length; i += context2.embedder.batchSize) {
3173
+ batches.push(documents.slice(i, i + context2.embedder.batchSize));
3174
+ }
3175
+ let promises2 = [];
3176
+ if (batches.length > 0) {
3177
+ promises2 = batches.map((documents2) => {
3178
+ return bullmqDecorator({
3179
+ label: `Job running context '${context2.name}' with embedder '${context2.embedder.name}' for '${req.body.label}'`,
3180
+ type: "embedder",
3181
+ embedder: context2.embedder.id,
3182
+ updater: updater.id,
3183
+ context: context2.id,
3184
+ trigger: updater.type,
3185
+ source: source.id,
3186
+ inputs: req.body.inputs,
3187
+ ...updater.configuration && { configuration: req.body.configuration },
3188
+ documents: documents2,
3189
+ queue: context2.embedder.queue,
3190
+ user: authenticationResult.user.id
3191
+ });
3192
+ });
3193
+ }
3194
+ const jobs = await Promise.all(promises2);
3195
+ res.status(200).json(jobs);
3196
+ return;
3197
+ });
3198
+ }
3199
+ });
3200
+ });
3201
+ });
3202
+ agents.forEach((agent) => {
3203
+ const slug = agent.slug;
3204
+ if (!slug) return;
3205
+ routeLogs.push({
3206
+ route: slug + "/:instance",
3207
+ method: "POST",
3208
+ note: `Agent endpoint for ${agent.id}`
3209
+ });
3210
+ app.post(slug + "/:instance", async (req, res) => {
3211
+ const instance = req.params.instance;
3212
+ if (!instance) {
3213
+ res.status(400).json({
3214
+ message: "Missing instance in request."
3215
+ });
3216
+ return;
3217
+ }
3218
+ const { db: db2 } = await postgresClient();
3219
+ const agentInstance = await db2.from("agents").where({
3220
+ id: instance
3221
+ }).first();
3222
+ if (!agentInstance) {
3223
+ res.status(400).json({
3224
+ message: "Agent instance not found."
3225
+ });
3226
+ return;
3227
+ }
3228
+ if (agent.rateLimit) {
3229
+ const limit = await rateLimiter(
3230
+ agent.rateLimit.name || agent.id,
3231
+ agent.rateLimit.rate_limit.time,
3232
+ agent.rateLimit.rate_limit.limit,
3233
+ 1
3234
+ );
3235
+ if (!limit.status) {
3236
+ res.status(429).json({
3237
+ message: "Rate limit exceeded.",
3238
+ retryAfter: limit.retryAfter
3239
+ });
3240
+ return;
3241
+ }
3242
+ }
3243
+ const stream = req.headers["stream"] || false;
3244
+ const requestValidationResult = requestValidators.agents(req);
3245
+ if (requestValidationResult.error) {
3246
+ res.status(requestValidationResult.code || 500).json({ detail: `${requestValidationResult.message}` });
3247
+ return;
3248
+ }
3249
+ const authenticationResult = await requestValidators.authenticate(req);
3250
+ if (!authenticationResult.user?.id) {
3251
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3252
+ return;
3253
+ }
3254
+ if (!!stream) {
3255
+ const chatClient = await agent.chat(agentInstance.id);
3256
+ if (!chatClient) {
3257
+ res.status(500).json({
3258
+ message: "Agent instantiation not successful."
3259
+ });
3260
+ return;
3261
+ }
3262
+ const { textStream } = await chatClient.stream(req.body.messages, {
3263
+ threadId: `${req.body.threadId}`,
3264
+ // conversation id
3265
+ resourceId: `${req.body.resourceId}`,
3266
+ // user id
3267
+ ...agent.outputSchema && { output: agent.outputSchema },
3268
+ maxRetries: 2,
3269
+ // todo make part of ExuluAgent class
3270
+ maxSteps: 5,
3271
+ // todo make part of ExuluAgent class
3272
+ onError: (error) => console.error("[EXULU] chat stream error.", error),
3273
+ onFinish: ({ response, usage }) => console.info(
3274
+ "[EXULU] chat stream finished.",
3275
+ usage
3276
+ )
3277
+ });
3278
+ for await (const delta of textStream) {
3279
+ res.write(`data: ${delta}
3280
+
3281
+ `);
3282
+ }
3283
+ res.end();
3284
+ return;
3285
+ } else {
3286
+ const response = await agent.chat.generate(req.body.messages, {
3287
+ resourceId: `${authenticationResult.user.id}`,
3288
+ output: agent.outputSchema,
3289
+ threadId: `${req.body.threadId}`,
3290
+ // conversation id
3291
+ maxRetries: 2,
3292
+ // todo make part of ExuluAgent class
3293
+ maxSteps: 5
3294
+ // todo make part of ExuluAgent class
3295
+ });
3296
+ res.status(200).json(response);
3297
+ return;
3298
+ }
3299
+ });
3300
+ });
3301
+ workflows.forEach((workflow) => {
3302
+ routeLogs.push({
3303
+ route: workflow.slug,
3304
+ method: "POST",
3305
+ note: `Execute workflow ${workflow.name}`
3306
+ });
3307
+ app.post(`${workflow.slug}`, async (req, res) => {
3308
+ if (!workflow.queue) {
3309
+ res.status(500).json({ detail: "No queue set for workflow." });
3310
+ return;
3311
+ }
3312
+ const authenticationResult = await requestValidators.authenticate(req);
3313
+ if (!authenticationResult.user?.id) {
3314
+ res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
3315
+ return;
3316
+ }
3317
+ const requestValidationResult = requestValidators.workflows(req);
3318
+ if (requestValidationResult.error) {
3319
+ res.status(requestValidationResult.code || 500).json({ detail: `${requestValidationResult.message}` });
3320
+ return;
3321
+ }
3322
+ const inputs = await preprocessInputs(req.body.inputs);
3323
+ if (workflow.queue) {
3324
+ const job = await bullmqDecorator({
3325
+ label: `Job running '${workflow.name}' for '${req.body.label}'`,
3326
+ agent: req.body.agent,
3327
+ workflow: workflow.id,
3328
+ type: "workflow",
3329
+ inputs,
3330
+ session: req.body.session,
3331
+ queue: workflow.queue,
3332
+ user: authenticationResult.user.id
3333
+ });
3334
+ res.status(200).json({
3335
+ "job": {
3336
+ "status": "waiting",
3337
+ "name": job.name,
3338
+ "queue": workflow.queue.name,
3339
+ "redisId": job.redis,
3340
+ "jobId": job.id
3341
+ },
3342
+ "output": {}
3343
+ });
3344
+ return;
3345
+ }
3346
+ const { runId, start, watch } = workflow.workflow.createRun();
3347
+ console.log("[EXULU] running workflow with inputs.", inputs);
3348
+ const output = await start({
3349
+ triggerData: {
3350
+ ...inputs,
3351
+ user: authenticationResult.user.id
3352
+ }
3353
+ });
3354
+ const failedSteps = Object.entries(output.results).filter(([_, step]) => step.status === "failed").map(([id, step]) => `${id}: ${step.error}`);
3355
+ if (failedSteps.length > 0) {
3356
+ const message = `Workflow has failed steps: ${failedSteps.join("\n - ")}`;
3357
+ throw new Error(message);
3358
+ }
3359
+ res.status(200).json({
3360
+ "job": {},
3361
+ "output": output
3362
+ });
3363
+ return;
3364
+ });
3365
+ });
3366
+ await createUppyRoutes(app);
3367
+ console.log("Routes:");
3368
+ console.table(routeLogs);
3369
+ };
3370
+ var preprocessInputs = async (data) => {
3371
+ for (const key in data) {
3372
+ if (key.includes("exulu_file_")) {
3373
+ const url = await getPresignedFileUrl(data[key]);
3374
+ const newKey = key.replace("exulu_file_", "");
3375
+ data[newKey] = url;
3376
+ delete data[key];
3377
+ } else if (Array.isArray(data[key])) {
3378
+ for (let i = 0; i < data[key].length; i++) {
3379
+ if (typeof data[key][i] === "object") {
3380
+ await preprocessInputs(data[key][i]);
3381
+ }
3382
+ }
3383
+ } else if (typeof data[key] === "object") {
3384
+ await preprocessInputs(data[key]);
3385
+ }
3386
+ }
3387
+ return data;
3388
+ };
3389
+ var getPresignedFileUrl = async (key) => {
3390
+ if (!process.env.NEXT_PUBLIC_UPLOAD_URL) {
3391
+ throw new Error("Missing process.env.NEXT_PUBLIC_UPLOAD_URL");
3392
+ }
3393
+ if (!process.env.INTERNAL_SECRET) {
3394
+ throw new Error("Missing process.env.NEXT_PUBLIC_UPLOAD_URL");
3395
+ }
3396
+ console.log(`[EXULU] fetching presigned url for file with key: ${key}`);
3397
+ let url = `${process.env.NEXT_PUBLIC_UPLOAD_URL}/s3/download?key=${key}`;
3398
+ const response = await fetch(url, {
3399
+ method: "GET",
3400
+ headers: {
3401
+ "Content-Type": "application/json",
3402
+ "Internal-Key": process.env.INTERNAL_SECRET
3403
+ }
3404
+ });
3405
+ const json = await response.json();
3406
+ if (!json.url) {
3407
+ throw new Error(`Could not generate presigned url for file with key: ${key}`);
3408
+ }
3409
+ console.log(`[EXULU] presigned url for file with key: ${key}, generated: ${json.url}`);
3410
+ return json.url;
3411
+ };
3412
+
3413
+ // src/registry/workers.ts
3414
+ import IORedis from "ioredis";
3415
+ import { Worker } from "bullmq";
3416
+
3417
+ // src/registry/utils.ts
3418
+ import "bullmq";
3419
+ var bullmq = {
3420
+ validate: (job) => {
3421
+ if (!job.data) {
3422
+ throw new Error(`Missing job data for job ${job.id}.`);
3423
+ }
3424
+ if (!job.data.type) {
3425
+ throw new Error(`Missing property "type" in data for job ${job.id}.`);
3426
+ }
3427
+ if (!job.data.inputs) {
3428
+ throw new Error(`Missing property "inputs" in data for job ${job.id}.`);
3429
+ }
3430
+ if (job.data.type !== "embedder" && job.data.type !== "workflow") {
3431
+ throw new Error(`Property "type" in data for job ${job.id} must be of value "embedder" or "workflow".`);
3432
+ }
3433
+ if (!job.data.workflow && !job.data.embedder) {
3434
+ throw new Error(`Property "backend" in data for job ${job.id} missing. Job data: ${JSON.stringify(job)}`);
3435
+ }
3436
+ },
3437
+ process: {
3438
+ workflow: async (job, workflow, logsDir) => {
3439
+ if (!workflow) {
3440
+ throw new Error(`Workflow function with id: ${job.data.backend} not found in registry.`);
3441
+ }
3442
+ const { runId, start, watch } = workflow.workflow.createRun();
3443
+ console.log("[EXULU] starting workflow with job inputs.", job.data.inputs);
3444
+ const logger = new ExuluLogger(job, logsDir);
3445
+ const output = await start({ triggerData: {
3446
+ ...job.data.inputs,
3447
+ redis: job.id,
3448
+ logger
3449
+ } });
3450
+ const failedSteps = Object.entries(output.results).filter(([_, step]) => step.status === "failed").map(([id, step]) => `${id}: ${step.error}`);
3451
+ if (failedSteps.length > 0) {
3452
+ const message = `Workflow has failed steps: ${failedSteps.join("\n - ")}`;
3453
+ logger.write(message, "ERROR");
3454
+ throw new Error(message);
3455
+ }
3456
+ await logger.write(`Workflow completed. ${JSON.stringify(output.results)}`, "INFO");
3457
+ return output;
3458
+ }
3459
+ }
3460
+ };
3461
+
3462
+ // src/registry/workers.ts
3463
+ import * as fs2 from "fs";
3464
+ import path2 from "path";
3465
+ var defaultLogsDir = path2.join(process.cwd(), "logs");
3466
+ var redisConnection = new IORedis({
3467
+ ...redisServer,
3468
+ maxRetriesPerRequest: null
3469
+ });
3470
+ var createWorkers = async (queues2, contexts, embedders, workflows, _logsDir) => {
3471
+ const logsDir = _logsDir || defaultLogsDir;
3472
+ const workers = queues2.map((queue) => {
3473
+ console.log(`[EXULU] creating worker for queue ${queue}.`);
3474
+ const worker = new Worker(
3475
+ `${queue}`,
3476
+ async (job) => {
3477
+ const { db: db2 } = await postgresClient();
3478
+ try {
3479
+ bullmq.validate(job);
3480
+ if (job.data.type === "embedder") {
3481
+ if (!job.data.updater) {
3482
+ throw new Error("No updater set for embedder job.");
3483
+ }
3484
+ const context = contexts.find((context2) => context2.id === job.data.context);
3485
+ if (!context) {
3486
+ throw new Error(`Context ${job.data.context} not found in the registry.`);
3487
+ }
3488
+ if (!job.data.embedder) {
3489
+ throw new Error(`No embedder set for embedder job.`);
3490
+ }
3491
+ const embedder = embedders.find((embedder2) => embedder2.id === job.data.embedder);
3492
+ if (!embedder) {
3493
+ throw new Error(`Embedder ${job.data.embedder} not found in the registry.`);
3494
+ }
3495
+ if (!job.data.source) {
3496
+ throw new Error("No source set for embedder job.");
3497
+ }
3498
+ const source = context.sources.get(job.data.source);
3499
+ if (!source) {
3500
+ throw new Error(`Source ${job.data.source} not found in the registry.`);
3501
+ }
3502
+ if (!job.data.updater) {
3503
+ throw new Error("No updater set for embedder job.");
3504
+ }
3505
+ const updater = source.updaters.find((updater2) => updater2.id === job.data.updater);
3506
+ if (!updater) {
3507
+ throw new Error(`Updater ${job.data.updater} not found in the registry.`);
3508
+ }
3509
+ if (!job.data.documents) {
3510
+ throw new Error("No input documents set for embedder job.");
3511
+ }
3512
+ if (!Array.isArray(job.data.documents)) {
3513
+ throw new Error("Input documents must be an array.");
3514
+ }
3515
+ const result = await embedder.upsert(job.data.context, job.data.documents, {
3516
+ label: context.name,
3517
+ trigger: job.data.trigger || "unknown"
3518
+ });
3519
+ const mongoRecord = await db2.from("jobs").where({ redis: job.id }).first();
3520
+ if (!mongoRecord) {
3521
+ throw new Error("Job not found in the database.");
3522
+ }
3523
+ const finishedAt = /* @__PURE__ */ new Date();
3524
+ const duration = (finishedAt.getTime() - new Date(mongoRecord.createdAt).getTime()) / 1e3;
3525
+ await db2.from("jobs").where({ redis: job.id }).update({
3526
+ status: "completed",
3527
+ finishedAt,
3528
+ duration,
3529
+ result: JSON.stringify(result)
3530
+ });
3531
+ return result;
3532
+ }
3533
+ if (job.data.type === "workflow") {
3534
+ const workflow = workflows.find((workflow2) => workflow2.id === job.data.workflow);
3535
+ if (!workflow) {
3536
+ throw new Error(`Workflow ${job.data.workflow} not found in the registry.`);
3537
+ }
3538
+ const result = await bullmq.process.workflow(job, workflow, logsDir);
3539
+ const mongoRecord = await db2.from("jobs").where({ redis: job.id }).first();
3540
+ if (!mongoRecord) {
3541
+ throw new Error("Job not found in the database.");
3542
+ }
3543
+ const finishedAt = /* @__PURE__ */ new Date();
3544
+ const duration = (finishedAt.getTime() - new Date(mongoRecord.createdAt).getTime()) / 1e3;
3545
+ await db2.from("jobs").where({ redis: job.id }).update({
3546
+ status: "completed",
3547
+ finishedAt,
3548
+ duration,
3549
+ result: JSON.stringify(result)
3550
+ });
3551
+ return result;
3552
+ }
3553
+ } catch (error) {
3554
+ await db2.from("jobs").where({ redis: job.id }).update({
3555
+ status: "failed",
3556
+ finishedAt: /* @__PURE__ */ new Date(),
3557
+ error: error instanceof Error ? error.message : String(error)
3558
+ });
3559
+ throw new Error(error instanceof Error ? error.message : String(error));
3560
+ }
3561
+ },
3562
+ { connection: redisConnection }
3563
+ );
3564
+ worker.on("completed", (job, returnvalue) => {
3565
+ console.log(`[EXULU] completed job ${job.id}.`, returnvalue);
3566
+ });
3567
+ worker.on("failed", (job, error, prev) => {
3568
+ if (job?.id) {
3569
+ console.error(`[EXULU] failed job ${job.id}.`);
3570
+ }
3571
+ console.error(`[EXULU] job error.`, error);
3572
+ });
3573
+ worker.on("progress", (job, progress) => {
3574
+ console.log(`[EXULU] job progress ${job.id}.`, progress);
3575
+ });
3576
+ return worker;
3577
+ });
3578
+ const logsCleaner = createLogsCleanerWorker(logsDir);
3579
+ workers.push(logsCleaner);
3580
+ return workers;
3581
+ };
3582
+ var createLogsCleanerWorker = (logsDir) => {
3583
+ const logsCleaner = new Worker(
3584
+ global_queues.logs_cleaner,
3585
+ async (job) => {
3586
+ console.log(`[EXULU] recurring job ${job.id}.`);
3587
+ const folder = fs2.readdirSync(logsDir);
3588
+ const files = folder.filter((file) => file.endsWith(".log"));
3589
+ const now = /* @__PURE__ */ new Date();
3590
+ const daysToKeep = job.data.ttld;
3591
+ const dateToKeep = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1e3);
3592
+ files.forEach((file) => {
3593
+ const filePath = path2.join(logsDir, file);
3594
+ const fileStats = fs2.statSync(filePath);
3595
+ if (fileStats.mtime < dateToKeep) {
3596
+ fs2.unlinkSync(filePath);
3597
+ }
3598
+ });
3599
+ },
3600
+ { connection: redisConnection }
3601
+ );
3602
+ logsCleaner.on("completed", (job, returnvalue) => {
3603
+ console.log(`[EXULU] completed logs cleaner ${job.id}.`, returnvalue);
3604
+ });
3605
+ logsCleaner.on("failed", (job, error, prev) => {
3606
+ if (job?.id) {
3607
+ console.error(`[EXULU] failed logs cleaner ${job.id}.`);
3608
+ }
3609
+ console.error(`[EXULU] job error logs cleaner.`, error);
3610
+ });
3611
+ return logsCleaner;
3612
+ };
3613
+
3614
+ // src/registry/index.ts
3615
+ var ExuluApp = class {
3616
+ _agents;
3617
+ _workflows;
3618
+ _config;
3619
+ _embedders;
3620
+ _queues = [];
3621
+ _contexts;
3622
+ _tools;
3623
+ constructor({ contexts, embedders, agents, workflows, config, tools }) {
3624
+ this._embedders = embedders ?? [];
3625
+ this._workflows = workflows ?? [];
3626
+ this._contexts = contexts ?? {};
3627
+ this._agents = agents ?? [];
3628
+ this._config = config;
3629
+ this._tools = tools ?? [];
3630
+ const queues2 = [
3631
+ ...embedders?.length ? embedders.map((agent) => agent.queue?.name || null) : [],
3632
+ ...workflows?.length ? workflows.map((workflow) => workflow.queue?.name || null) : []
3633
+ ];
3634
+ this._queues = [...new Set(queues2.filter((o) => !!o))];
3635
+ }
3636
+ embedder(id) {
3637
+ return this._embedders.find((x) => x.id === id);
3638
+ }
3639
+ tool(id) {
3640
+ return this._tools.find((x) => x.id === id);
3641
+ }
3642
+ tools() {
3643
+ return this._tools;
3644
+ }
3645
+ context(id) {
3646
+ return Object.values(this._contexts ?? {}).find((x) => x.id === id);
3647
+ }
3648
+ agent(id) {
3649
+ return this._agents.find((x) => x.id === id);
3650
+ }
3651
+ workflow(id) {
3652
+ return this._workflows.find((x) => x.id === id);
3653
+ }
3654
+ get embedders() {
3655
+ return this._embedders;
3656
+ }
3657
+ get contexts() {
3658
+ return Object.values(this._contexts ?? {});
3659
+ }
3660
+ get workflows() {
3661
+ return this._workflows;
3662
+ }
3663
+ get agents() {
3664
+ return this._agents;
3665
+ }
3666
+ bullmq = {
3667
+ workers: {
3668
+ create: async () => {
3669
+ return await createWorkers(
3670
+ this._queues,
3671
+ Object.values(this._contexts ?? {}),
3672
+ this._embedders,
3673
+ this._workflows,
3674
+ this._config.workers?.logsDir
3675
+ );
3676
+ }
3677
+ }
3678
+ };
3679
+ server = {
3680
+ express: {
3681
+ init: async (app) => {
3682
+ return await createExpressRoutes(
3683
+ app,
3684
+ this._agents,
3685
+ this._embedders,
3686
+ this._tools,
3687
+ this._workflows,
3688
+ Object.values(this._contexts ?? {})
3689
+ );
3690
+ }
3691
+ }
3692
+ };
3693
+ };
3694
+
3695
+ // src/index.ts
3696
+ var ExuluJobs = {
3697
+ redis: redisClient,
3698
+ jobs: {
3699
+ validate: validateJob
3700
+ }
3701
+ };
3702
+ var ExuluDatabase = {
3703
+ init: async () => {
3704
+ await execute();
3705
+ }
3706
+ };
3707
+ export {
3708
+ STATISTICS_TYPE_ENUM as EXULU_STATISTICS_TYPE_ENUM,
3709
+ ExuluAgent,
3710
+ ExuluApp,
3711
+ authentication as ExuluAuthentication,
3712
+ ExuluContext,
3713
+ ExuluDatabase,
3714
+ ExuluEmbedder,
3715
+ ExuluJobs,
3716
+ queues as ExuluQueues,
3717
+ ExuluSource,
3718
+ ExuluTool,
3719
+ ExuluWorkflow,
3720
+ ExuluZodFileType
3721
+ };