@exulu/backend 1.31.1 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- ## [1.31.1](https://github.com/Qventu/exulu-backend/compare/v1.31.0...v1.31.1) (2025-11-10)
1
+ # [1.32.0](https://github.com/Qventu/exulu-backend/compare/v1.31.2...v1.32.0) (2025-11-11)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * redis auth url ([bbf51d6](https://github.com/Qventu/exulu-backend/commit/bbf51d63be8dbbc88e8989633207db4b147e3da6))
6
+ * implement scheduled data sources for contexts ([d936754](https://github.com/Qventu/exulu-backend/commit/d9367545fc3dc35b9c2d2ad0820fc7d2fddd4666))
package/dist/index.cjs CHANGED
@@ -67,6 +67,7 @@ var redisServer = {
67
67
  // src/redis/client.ts
68
68
  var client = {};
69
69
  async function redisClient() {
70
+ console.log("[EXULU] redisServer:", redisServer);
70
71
  if (!redisServer.host || !redisServer.port) {
71
72
  return { client: null };
72
73
  }
@@ -2333,7 +2334,7 @@ function createMutations(table, agents, contexts, tools, config) {
2333
2334
  if (!item) {
2334
2335
  throw new Error("Item not found, or your user does not have access to it.");
2335
2336
  }
2336
- const { job, result } = await exists.process(
2337
+ const { job, result } = await exists.processField(
2337
2338
  "api",
2338
2339
  context.user.id,
2339
2340
  context.user.role?.id,
@@ -5379,10 +5380,12 @@ var ExuluContext = class {
5379
5380
  resultReranker;
5380
5381
  // todo typings
5381
5382
  configuration;
5382
- constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration }) {
5383
+ sources = [];
5384
+ constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources }) {
5383
5385
  this.id = id;
5384
5386
  this.name = name;
5385
5387
  this.fields = fields || [];
5388
+ this.sources = sources || [];
5386
5389
  this.configuration = configuration || {
5387
5390
  calculateVectors: "manual",
5388
5391
  language: "english",
@@ -5395,7 +5398,7 @@ var ExuluContext = class {
5395
5398
  this.queryRewriter = queryRewriter;
5396
5399
  this.resultReranker = resultReranker;
5397
5400
  }
5398
- process = async (trigger, user, role, item, config) => {
5401
+ processField = async (trigger, user, role, item, config) => {
5399
5402
  console.log("[EXULU] processing field", item.field, " in context", this.id);
5400
5403
  console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
5401
5404
  const field = this.fields.find((field2) => field2.name === item.field?.replace("_s3key", ""));
@@ -5457,6 +5460,9 @@ var ExuluContext = class {
5457
5460
  results: []
5458
5461
  };
5459
5462
  };
5463
+ executeSource = async (source, inputs) => {
5464
+ return await source.execute(inputs);
5465
+ };
5460
5466
  tableExists = async () => {
5461
5467
  const { db: db3 } = await postgresClient();
5462
5468
  const tableName = getTableName(this.id);
@@ -6650,8 +6656,13 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
6650
6656
  throw new Error("No redis server configured in the environment, so cannot start worker.");
6651
6657
  }
6652
6658
  if (!redisConnection) {
6653
- redisConnection = new import_ioredis.default({
6654
- ...redisServer,
6659
+ let url = "";
6660
+ if (redisServer.username) {
6661
+ url = `redis://${redisServer.username}:${redisServer.password}@${redisServer.host}:${redisServer.port}`;
6662
+ } else {
6663
+ url = `redis://${redisServer.host}:${redisServer.port}`;
6664
+ }
6665
+ redisConnection = new import_ioredis.default(url, {
6655
6666
  enableOfflineQueue: true,
6656
6667
  retryStrategy: function(times) {
6657
6668
  return Math.max(Math.min(Math.exp(times), 2e4), 1e3);
@@ -6966,6 +6977,49 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
6966
6977
  metadata: {}
6967
6978
  };
6968
6979
  }
6980
+ if (data.type === "source") {
6981
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
6982
+ if (!data.source) {
6983
+ throw new Error(`No source id set for source job.`);
6984
+ }
6985
+ if (!data.context) {
6986
+ throw new Error(`No context id set for source job.`);
6987
+ }
6988
+ const context = contexts.find((c) => c.id === data.context);
6989
+ if (!context) {
6990
+ throw new Error(`Context ${data.context} not found in the registry.`);
6991
+ }
6992
+ const source = context.sources.find((s) => s.id === data.source);
6993
+ if (!source) {
6994
+ throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
6995
+ }
6996
+ const result = await source.execute(data.inputs);
6997
+ let jobs = [];
6998
+ let items = [];
6999
+ for (const item of result) {
7000
+ const { item: createdItem, job } = await context.createItem(item, config, data.user, data.role);
7001
+ if (job) {
7002
+ jobs.push(job);
7003
+ console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
7004
+ item: createdItem,
7005
+ job
7006
+ }));
7007
+ }
7008
+ if (createdItem.id) {
7009
+ items.push(createdItem.id);
7010
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
7011
+ item: createdItem
7012
+ }));
7013
+ }
7014
+ }
7015
+ return {
7016
+ result,
7017
+ metadata: {
7018
+ jobs,
7019
+ items
7020
+ }
7021
+ };
7022
+ }
6969
7023
  throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
6970
7024
  } catch (error) {
6971
7025
  console.error(`[EXULU] job failed.`, error instanceof Error ? error.message : String(error));
@@ -8872,6 +8926,41 @@ var ExuluApp = class {
8872
8926
  if (queues2) {
8873
8927
  filteredQueues = filteredQueues.filter((q) => queues2.includes(q.queue.name));
8874
8928
  }
8929
+ const sources = Object.values(this._contexts ?? {}).flatMap((context) => ({
8930
+ ...context.sources,
8931
+ context: context.id
8932
+ }));
8933
+ if (sources.length > 0) {
8934
+ console.log("[EXULU] Creating ContextSource schedulers for", sources.length, "sources.");
8935
+ for (const source of sources) {
8936
+ const queue = await source.config?.queue;
8937
+ if (queue) {
8938
+ if (!source.config.schedule) {
8939
+ throw new Error("Schedule is required for source when configuring a queue: " + source.name);
8940
+ }
8941
+ console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
8942
+ await queue.queue?.upsertJobScheduler(source.id, {
8943
+ pattern: source.config?.schedule
8944
+ }, {
8945
+ // default job data
8946
+ name: `${source.id}-job`,
8947
+ data: {
8948
+ source: source.id,
8949
+ context: source.context,
8950
+ type: "source"
8951
+ },
8952
+ opts: {
8953
+ backoff: {
8954
+ type: source.config.backoff?.type || "exponential",
8955
+ delay: source.config.backoff?.delay || 2e3
8956
+ },
8957
+ attempts: source.config.retries || 3,
8958
+ removeOnFail: 200
8959
+ }
8960
+ });
8961
+ }
8962
+ }
8963
+ }
8875
8964
  return await createWorkers(
8876
8965
  this._agents,
8877
8966
  filteredQueues,
package/dist/index.d.cts CHANGED
@@ -544,6 +544,21 @@ declare class ExuluStorage {
544
544
  getPresignedUrl: (key: string) => Promise<string>;
545
545
  uploadFile: (user: number, file: Buffer | Uint8Array, key: string, type: string, metadata?: Record<string, string>) => Promise<string>;
546
546
  }
547
+ type ExuluContextSource = {
548
+ id: string;
549
+ name: string;
550
+ description: string;
551
+ config: {
552
+ schedule: string;
553
+ queue: Promise<ExuluQueueConfig>;
554
+ retries?: number;
555
+ backoff?: {
556
+ type: 'exponential' | 'linear';
557
+ delay: number;
558
+ };
559
+ };
560
+ execute: (inputs: any) => Promise<Item[]>;
561
+ };
547
562
  declare class ExuluContext {
548
563
  id: string;
549
564
  name: string;
@@ -559,12 +574,14 @@ declare class ExuluContext {
559
574
  defaultRightsMode?: ExuluRightsMode;
560
575
  language?: "german" | "english";
561
576
  };
562
- constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration }: {
577
+ sources: ExuluContextSource[];
578
+ constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources }: {
563
579
  id: string;
564
580
  name: string;
565
581
  fields: ExuluContextFieldDefinition[];
566
582
  description: string;
567
583
  embedder?: ExuluEmbedder;
584
+ sources: ExuluContextSource[];
568
585
  category?: string;
569
586
  active: boolean;
570
587
  rateLimit?: RateLimiterRule;
@@ -576,13 +593,14 @@ declare class ExuluContext {
576
593
  language?: "german" | "english";
577
594
  };
578
595
  });
579
- process: (trigger: STATISTICS_LABELS, user: number, role: string, item: Item & {
596
+ processField: (trigger: STATISTICS_LABELS, user: number, role: string, item: Item & {
580
597
  field: string;
581
598
  }, config: ExuluConfig) => Promise<{
582
599
  result: string;
583
600
  job?: string;
584
601
  }>;
585
602
  deleteAll: () => Promise<VectorOperationResponse>;
603
+ executeSource: (source: ExuluContextSource, inputs: any) => Promise<Item[]>;
586
604
  tableExists: () => Promise<boolean>;
587
605
  chunksTableExists: () => Promise<boolean>;
588
606
  createAndUpsertEmbeddings: (item: Item, config: ExuluConfig, user?: number, statistics?: ExuluStatisticParams, role?: string, job?: string) => Promise<{
package/dist/index.d.ts CHANGED
@@ -544,6 +544,21 @@ declare class ExuluStorage {
544
544
  getPresignedUrl: (key: string) => Promise<string>;
545
545
  uploadFile: (user: number, file: Buffer | Uint8Array, key: string, type: string, metadata?: Record<string, string>) => Promise<string>;
546
546
  }
547
+ type ExuluContextSource = {
548
+ id: string;
549
+ name: string;
550
+ description: string;
551
+ config: {
552
+ schedule: string;
553
+ queue: Promise<ExuluQueueConfig>;
554
+ retries?: number;
555
+ backoff?: {
556
+ type: 'exponential' | 'linear';
557
+ delay: number;
558
+ };
559
+ };
560
+ execute: (inputs: any) => Promise<Item[]>;
561
+ };
547
562
  declare class ExuluContext {
548
563
  id: string;
549
564
  name: string;
@@ -559,12 +574,14 @@ declare class ExuluContext {
559
574
  defaultRightsMode?: ExuluRightsMode;
560
575
  language?: "german" | "english";
561
576
  };
562
- constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration }: {
577
+ sources: ExuluContextSource[];
578
+ constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources }: {
563
579
  id: string;
564
580
  name: string;
565
581
  fields: ExuluContextFieldDefinition[];
566
582
  description: string;
567
583
  embedder?: ExuluEmbedder;
584
+ sources: ExuluContextSource[];
568
585
  category?: string;
569
586
  active: boolean;
570
587
  rateLimit?: RateLimiterRule;
@@ -576,13 +593,14 @@ declare class ExuluContext {
576
593
  language?: "german" | "english";
577
594
  };
578
595
  });
579
- process: (trigger: STATISTICS_LABELS, user: number, role: string, item: Item & {
596
+ processField: (trigger: STATISTICS_LABELS, user: number, role: string, item: Item & {
580
597
  field: string;
581
598
  }, config: ExuluConfig) => Promise<{
582
599
  result: string;
583
600
  job?: string;
584
601
  }>;
585
602
  deleteAll: () => Promise<VectorOperationResponse>;
603
+ executeSource: (source: ExuluContextSource, inputs: any) => Promise<Item[]>;
586
604
  tableExists: () => Promise<boolean>;
587
605
  chunksTableExists: () => Promise<boolean>;
588
606
  createAndUpsertEmbeddings: (item: Item, config: ExuluConfig, user?: number, statistics?: ExuluStatisticParams, role?: string, job?: string) => Promise<{
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ var redisServer = {
15
15
  // src/redis/client.ts
16
16
  var client = {};
17
17
  async function redisClient() {
18
+ console.log("[EXULU] redisServer:", redisServer);
18
19
  if (!redisServer.host || !redisServer.port) {
19
20
  return { client: null };
20
21
  }
@@ -2281,7 +2282,7 @@ function createMutations(table, agents, contexts, tools, config) {
2281
2282
  if (!item) {
2282
2283
  throw new Error("Item not found, or your user does not have access to it.");
2283
2284
  }
2284
- const { job, result } = await exists.process(
2285
+ const { job, result } = await exists.processField(
2285
2286
  "api",
2286
2287
  context.user.id,
2287
2288
  context.user.role?.id,
@@ -5346,10 +5347,12 @@ var ExuluContext = class {
5346
5347
  resultReranker;
5347
5348
  // todo typings
5348
5349
  configuration;
5349
- constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration }) {
5350
+ sources = [];
5351
+ constructor({ id, name, description, embedder, active, rateLimit, fields, queryRewriter, resultReranker, configuration, sources }) {
5350
5352
  this.id = id;
5351
5353
  this.name = name;
5352
5354
  this.fields = fields || [];
5355
+ this.sources = sources || [];
5353
5356
  this.configuration = configuration || {
5354
5357
  calculateVectors: "manual",
5355
5358
  language: "english",
@@ -5362,7 +5365,7 @@ var ExuluContext = class {
5362
5365
  this.queryRewriter = queryRewriter;
5363
5366
  this.resultReranker = resultReranker;
5364
5367
  }
5365
- process = async (trigger, user, role, item, config) => {
5368
+ processField = async (trigger, user, role, item, config) => {
5366
5369
  console.log("[EXULU] processing field", item.field, " in context", this.id);
5367
5370
  console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
5368
5371
  const field = this.fields.find((field2) => field2.name === item.field?.replace("_s3key", ""));
@@ -5424,6 +5427,9 @@ var ExuluContext = class {
5424
5427
  results: []
5425
5428
  };
5426
5429
  };
5430
+ executeSource = async (source, inputs) => {
5431
+ return await source.execute(inputs);
5432
+ };
5427
5433
  tableExists = async () => {
5428
5434
  const { db: db3 } = await postgresClient();
5429
5435
  const tableName = getTableName(this.id);
@@ -6617,8 +6623,13 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
6617
6623
  throw new Error("No redis server configured in the environment, so cannot start worker.");
6618
6624
  }
6619
6625
  if (!redisConnection) {
6620
- redisConnection = new IORedis({
6621
- ...redisServer,
6626
+ let url = "";
6627
+ if (redisServer.username) {
6628
+ url = `redis://${redisServer.username}:${redisServer.password}@${redisServer.host}:${redisServer.port}`;
6629
+ } else {
6630
+ url = `redis://${redisServer.host}:${redisServer.port}`;
6631
+ }
6632
+ redisConnection = new IORedis(url, {
6622
6633
  enableOfflineQueue: true,
6623
6634
  retryStrategy: function(times) {
6624
6635
  return Math.max(Math.min(Math.exp(times), 2e4), 1e3);
@@ -6933,6 +6944,49 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
6933
6944
  metadata: {}
6934
6945
  };
6935
6946
  }
6947
+ if (data.type === "source") {
6948
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
6949
+ if (!data.source) {
6950
+ throw new Error(`No source id set for source job.`);
6951
+ }
6952
+ if (!data.context) {
6953
+ throw new Error(`No context id set for source job.`);
6954
+ }
6955
+ const context = contexts.find((c) => c.id === data.context);
6956
+ if (!context) {
6957
+ throw new Error(`Context ${data.context} not found in the registry.`);
6958
+ }
6959
+ const source = context.sources.find((s) => s.id === data.source);
6960
+ if (!source) {
6961
+ throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
6962
+ }
6963
+ const result = await source.execute(data.inputs);
6964
+ let jobs = [];
6965
+ let items = [];
6966
+ for (const item of result) {
6967
+ const { item: createdItem, job } = await context.createItem(item, config, data.user, data.role);
6968
+ if (job) {
6969
+ jobs.push(job);
6970
+ console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
6971
+ item: createdItem,
6972
+ job
6973
+ }));
6974
+ }
6975
+ if (createdItem.id) {
6976
+ items.push(createdItem.id);
6977
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
6978
+ item: createdItem
6979
+ }));
6980
+ }
6981
+ }
6982
+ return {
6983
+ result,
6984
+ metadata: {
6985
+ jobs,
6986
+ items
6987
+ }
6988
+ };
6989
+ }
6936
6990
  throw new Error(`Invalid job type: ${data.type} for job ${bullmqJob.name}.`);
6937
6991
  } catch (error) {
6938
6992
  console.error(`[EXULU] job failed.`, error instanceof Error ? error.message : String(error));
@@ -8839,6 +8893,41 @@ var ExuluApp = class {
8839
8893
  if (queues2) {
8840
8894
  filteredQueues = filteredQueues.filter((q) => queues2.includes(q.queue.name));
8841
8895
  }
8896
+ const sources = Object.values(this._contexts ?? {}).flatMap((context) => ({
8897
+ ...context.sources,
8898
+ context: context.id
8899
+ }));
8900
+ if (sources.length > 0) {
8901
+ console.log("[EXULU] Creating ContextSource schedulers for", sources.length, "sources.");
8902
+ for (const source of sources) {
8903
+ const queue = await source.config?.queue;
8904
+ if (queue) {
8905
+ if (!source.config.schedule) {
8906
+ throw new Error("Schedule is required for source when configuring a queue: " + source.name);
8907
+ }
8908
+ console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
8909
+ await queue.queue?.upsertJobScheduler(source.id, {
8910
+ pattern: source.config?.schedule
8911
+ }, {
8912
+ // default job data
8913
+ name: `${source.id}-job`,
8914
+ data: {
8915
+ source: source.id,
8916
+ context: source.context,
8917
+ type: "source"
8918
+ },
8919
+ opts: {
8920
+ backoff: {
8921
+ type: source.config.backoff?.type || "exponential",
8922
+ delay: source.config.backoff?.delay || 2e3
8923
+ },
8924
+ attempts: source.config.retries || 3,
8925
+ removeOnFail: 200
8926
+ }
8927
+ });
8928
+ }
8929
+ }
8930
+ }
8842
8931
  return await createWorkers(
8843
8932
  this._agents,
8844
8933
  filteredQueues,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.31.1",
4
+ "version": "1.32.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {