@exulu/backend 1.40.0 → 1.41.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/dist/index.cjs CHANGED
@@ -48,7 +48,7 @@ __export(index_exports, {
48
48
  ExuluUtils: () => ExuluUtils,
49
49
  ExuluVariables: () => ExuluVariables,
50
50
  db: () => db2,
51
- logMetadata: () => logMetadata2
51
+ logMetadata: () => logMetadata
52
52
  });
53
53
  module.exports = __toCommonJS(index_exports);
54
54
  var import_config = require("dotenv/config");
@@ -203,21 +203,37 @@ async function postgresClient() {
203
203
  database: dbName,
204
204
  password: process.env.POSTGRES_DB_PASSWORD,
205
205
  ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
206
- connectionTimeoutMillis: 1e4
206
+ connectionTimeoutMillis: 3e4,
207
+ // Increased from 10s to 30s to handle connection spikes
208
+ // PostgreSQL statement timeout (in milliseconds) - kills queries that run too long
209
+ // This prevents runaway queries from blocking connections
210
+ statement_timeout: 18e5,
211
+ // 30 minutes - should be longer than max job timeout (1200s = 20m)
212
+ // Connection idle timeout - how long pg client waits before timing out
213
+ query_timeout: 18e5
214
+ // 30 minutes
207
215
  },
208
216
  pool: {
209
- min: 2,
210
- max: 20,
211
- // Increased from 20 to handle more concurrent operations
212
- acquireTimeoutMillis: 3e4,
217
+ min: 5,
218
+ // Increased from 2 to ensure enough connections available
219
+ max: 50,
220
+ // Increased from 20 to handle more concurrent operations with processor jobs
221
+ acquireTimeoutMillis: 6e4,
222
+ // Increased from 30s to 60s to handle pool contention
213
223
  createTimeoutMillis: 3e4,
214
- idleTimeoutMillis: 3e4,
224
+ idleTimeoutMillis: 6e4,
225
+ // Increased to keep connections alive longer
215
226
  reapIntervalMillis: 1e3,
216
227
  createRetryIntervalMillis: 200,
217
228
  // Log pool events to help debug connection issues
218
229
  afterCreate: (conn, done) => {
219
230
  console.log("[EXULU] New database connection created");
220
- done(null, conn);
231
+ conn.query("SET statement_timeout = 1800000", (err) => {
232
+ if (err) {
233
+ console.error("[EXULU] Error setting statement_timeout:", err);
234
+ }
235
+ done(err, conn);
236
+ });
221
237
  }
222
238
  }
223
239
  });
@@ -1467,6 +1483,10 @@ var addCoreFields = (schema) => {
1467
1483
  field.name = field.name + "_s3key";
1468
1484
  }
1469
1485
  });
1486
+ schema.fields.push({
1487
+ name: "last_processed_at",
1488
+ type: "date"
1489
+ });
1470
1490
  if (schema.RBAC) {
1471
1491
  if (!schema.fields.some((field) => field.name === "rights_mode")) {
1472
1492
  schema.fields.push({
@@ -2349,33 +2369,38 @@ function createMutations(table, agents, contexts, tools, config) {
2349
2369
  }
2350
2370
  };
2351
2371
  if (table.type === "items") {
2352
- if (table.fields.some((field) => field.processor?.execute)) {
2353
- mutations[`${tableNameSingular}ProcessItemField`] = async (_, args, context, info) => {
2372
+ if (table.processor) {
2373
+ const contextItemProcessorMutation = async (context, items, user, role) => {
2374
+ let jobs = [];
2375
+ let results = [];
2376
+ await Promise.all(items.map(async (item) => {
2377
+ const result = await context.processField(
2378
+ "api",
2379
+ item,
2380
+ config,
2381
+ user,
2382
+ role
2383
+ );
2384
+ if (result.job) {
2385
+ jobs.push(result.job);
2386
+ }
2387
+ if (result.result) {
2388
+ results.push(result.result);
2389
+ }
2390
+ }));
2391
+ return {
2392
+ message: jobs.length > 0 ? "Processing job scheduled." : "Items processed successfully.",
2393
+ results: results.map((result) => JSON.stringify(result)),
2394
+ jobs
2395
+ };
2396
+ };
2397
+ mutations[`${tableNameSingular}ProcessItem`] = async (_, args, context, info) => {
2354
2398
  if (!context.user?.super_admin) {
2355
2399
  throw new Error("You are not authorized to process fields via API, user must be super admin.");
2356
2400
  }
2357
- const exists = contexts.find((context2) => context2.id === table.id);
2358
- if (!exists) {
2359
- throw new Error(`Context ${table.id} not found.`);
2360
- }
2361
- if (!args.field) {
2362
- throw new Error("Field argument missing, the field argument is required.");
2363
- }
2364
2401
  if (!args.item) {
2365
2402
  throw new Error("Item argument missing, the item argument is required.");
2366
2403
  }
2367
- const name = args.field?.replace("_s3key", "");
2368
- console.log("[EXULU] name", name);
2369
- console.log("[EXULU] fields", exists.fields.map((field2) => field2.name));
2370
- const field = exists.fields.find((field2) => {
2371
- return field2.name.replace("_s3key", "") === name;
2372
- });
2373
- if (!field) {
2374
- throw new Error(`Field ${name} not found in context ${exists.id}].`);
2375
- }
2376
- if (!field.processor) {
2377
- throw new Error(`Processor not set for field ${args.field} in context ${exists.id}.`);
2378
- }
2379
2404
  const { db: db3 } = context;
2380
2405
  let query = db3.from(tableNamePlural).select("*").where({ id: args.item });
2381
2406
  query = applyAccessControl(table, query, context.user);
@@ -2383,21 +2408,38 @@ function createMutations(table, agents, contexts, tools, config) {
2383
2408
  if (!item) {
2384
2409
  throw new Error("Item not found, or your user does not have access to it.");
2385
2410
  }
2386
- const { job, result } = await exists.processField(
2387
- "api",
2388
- {
2389
- ...item,
2390
- field: args.field
2391
- },
2392
- config,
2411
+ const exists = contexts.find((context2) => context2.id === table.id);
2412
+ if (!exists) {
2413
+ throw new Error(`Context ${table.id} not found.`);
2414
+ }
2415
+ return contextItemProcessorMutation(exists, [item], context.user.id, context.user.role?.id);
2416
+ };
2417
+ mutations[`${tableNameSingular}ProcessItems`] = async (_, args, context, info) => {
2418
+ if (!context.user?.super_admin) {
2419
+ throw new Error("You are not authorized to process fields via API, user must be super admin.");
2420
+ }
2421
+ const { limit = 10, filters = [], sort } = args;
2422
+ const { db: db3 } = context;
2423
+ const { items } = await paginationRequest({
2424
+ db: db3,
2425
+ limit,
2426
+ page: 0,
2427
+ filters,
2428
+ sort,
2429
+ table,
2430
+ user: context.user,
2431
+ fields: "*"
2432
+ });
2433
+ const exists = contexts.find((context2) => context2.id === table.id);
2434
+ if (!exists) {
2435
+ throw new Error(`Context ${table.id} not found.`);
2436
+ }
2437
+ return contextItemProcessorMutation(
2438
+ exists,
2439
+ items,
2393
2440
  context.user.id,
2394
2441
  context.user.role?.id
2395
2442
  );
2396
- return {
2397
- message: job ? "Processing job scheduled." : "Item processed successfully.",
2398
- result,
2399
- job
2400
- };
2401
2443
  };
2402
2444
  }
2403
2445
  mutations[`${tableNameSingular}ExecuteSource`] = async (_, args, context, info) => {
@@ -2565,7 +2607,7 @@ function createMutations(table, agents, contexts, tools, config) {
2565
2607
  }
2566
2608
  return mutations;
2567
2609
  }
2568
- var applyAccessControl = (table, query, user) => {
2610
+ var applyAccessControl = (table, query, user, field_prefix) => {
2569
2611
  const tableNamePlural = table.name.plural.toLowerCase();
2570
2612
  if (table.name.plural !== "agent_sessions" && user?.super_admin === true) {
2571
2613
  return query;
@@ -2581,18 +2623,19 @@ var applyAccessControl = (table, query, user) => {
2581
2623
  if (!hasRBAC) {
2582
2624
  return query;
2583
2625
  }
2626
+ const prefix = field_prefix ? field_prefix + "." : "";
2584
2627
  try {
2585
2628
  query = query.where(function() {
2586
- this.where("rights_mode", "public");
2587
- this.orWhere("created_by", user.id);
2629
+ this.where(`${prefix}rights_mode`, "public");
2630
+ this.orWhere(`${prefix}created_by`, user.id);
2588
2631
  this.orWhere(function() {
2589
- this.where("rights_mode", "users").whereExists(function() {
2632
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2590
2633
  this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "User").where("rbac.user_id", user.id);
2591
2634
  });
2592
2635
  });
2593
2636
  if (user.role) {
2594
2637
  this.orWhere(function() {
2595
- this.where("rights_mode", "roles").whereExists(function() {
2638
+ this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2596
2639
  this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "Role").where("rbac.role_id", user.role.id);
2597
2640
  });
2598
2641
  });
@@ -2604,9 +2647,11 @@ var applyAccessControl = (table, query, user) => {
2604
2647
  }
2605
2648
  return query;
2606
2649
  };
2607
- var converOperatorToQuery = (query, fieldName, operators, table) => {
2650
+ var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
2608
2651
  const field = table?.fields.find((f) => f.name === fieldName);
2609
2652
  const isJsonField = field?.type === "json";
2653
+ const prefix = field_prefix ? field_prefix + "." : "";
2654
+ fieldName = prefix + fieldName;
2610
2655
  if (operators.eq !== void 0) {
2611
2656
  if (isJsonField) {
2612
2657
  query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
@@ -2916,53 +2961,95 @@ var finalizeRequestedFields = async ({
2916
2961
  }
2917
2962
  const { db: db3 } = await postgresClient();
2918
2963
  const query = db3.from(getChunksTableName(context.id)).where({ source: result.id }).select("id", "content", "source", "chunk_index", "createdAt", "updatedAt");
2919
- query.select(
2920
- db3.raw("vector_dims(??) as embedding_size", [`embedding`])
2921
- );
2922
2964
  const chunks = await query;
2923
2965
  result.chunks = chunks.map((chunk) => ({
2924
- cosine_distance: 0,
2925
- fts_rank: 0,
2926
- hybrid_score: 0,
2927
- content: chunk.content,
2928
- source: chunk.source,
2966
+ chunk_content: chunk.content,
2967
+ chunk_source: chunk.source,
2929
2968
  chunk_index: chunk.chunk_index,
2930
2969
  chunk_id: chunk.id,
2931
2970
  chunk_created_at: chunk.createdAt,
2932
2971
  chunk_updated_at: chunk.updatedAt,
2933
- embedding_size: chunk.embedding_size
2972
+ item_updated_at: chunk.item_updated_at,
2973
+ item_created_at: chunk.item_created_at,
2974
+ item_id: chunk.item_id,
2975
+ item_external_id: chunk.item_external_id,
2976
+ item_name: chunk.item_name
2934
2977
  }));
2935
2978
  }
2936
2979
  }
2937
2980
  }
2938
2981
  return result;
2939
2982
  };
2940
- var applyFilters = (query, filters, table) => {
2983
+ var applyFilters = (query, filters, table, field_prefix) => {
2941
2984
  filters.forEach((filter) => {
2942
2985
  Object.entries(filter).forEach(([fieldName, operators]) => {
2943
2986
  if (operators) {
2944
2987
  if (operators.and !== void 0) {
2945
2988
  operators.and.forEach((operator) => {
2946
- query = converOperatorToQuery(query, fieldName, operator, table);
2989
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2947
2990
  });
2948
2991
  }
2949
2992
  if (operators.or !== void 0) {
2950
2993
  operators.or.forEach((operator) => {
2951
- query = converOperatorToQuery(query, fieldName, operator, table);
2994
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2952
2995
  });
2953
2996
  }
2954
- query = converOperatorToQuery(query, fieldName, operators, table);
2997
+ query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
2955
2998
  }
2956
2999
  });
2957
3000
  });
2958
3001
  return query;
2959
3002
  };
2960
- var applySorting = (query, sort) => {
3003
+ var applySorting = (query, sort, field_prefix) => {
3004
+ const prefix = field_prefix ? field_prefix + "." : "";
2961
3005
  if (sort) {
3006
+ sort.field = prefix + sort.field;
2962
3007
  query = query.orderBy(sort.field, sort.direction.toLowerCase());
2963
3008
  }
2964
3009
  return query;
2965
3010
  };
3011
+ var paginationRequest = async ({
3012
+ db: db3,
3013
+ limit,
3014
+ page,
3015
+ filters,
3016
+ sort,
3017
+ table,
3018
+ user,
3019
+ fields
3020
+ }) => {
3021
+ if (limit > 1e4) {
3022
+ throw new Error("Limit cannot be greater than 10.000.");
3023
+ }
3024
+ const tableName = table.name.plural.toLowerCase();
3025
+ let countQuery = db3(tableName);
3026
+ countQuery = applyFilters(countQuery, filters, table);
3027
+ countQuery = applyAccessControl(table, countQuery, user);
3028
+ const countResult = await countQuery.count("* as count");
3029
+ const itemCount = Number(countResult[0]?.count || 0);
3030
+ const pageCount = Math.ceil(itemCount / limit);
3031
+ const currentPage = page;
3032
+ const hasPreviousPage = currentPage > 1;
3033
+ const hasNextPage = currentPage < pageCount - 1;
3034
+ let dataQuery = db3(tableName);
3035
+ dataQuery = applyFilters(dataQuery, filters, table);
3036
+ dataQuery = applyAccessControl(table, dataQuery, user);
3037
+ dataQuery = applySorting(dataQuery, sort);
3038
+ if (page > 1) {
3039
+ dataQuery = dataQuery.offset((page - 1) * limit);
3040
+ }
3041
+ let items = await dataQuery.select(fields ? fields : "*").limit(limit);
3042
+ return {
3043
+ items,
3044
+ pageInfo: {
3045
+ pageCount,
3046
+ itemCount,
3047
+ currentPage,
3048
+ hasPreviousPage,
3049
+ hasNextPage
3050
+ }
3051
+ };
3052
+ };
2966
3053
  function createQueries(table, agents, tools, contexts) {
2967
3054
  const tableNamePlural = table.name.plural.toLowerCase();
2968
3055
  const tableNameSingular = table.name.singular.toLowerCase();
@@ -2998,38 +3085,22 @@ function createQueries(table, agents, tools, contexts) {
2998
3085
  return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
2999
3086
  },
3000
3087
  [`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
3001
- const { limit = 10, page = 0, filters = [], sort } = args;
3002
3088
  const { db: db3 } = context;
3003
- if (limit > 500) {
3004
- throw new Error("Limit cannot be greater than 500.");
3005
- }
3006
- let countQuery = db3(tableNamePlural);
3007
- countQuery = applyFilters(countQuery, filters, table);
3008
- countQuery = applyAccessControl(table, countQuery, context.user);
3009
- const countResult = await countQuery.count("* as count");
3010
- const itemCount = Number(countResult[0]?.count || 0);
3011
- const pageCount = Math.ceil(itemCount / limit);
3012
- const currentPage = page;
3013
- const hasPreviousPage = currentPage > 1;
3014
- const hasNextPage = currentPage < pageCount - 1;
3015
- let dataQuery = db3(tableNamePlural);
3016
- dataQuery = applyFilters(dataQuery, filters, table);
3017
- dataQuery = applyAccessControl(table, dataQuery, context.user);
3089
+ const { limit = 10, page = 0, filters = [], sort } = args;
3018
3090
  const requestedFields = getRequestedFields(info);
3019
- dataQuery = applySorting(dataQuery, sort);
3020
- if (page > 1) {
3021
- dataQuery = dataQuery.offset((page - 1) * limit);
3022
- }
3023
3091
  const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
3024
- let items = await dataQuery.select(sanitizedFields).limit(limit);
3092
+ const { items, pageInfo } = await paginationRequest({
3093
+ db: db3,
3094
+ limit,
3095
+ page,
3096
+ filters,
3097
+ sort,
3098
+ table,
3099
+ user: context.user,
3100
+ fields: sanitizedFields
3101
+ });
3025
3102
  return {
3026
- pageInfo: {
3027
- pageCount,
3028
- itemCount,
3029
- currentPage,
3030
- hasPreviousPage,
3031
- hasNextPage
3032
- },
3103
+ pageInfo,
3033
3104
  items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
3034
3105
  };
3035
3106
  },
@@ -3078,7 +3149,7 @@ function createQueries(table, agents, tools, contexts) {
3078
3149
  }
3079
3150
  const { limit = 10, page = 0, filters = [], sort } = args;
3080
3151
  return await vectorSearch({
3081
- limit,
3152
+ limit: limit || exists.configuration.maxRetrievalResults || 10,
3082
3153
  page,
3083
3154
  filters,
3084
3155
  sort,
@@ -3137,106 +3208,111 @@ var vectorSearch = async ({
3137
3208
  }
3138
3209
  const mainTable = getTableName(id);
3139
3210
  const chunksTable = getChunksTableName(id);
3140
- let countQuery = db3(mainTable);
3141
- countQuery = applyFilters(countQuery, filters, table);
3142
- countQuery = applyAccessControl(table, countQuery, user);
3143
- const columns = await db3(mainTable).columnInfo();
3144
- let itemsQuery = db3(mainTable).select(Object.keys(columns).map((column) => mainTable + "." + column));
3145
- itemsQuery = applyFilters(itemsQuery, filters, table);
3146
- itemsQuery = applyAccessControl(table, itemsQuery, user);
3147
- itemsQuery = applySorting(itemsQuery, sort);
3211
+ let chunksQuery = db3(chunksTable + " as chunks").select([
3212
+ "chunks.id as chunk_id",
3213
+ "chunks.source",
3214
+ "chunks.content",
3215
+ "chunks.chunk_index",
3216
+ db3.raw('chunks."createdAt" as chunk_created_at'),
3217
+ db3.raw('chunks."updatedAt" as chunk_updated_at'),
3218
+ "chunks.metadata",
3219
+ "items.id as item_id",
3220
+ "items.name as item_name",
3221
+ "items.external_id as item_external_id",
3222
+ db3.raw('items."updatedAt" as item_updated_at'),
3223
+ db3.raw('items."createdAt" as item_created_at')
3224
+ ]);
3225
+ chunksQuery.leftJoin(mainTable + " as items", function() {
3226
+ this.on("chunks.source", "=", "items.id");
3227
+ });
3228
+ chunksQuery = applyFilters(chunksQuery, filters, table, "items");
3229
+ chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
3230
+ chunksQuery = applySorting(chunksQuery, sort, "items");
3148
3231
  if (queryRewriter) {
3149
3232
  query = await queryRewriter(query);
3150
3233
  }
3151
- itemsQuery.limit(limit * 3);
3152
- itemsQuery.leftJoin(chunksTable, function() {
3153
- this.on(chunksTable + ".source", "=", mainTable + ".id");
3154
- });
3155
- itemsQuery.select(chunksTable + ".id as chunk_id");
3156
- itemsQuery.select(chunksTable + ".source");
3157
- itemsQuery.select(chunksTable + ".content");
3158
- itemsQuery.select(chunksTable + ".chunk_index");
3159
- itemsQuery.select(chunksTable + ".createdAt as chunk_created_at");
3160
- itemsQuery.select(chunksTable + ".updatedAt as chunk_updated_at");
3161
- itemsQuery.select(db3.raw("vector_dims(??) as embedding_size", [`${chunksTable}.embedding`]));
3162
- const { chunks } = await embedder.generateFromQuery(context.id, query, {
3234
+ const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
3163
3235
  label: table.name.singular,
3164
3236
  trigger
3165
3237
  }, user?.id, role);
3166
- if (!chunks?.[0]?.vector) {
3238
+ if (!queryChunks?.[0]?.vector) {
3167
3239
  throw new Error("No vector generated for query.");
3168
3240
  }
3169
- const vector = chunks[0].vector;
3241
+ const vector = queryChunks[0].vector;
3170
3242
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3171
3243
  const vectorExpr = `${vectorStr}::vector`;
3172
3244
  const language = configuration.language || "english";
3173
- let items = [];
3245
+ let resultChunks = [];
3174
3246
  switch (method) {
3175
3247
  case "tsvector":
3176
- itemsQuery.select(db3.raw(
3177
- `ts_rank(${chunksTable}.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3248
+ chunksQuery.limit(limit * 2);
3249
+ chunksQuery.select(db3.raw(
3250
+ `ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3178
3251
  [language, query]
3179
3252
  )).whereRaw(
3180
- `${chunksTable}.fts @@ websearch_to_tsquery(?, ?)`,
3253
+ `chunks.fts @@ websearch_to_tsquery(?, ?)`,
3181
3254
  [language, query]
3182
3255
  ).orderByRaw(`fts_rank DESC`);
3183
- items = await itemsQuery;
3256
+ resultChunks = await chunksQuery;
3184
3257
  break;
3185
3258
  case "cosineDistance":
3186
- default:
3187
- itemsQuery.whereNotNull(`${chunksTable}.embedding`);
3188
- itemsQuery.select(
3189
- db3.raw(`1 - (${chunksTable}.embedding <=> ${vectorExpr}) AS cosine_distance`)
3259
+ chunksQuery.limit(limit * 2);
3260
+ chunksQuery.whereNotNull(`chunks.embedding`);
3261
+ console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
3262
+ chunksQuery.select(
3263
+ db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
3190
3264
  );
3191
- itemsQuery.orderByRaw(
3192
- `${chunksTable}.embedding <=> ${vectorExpr} ASC NULLS LAST`
3265
+ chunksQuery.orderByRaw(
3266
+ `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3193
3267
  );
3194
- items = await itemsQuery;
3268
+ resultChunks = await chunksQuery;
3195
3269
  break;
3196
3270
  case "hybridSearch":
3197
- const matchCount = Math.min(limit * 5, 100);
3271
+ const matchCount = Math.min(limit * 2, 100);
3198
3272
  const fullTextWeight = 1;
3199
3273
  const semanticWeight = 1;
3200
3274
  const rrfK = 50;
3201
3275
  const hybridSQL = `
3202
3276
  WITH full_text AS (
3203
3277
  SELECT
3204
- c.id,
3205
- c.source,
3278
+ chunks.id,
3279
+ chunks.source,
3206
3280
  row_number() OVER (
3207
- ORDER BY ts_rank_cd(c.fts, websearch_to_tsquery(?, ?)) DESC
3281
+ ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
3208
3282
  ) AS rank_ix
3209
- FROM ${chunksTable} c
3210
- WHERE c.fts @@ websearch_to_tsquery(?, ?)
3283
+ FROM ${chunksTable} as chunks
3284
+ WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
3211
3285
  ORDER BY rank_ix
3212
3286
  LIMIT LEAST(?, 15) * 2
3213
3287
  ),
3214
3288
  semantic AS (
3215
3289
  SELECT
3216
- c.id,
3217
- c.source,
3290
+ chunks.id,
3291
+ chunks.source,
3218
3292
  row_number() OVER (
3219
- ORDER BY c.embedding <=> ${vectorExpr} ASC
3293
+ ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3220
3294
  ) AS rank_ix
3221
- FROM ${chunksTable} c
3222
- WHERE c.embedding IS NOT NULL
3295
+ FROM ${chunksTable} as chunks
3296
+ WHERE chunks.embedding IS NOT NULL
3223
3297
  ORDER BY rank_ix
3224
3298
  LIMIT LEAST(?, 50) * 2
3225
3299
  )
3226
3300
  SELECT
3227
- m.*,
3228
- c.id AS chunk_id,
3229
- c.source,
3230
- c.content,
3231
- c.chunk_index,
3232
- c.metadata,
3233
- c."createdAt" AS chunk_created_at,
3234
- c."updatedAt" AS chunk_updated_at,
3235
- vector_dims(c.embedding) as embedding_size,
3236
-
3301
+ items.id as item_id,
3302
+ items.name as item_name,
3303
+ items.external_id as item_external_id,
3304
+ chunks.id AS chunk_id,
3305
+ chunks.source,
3306
+ chunks.content,
3307
+ chunks.chunk_index,
3308
+ chunks.metadata,
3309
+ chunks."createdAt" as chunk_created_at,
3310
+ chunks."updatedAt" as chunk_updated_at,
3311
+ items."updatedAt" as item_updated_at,
3312
+ items."createdAt" as item_created_at,
3237
3313
  /* Per-signal scores for introspection */
3238
- ts_rank(c.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3239
- (1 - (c.embedding <=> ${vectorExpr})) AS cosine_distance,
3314
+ ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3315
+ (1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
3240
3316
 
3241
3317
  /* Hybrid RRF score */
3242
3318
  (
@@ -3248,10 +3324,10 @@ var vectorSearch = async ({
3248
3324
  FROM full_text ft
3249
3325
  FULL OUTER JOIN semantic se
3250
3326
  ON ft.id = se.id
3251
- JOIN ${chunksTable} c
3252
- ON COALESCE(ft.id, se.id) = c.id
3253
- JOIN ${mainTable} m
3254
- ON m.id = c.source
3327
+ JOIN ${chunksTable} as chunks
3328
+ ON COALESCE(ft.id, se.id) = chunks.id
3329
+ JOIN ${mainTable} as items
3330
+ ON items.id = chunks.source
3255
3331
  ORDER BY hybrid_score DESC
3256
3332
  LIMIT LEAST(?, 50)
3257
3333
  OFFSET 0
@@ -3277,86 +3353,57 @@ var vectorSearch = async ({
3277
3353
  matchCount
3278
3354
  // final limit
3279
3355
  ];
3280
- items = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3281
- }
3282
- console.log("[EXULU] Vector search results:", items?.length);
3283
- const seenSources = /* @__PURE__ */ new Map();
3284
- items = items.reduce((acc, item) => {
3285
- if (!seenSources.has(item.source)) {
3286
- seenSources.set(item.source, {
3287
- ...Object.fromEntries(
3288
- Object.keys(item).filter(
3289
- (key) => key !== "cosine_distance" && // kept per chunk below
3290
- key !== "fts_rank" && // kept per chunk below
3291
- key !== "hybrid_score" && // we will compute per item below
3292
- key !== "content" && key !== "source" && key !== "chunk_index" && key !== "chunk_id" && key !== "chunk_created_at" && key !== "chunk_updated_at" && key !== "embedding_size"
3293
- ).map((key) => [key, item[key]])
3294
- ),
3295
- chunks: [{
3296
- content: item.content,
3297
- chunk_index: item.chunk_index,
3298
- chunk_id: item.chunk_id,
3299
- source: item.source,
3300
- metadata: item.metadata,
3301
- chunk_created_at: item.chunk_created_at,
3302
- chunk_updated_at: item.chunk_updated_at,
3303
- embedding_size: item.embedding_size,
3304
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3305
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3306
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3307
- }]
3308
- });
3309
- acc.push(seenSources.get(item.source));
3310
- } else {
3311
- seenSources.get(item.source).chunks.push({
3312
- content: item.content,
3313
- chunk_index: item.chunk_index,
3314
- chunk_id: item.chunk_id,
3315
- chunk_created_at: item.chunk_created_at,
3316
- embedding_size: item.embedding_size,
3317
- metadata: item.metadata,
3318
- source: item.source,
3319
- chunk_updated_at: item.chunk_updated_at,
3320
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3321
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3322
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3323
- });
3324
- }
3325
- return acc;
3326
- }, []);
3327
- console.log("[EXULU] Vector search results after deduplication:", items?.length);
3328
- items.forEach((item) => {
3329
- if (!item.chunks?.length) {
3330
- return;
3331
- }
3332
- if (method === "tsvector") {
3333
- const ranks = item.chunks.map((c) => typeof c.fts_rank === "number" ? c.fts_rank : 0);
3334
- const total = ranks.reduce((a, b) => a + b, 0);
3335
- const average = ranks.length ? total / ranks.length : 0;
3336
- item.averageRelevance = average;
3337
- item.totalRelevance = total;
3338
- } else if (method === "cosineDistance") {
3339
- let methodProperty = "cosine_distance";
3340
- const average = item.chunks.reduce((acc, item2) => {
3341
- return acc + item2[methodProperty];
3342
- }, 0) / item.chunks.length;
3343
- const total = item.chunks.reduce((acc, item2) => {
3344
- return acc + item2[methodProperty];
3345
- }, 0);
3346
- item.averageRelevance = average;
3347
- item.totalRelevance = total;
3348
- } else if (method === "hybridSearch") {
3349
- const scores = item.chunks.map((c) => typeof c.hybrid_score === "number" ? c.hybrid_score * 10 + 1 : 0);
3350
- const total = scores.reduce((a, b) => a + b, 0);
3351
- const average = scores.length ? total / scores.length : 0;
3352
- item.averageRelevance = average;
3353
- item.totalRelevance = total;
3356
+ resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3357
+ }
3358
+ console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
3359
+ resultChunks = resultChunks.map((chunk) => ({
3360
+ chunk_content: chunk.content,
3361
+ chunk_index: chunk.chunk_index,
3362
+ chunk_id: chunk.chunk_id,
3363
+ chunk_source: chunk.source,
3364
+ chunk_metadata: chunk.metadata,
3365
+ chunk_created_at: chunk.chunk_created_at,
3366
+ chunk_updated_at: chunk.chunk_updated_at,
3367
+ item_updated_at: chunk.item_updated_at,
3368
+ item_created_at: chunk.item_created_at,
3369
+ item_id: chunk.item_id,
3370
+ item_external_id: chunk.item_external_id,
3371
+ item_name: chunk.item_name,
3372
+ context: {
3373
+ name: table.name.singular,
3374
+ id: table.id || ""
3375
+ },
3376
+ ...method === "cosineDistance" && { chunk_cosine_distance: chunk.cosine_distance },
3377
+ ...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
3378
+ ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score }
3379
+ }));
3380
+ if (resultChunks.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3381
+ const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
3382
+ const topScore = resultChunks[0][scoreKey];
3383
+ const bottomScore = resultChunks[resultChunks.length - 1][scoreKey];
3384
+ const medianScore = resultChunks[Math.floor(resultChunks.length / 2)][scoreKey];
3385
+ console.log("[EXULU] Score distribution:", {
3386
+ method,
3387
+ count: resultChunks.length,
3388
+ topScore: topScore?.toFixed(4),
3389
+ bottomScore: bottomScore?.toFixed(4),
3390
+ medianScore: medianScore?.toFixed(4)
3391
+ });
3392
+ const adaptiveThreshold = topScore * 0.7;
3393
+ const beforeFilterCount = resultChunks.length;
3394
+ resultChunks = resultChunks.filter((chunk) => {
3395
+ const score = chunk[scoreKey];
3396
+ return score !== void 0 && score >= adaptiveThreshold;
3397
+ });
3398
+ const filteredCount = beforeFilterCount - resultChunks.length;
3399
+ if (filteredCount > 0) {
3400
+ console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
3354
3401
  }
3355
- });
3402
+ }
3356
3403
  if (resultReranker && query) {
3357
- items = await resultReranker(items);
3404
+ resultChunks = await resultReranker(resultChunks);
3358
3405
  }
3359
- console.log("[EXULU] Vector search results after slicing:", items?.length);
3406
+ resultChunks = resultChunks.slice(0, limit);
3360
3407
  await updateStatistic({
3361
3408
  name: "count",
3362
3409
  label: table.name.singular,
@@ -3374,7 +3421,7 @@ var vectorSearch = async ({
3374
3421
  id: table.id || "",
3375
3422
  embedder: embedder.name
3376
3423
  },
3377
- items
3424
+ chunks: resultChunks
3378
3425
  };
3379
3426
  };
3380
3427
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3404,10 +3451,10 @@ var contextToTableDefinition = (context) => {
3404
3451
  plural: tableName?.endsWith("s") ? tableName : tableName + "s"
3405
3452
  },
3406
3453
  RBAC: true,
3454
+ processor: context.processor,
3407
3455
  fields: context.fields.map((field) => ({
3408
3456
  name: sanitizeName(field.name),
3409
3457
  type: field.type,
3410
- processor: field.processor,
3411
3458
  required: field.required,
3412
3459
  default: field.default,
3413
3460
  index: field.index,
@@ -3537,7 +3584,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3537
3584
  const tableNamePlural = table.name.plural.toLowerCase();
3538
3585
  const tableNameSingular = table.name.singular.toLowerCase();
3539
3586
  const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
3540
- const processorFields = table.fields.filter((field) => field.processor?.execute);
3541
3587
  typeDefs += `
3542
3588
  ${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
3543
3589
 
@@ -3564,9 +3610,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3564
3610
  ${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
3565
3611
  ${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
3566
3612
  `;
3567
- if (processorFields?.length > 0) {
3613
+ if (table.processor) {
3568
3614
  mutationDefs += `
3569
- ${tableNameSingular}ProcessItemField(item: ID!, field: ${tableNameSingular}ProcessorFieldEnum!): ${tableNameSingular}ProcessItemFieldReturnPayload
3615
+ ${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
3616
+ ${tableNameSingular}ProcessItems(limit: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}ProcessItemFieldReturnPayload
3570
3617
  `;
3571
3618
  }
3572
3619
  modelDefs += `
@@ -3584,8 +3631,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3584
3631
 
3585
3632
  type ${tableNameSingular}ProcessItemFieldReturnPayload {
3586
3633
  message: String!
3587
- result: String!
3588
- job: String
3634
+ results: [String]
3635
+ jobs: [String]
3589
3636
  }
3590
3637
 
3591
3638
  type ${tableNameSingular}DeleteChunksReturnPayload {
@@ -3600,20 +3647,31 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3600
3647
  tsvector
3601
3648
  }
3602
3649
 
3603
- ${processorFields.length > 0 ? `
3604
- enum ${tableNameSingular}ProcessorFieldEnum {
3605
- ${processorFields.map((field) => field.name).join("\n")}
3606
- }
3607
- ` : ""}
3608
-
3609
-
3610
- type ${tableNameSingular}VectorSearchResult {
3611
- items: [${tableNameSingular}]!
3650
+ type ${tableNameSingular}VectorSearchResult {
3651
+ chunks: [${tableNameSingular}VectorSearchChunk!]!
3612
3652
  context: VectoSearchResultContext!
3613
3653
  filters: JSON!
3614
3654
  query: String!
3615
3655
  method: VectorMethodEnum!
3616
3656
  }
3657
+
3658
+ type ${tableNameSingular}VectorSearchChunk {
3659
+ chunk_content: String
3660
+ chunk_index: Int
3661
+ chunk_id: String
3662
+ chunk_source: String
3663
+ chunk_metadata: JSON
3664
+ chunk_created_at: Date
3665
+ chunk_updated_at: Date
3666
+ item_updated_at: Date
3667
+ item_created_at: Date
3668
+ item_id: String!
3669
+ item_external_id: String
3670
+ item_name: String!
3671
+ chunk_cosine_distance: Float
3672
+ chunk_fts_rank: Float
3673
+ chunk_hybrid_score: Float
3674
+ }
3617
3675
 
3618
3676
  type VectoSearchResultContext {
3619
3677
  name: String!
@@ -3722,6 +3780,7 @@ type PageInfo {
3722
3780
  worker: config2.concurrency?.worker || void 0,
3723
3781
  queue: config2.concurrency?.queue || void 0
3724
3782
  },
3783
+ timeoutInSeconds: config2.timeoutInSeconds,
3725
3784
  ratelimit: config2.ratelimit,
3726
3785
  isMaxed: await config2.queue.isMaxed(),
3727
3786
  isPaused: await config2.queue.isPaused(),
@@ -3935,25 +3994,21 @@ type PageInfo {
3935
3994
  };
3936
3995
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3937
3996
  const data = await Promise.all(contexts.map(async (context2) => {
3938
- let processors = await Promise.all(context2.fields.map(async (field) => {
3939
- if (field.processor) {
3940
- let queueName = void 0;
3941
- if (field.processor?.config?.queue) {
3942
- const config2 = await field.processor?.config?.queue;
3943
- queueName = config2?.queue?.name || void 0;
3944
- }
3945
- return {
3946
- field: field.name,
3947
- description: field.processor?.description,
3948
- queue: queueName,
3949
- trigger: field.processor?.config?.trigger,
3950
- timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 600,
3951
- generateEmbeddings: field.processor?.config?.generateEmbeddings || false
3952
- };
3953
- }
3954
- return null;
3955
- }));
3956
- processors = processors.filter((processor) => processor !== null);
3997
+ let processor = null;
3998
+ if (context2.processor) {
3999
+ processor = await new Promise(async (resolve, reject) => {
4000
+ const config2 = await context2.processor?.config;
4001
+ const queue = await config2?.queue;
4002
+ resolve({
4003
+ name: context2.processor.name,
4004
+ description: context2.processor.description,
4005
+ queue: queue?.queue?.name || void 0,
4006
+ trigger: context2.processor?.config?.trigger || "manual",
4007
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
4008
+ generateEmbeddings: context2.processor?.config?.generateEmbeddings || false
4009
+ });
4010
+ });
4011
+ }
3957
4012
  const sources = await Promise.all(context2.sources.map(async (source) => {
3958
4013
  let queueName = void 0;
3959
4014
  if (source.config) {
@@ -3985,7 +4040,7 @@ type PageInfo {
3985
4040
  slug: "/contexts/" + context2.id,
3986
4041
  active: context2.active,
3987
4042
  sources,
3988
- processors,
4043
+ processor,
3989
4044
  fields: context2.fields.map((field) => {
3990
4045
  return {
3991
4046
  ...field,
@@ -4011,25 +4066,21 @@ type PageInfo {
4011
4066
  if (!data) {
4012
4067
  return null;
4013
4068
  }
4014
- let processors = await Promise.all(data.fields.map(async (field) => {
4015
- if (field.processor) {
4016
- let queueName = void 0;
4017
- if (field.processor?.config?.queue) {
4018
- const config2 = await field.processor?.config?.queue;
4019
- queueName = config2?.queue?.name || void 0;
4020
- }
4021
- return {
4022
- field: field.name,
4023
- description: field.processor?.description,
4024
- queue: queueName,
4025
- trigger: field.processor?.config?.trigger,
4026
- timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 600,
4027
- generateEmbeddings: field.processor?.config?.generateEmbeddings || false
4028
- };
4029
- }
4030
- return null;
4031
- }));
4032
- processors = processors.filter((processor) => processor !== null);
4069
+ let processor = null;
4070
+ if (data.processor) {
4071
+ processor = await new Promise(async (resolve, reject) => {
4072
+ const config2 = await data.processor?.config;
4073
+ const queue = await config2?.queue;
4074
+ resolve({
4075
+ name: data.processor.name,
4076
+ description: data.processor.description,
4077
+ queue: queue?.queue?.name || void 0,
4078
+ trigger: data.processor?.config?.trigger || "manual",
4079
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
4080
+ generateEmbeddings: data.processor?.config?.generateEmbeddings || false
4081
+ });
4082
+ });
4083
+ }
4033
4084
  const sources = await Promise.all(data.sources.map(async (source) => {
4034
4085
  let queueName = void 0;
4035
4086
  if (source.config) {
@@ -4066,39 +4117,18 @@ type PageInfo {
4066
4117
  slug: "/contexts/" + data.id,
4067
4118
  active: data.active,
4068
4119
  sources,
4069
- processors,
4120
+ processor,
4070
4121
  fields: await Promise.all(data.fields.map(async (field) => {
4071
4122
  const label = field.name?.replace("_s3key", "");
4072
4123
  if (field.type === "file" && !field.name.endsWith("_s3key")) {
4073
4124
  field.name = field.name + "_s3key";
4074
4125
  }
4075
- let queue = null;
4076
- if (field.processor?.config?.queue) {
4077
- queue = await field.processor.config.queue;
4078
- }
4079
4126
  return {
4080
4127
  ...field,
4081
4128
  name: sanitizeName(field.name),
4082
4129
  ...field.type === "file" ? {
4083
4130
  allowedFileTypes: field.allowedFileTypes
4084
4131
  } : {},
4085
- ...field.processor ? {
4086
- processor: {
4087
- description: field.processor?.description,
4088
- config: {
4089
- trigger: field.processor?.config?.trigger,
4090
- queue: {
4091
- name: queue?.queue.name || void 0,
4092
- ratelimit: queue?.ratelimit || void 0,
4093
- concurrency: {
4094
- worker: queue?.concurrency?.worker || void 0,
4095
- queue: queue?.concurrency?.queue || void 0
4096
- }
4097
- }
4098
- },
4099
- execute: "function"
4100
- }
4101
- } : {},
4102
4132
  label
4103
4133
  };
4104
4134
  })),
@@ -4167,6 +4197,7 @@ type PageInfo {
4167
4197
  type QueueResult {
4168
4198
  name: String!
4169
4199
  concurrency: QueueConcurrency!
4200
+ timeoutInSeconds: Int!
4170
4201
  ratelimit: Int!
4171
4202
  isMaxed: Boolean!
4172
4203
  isPaused: Boolean!
@@ -4248,17 +4279,12 @@ type AgentEvalFunctionConfig {
4248
4279
  }
4249
4280
 
4250
4281
  type ItemChunks {
4251
- cosine_distance: Float
4252
- fts_rank: Float
4253
- hybrid_score: Float
4254
- content: String
4255
- source: ID
4256
- chunk_index: Int
4257
- chunk_id: ID
4258
- chunk_created_at: Date
4259
- chunk_updated_at: Date
4260
- embedding_size: Float
4261
- metadata: JSON
4282
+ chunk_id: String!
4283
+ chunk_index: Int!
4284
+ chunk_content: String!
4285
+ chunk_source: String!
4286
+ chunk_created_at: Date!
4287
+ chunk_updated_at: Date!
4262
4288
  }
4263
4289
 
4264
4290
  type Provider {
@@ -4294,7 +4320,7 @@ type Context {
4294
4320
  fields: JSON
4295
4321
  configuration: JSON
4296
4322
  sources: [ContextSource]
4297
- processors: [ContextProcessor]
4323
+ processor: ContextProcessor
4298
4324
  }
4299
4325
  type Embedder {
4300
4326
  name: String!
@@ -4308,7 +4334,7 @@ type EmbedderConfig {
4308
4334
  default: String
4309
4335
  }
4310
4336
  type ContextProcessor {
4311
- field: String!
4337
+ name: String!
4312
4338
  description: String
4313
4339
  queue: String
4314
4340
  trigger: String
@@ -4372,6 +4398,9 @@ type Job {
4372
4398
  name: String!
4373
4399
  returnvalue: JSON
4374
4400
  stacktrace: [String]
4401
+ finishedOn: Date
4402
+ processedOn: Date
4403
+ attemptsMade: Int
4375
4404
  failedReason: String
4376
4405
  state: String!
4377
4406
  data: JSON
@@ -4478,6 +4507,8 @@ var getPresignedUrl = async (bucket, key, config) => {
4478
4507
  if (!config.fileUploads) {
4479
4508
  throw new Error("File uploads are not configured");
4480
4509
  }
4510
+ console.log("[EXULU] getting presigned url for bucket", bucket);
4511
+ console.log("[EXULU] getting presigned url for key", key);
4481
4512
  const url = await (0, import_s3_request_presigner.getSignedUrl)(
4482
4513
  getS3Client(config),
4483
4514
  new import_client_s3.GetObjectCommand({
@@ -6044,12 +6075,12 @@ var ExuluContext = class {
6044
6075
  name;
6045
6076
  active;
6046
6077
  fields;
6078
+ processor;
6047
6079
  rateLimit;
6048
6080
  description;
6049
6081
  embedder;
6050
6082
  queryRewriter;
6051
6083
  resultReranker;
6052
- // todo typings
6053
6084
  configuration;
6054
6085
  sources = [];
6055
6086
  constructor({
@@ -6057,6 +6088,7 @@ var ExuluContext = class {
6057
6088
  name,
6058
6089
  description,
6059
6090
  embedder,
6091
+ processor,
6060
6092
  active,
6061
6093
  rateLimit,
6062
6094
  fields,
@@ -6069,10 +6101,12 @@ var ExuluContext = class {
6069
6101
  this.name = name;
6070
6102
  this.fields = fields || [];
6071
6103
  this.sources = sources || [];
6104
+ this.processor = processor;
6072
6105
  this.configuration = configuration || {
6073
6106
  calculateVectors: "manual",
6074
6107
  language: "english",
6075
- defaultRightsMode: "private"
6108
+ defaultRightsMode: "private",
6109
+ maxRetrievalResults: 10
6076
6110
  };
6077
6111
  this.description = description;
6078
6112
  this.embedder = embedder;
@@ -6082,26 +6116,18 @@ var ExuluContext = class {
6082
6116
  this.resultReranker = resultReranker;
6083
6117
  }
6084
6118
  processField = async (trigger, item, exuluConfig, user, role) => {
6085
- if (!item.field) {
6086
- throw new Error("Field property on item is required for running a specific processor.");
6087
- }
6088
- console.log("[EXULU] processing field", item.field, " in context", this.id);
6089
- console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
6090
- const field = this.fields.find((field2) => {
6091
- return field2.name.replace("_s3key", "") === item.field.replace("_s3key", "");
6092
- });
6093
- if (!field || !field.processor) {
6094
- console.error("[EXULU] field not found or processor not set for field", item.field, " in context", this.id);
6095
- throw new Error("Field not found or processor not set for field " + item.field + " in context " + this.id);
6096
- }
6119
+ console.log("[EXULU] processing item, ", item, " in context", this.id);
6097
6120
  const exuluStorage = new ExuluStorage({ config: exuluConfig });
6098
- const queue = await field.processor.config?.queue;
6121
+ if (!this.processor) {
6122
+ throw new Error(`Processor is not set for this context: ${this.id}.`);
6123
+ }
6124
+ const queue = await this.processor.config?.queue;
6099
6125
  if (queue?.queue.name) {
6100
6126
  console.log("[EXULU] processor is in queue mode, scheduling job.");
6101
6127
  const job = await bullmqDecorator({
6102
- timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 600,
6103
- label: `${this.name} ${field.name} data processor`,
6104
- processor: `${this.id}-${field.name}`,
6128
+ timeoutInSeconds: this.processor.config?.timeoutInSeconds || 600,
6129
+ label: `${this.name} ${this.processor.name} data processor`,
6130
+ processor: `${this.id}-${this.processor.name}`,
6105
6131
  context: this.id,
6106
6132
  inputs: item,
6107
6133
  item: item.id,
@@ -6116,12 +6142,12 @@ var ExuluContext = class {
6116
6142
  trigger
6117
6143
  });
6118
6144
  return {
6119
- result: {},
6145
+ result: void 0,
6120
6146
  job: job.id
6121
6147
  };
6122
6148
  }
6123
6149
  console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
6124
- const processorResult = await field.processor.execute({
6150
+ const processorResult = await this.processor.execute({
6125
6151
  item,
6126
6152
  user,
6127
6153
  role,
@@ -6138,7 +6164,8 @@ var ExuluContext = class {
6138
6164
  await db3.from(getTableName(this.id)).where({
6139
6165
  id: processorResult.id
6140
6166
  }).update({
6141
- ...processorResult
6167
+ ...processorResult,
6168
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
6142
6169
  });
6143
6170
  return {
6144
6171
  result: processorResult,
@@ -6152,7 +6179,8 @@ var ExuluContext = class {
6152
6179
  user: options.user,
6153
6180
  role: options.role,
6154
6181
  context: this,
6155
- db: db3
6182
+ db: db3,
6183
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10
6156
6184
  });
6157
6185
  return result;
6158
6186
  };
@@ -6256,9 +6284,8 @@ var ExuluContext = class {
6256
6284
  console.log("[EXULU] context configuration", this.configuration);
6257
6285
  let jobs = [];
6258
6286
  let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
6259
- for (const [key, value] of Object.entries(item)) {
6260
- console.log("[EXULU] Checking for processors for field", key);
6261
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6287
+ if (this.processor) {
6288
+ const processor = this.processor;
6262
6289
  console.log("[EXULU] Processor found", processor);
6263
6290
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6264
6291
  const {
@@ -6268,8 +6295,7 @@ var ExuluContext = class {
6268
6295
  "api",
6269
6296
  {
6270
6297
  ...item,
6271
- id: results[0].id,
6272
- field: key
6298
+ id: results[0].id
6273
6299
  },
6274
6300
  config,
6275
6301
  user,
@@ -6336,8 +6362,8 @@ var ExuluContext = class {
6336
6362
  await mutation;
6337
6363
  let jobs = [];
6338
6364
  let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
6339
- for (const [key, value] of Object.entries(item)) {
6340
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6365
+ if (this.processor) {
6366
+ const processor = this.processor;
6341
6367
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6342
6368
  const {
6343
6369
  job: processorJob,
@@ -6346,8 +6372,7 @@ var ExuluContext = class {
6346
6372
  "api",
6347
6373
  {
6348
6374
  ...item,
6349
- id: record.id,
6350
- field: key
6375
+ id: record.id
6351
6376
  },
6352
6377
  config,
6353
6378
  user,
@@ -6585,22 +6610,26 @@ var ExuluContext = class {
6585
6610
  };
6586
6611
  // Exports the context as a tool that can be used by an agent
6587
6612
  tool = () => {
6613
+ if (this.configuration.enableAsTool === false) {
6614
+ return null;
6615
+ }
6588
6616
  return new ExuluTool2({
6589
6617
  id: this.id,
6590
- name: `${this.name}`,
6618
+ name: `${this.name}_context_search`,
6591
6619
  type: "context",
6592
6620
  category: "contexts",
6593
6621
  inputSchema: import_zod.z.object({
6594
- query: import_zod.z.string()
6622
+ originalQuestion: import_zod.z.string().describe("The original question that the user asked"),
6623
+ relevantKeywords: import_zod.z.array(import_zod.z.string()).describe("The keywords that are relevant to the user's question, for example names of specific products, systems or parts, IDs, etc.")
6595
6624
  }),
6596
6625
  config: [],
6597
6626
  description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
6598
- execute: async ({ query, user, role }) => {
6627
+ execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
6599
6628
  const { db: db3 } = await postgresClient();
6600
6629
  const result = await vectorSearch({
6601
6630
  page: 1,
6602
- limit: 50,
6603
- query,
6631
+ limit: this.configuration.maxRetrievalResults ?? 10,
6632
+ query: originalQuestion,
6604
6633
  filters: [],
6605
6634
  user,
6606
6635
  role,
@@ -6620,7 +6649,13 @@ var ExuluContext = class {
6620
6649
  role: user?.role?.id
6621
6650
  });
6622
6651
  return {
6623
- items: result.items
6652
+ result: JSON.stringify(result.chunks.map((chunk) => ({
6653
+ ...chunk,
6654
+ context: {
6655
+ name: this.name,
6656
+ id: this.id
6657
+ }
6658
+ })))
6624
6659
  };
6625
6660
  }
6626
6661
  });
@@ -7487,7 +7522,7 @@ var import_ai3 = require("ai");
7487
7522
  var import_crypto_js4 = __toESM(require("crypto-js"), 1);
7488
7523
 
7489
7524
  // src/registry/log-metadata.ts
7490
- function logMetadata2(id, additionalMetadata) {
7525
+ function logMetadata(id, additionalMetadata) {
7491
7526
  return {
7492
7527
  __logMetadata: true,
7493
7528
  id,
@@ -7497,9 +7532,32 @@ function logMetadata2(id, additionalMetadata) {
7497
7532
 
7498
7533
  // src/registry/workers.ts
7499
7534
  var redisConnection;
7535
+ var unhandledRejectionHandlerInstalled = false;
7536
+ var installGlobalErrorHandlers = () => {
7537
+ if (unhandledRejectionHandlerInstalled) return;
7538
+ process.on("unhandledRejection", (reason, promise) => {
7539
+ console.error("[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.", {
7540
+ reason: reason instanceof Error ? reason.message : String(reason),
7541
+ stack: reason instanceof Error ? reason.stack : void 0
7542
+ });
7543
+ });
7544
+ process.on("uncaughtException", (error) => {
7545
+ console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
7546
+ error: error.message,
7547
+ stack: error.stack
7548
+ });
7549
+ if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
7550
+ console.error("[EXULU] Fatal error detected, exiting process.");
7551
+ process.exit(1);
7552
+ }
7553
+ });
7554
+ unhandledRejectionHandlerInstalled = true;
7555
+ console.log("[EXULU] Global error handlers installed to prevent worker crashes");
7556
+ };
7500
7557
  var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
7501
7558
  console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
7502
7559
  console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
7560
+ installGlobalErrorHandlers();
7503
7561
  if (!redisServer.host || !redisServer.port) {
7504
7562
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
7505
7563
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -7524,7 +7582,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7524
7582
  const worker = new import_bullmq4.Worker(
7525
7583
  `${queue.queue.name}`,
7526
7584
  async (bullmqJob) => {
7527
- console.log("[EXULU] starting execution for job", logMetadata2(bullmqJob.name, {
7585
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
7528
7586
  name: bullmqJob.name,
7529
7587
  jobId: bullmqJob.id,
7530
7588
  status: await bullmqJob.getState(),
@@ -7534,9 +7592,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7534
7592
  const data = bullmqJob.data;
7535
7593
  const timeoutInSeconds = data.timeoutInSeconds || 600;
7536
7594
  const timeoutMs = timeoutInSeconds * 1e3;
7595
+ let timeoutHandle;
7537
7596
  const timeoutPromise = new Promise((_, reject) => {
7538
- setTimeout(() => {
7539
- reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`));
7597
+ timeoutHandle = setTimeout(() => {
7598
+ const timeoutError = new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`);
7599
+ console.error(`[EXULU] ${timeoutError.message}`);
7600
+ reject(timeoutError);
7540
7601
  }, timeoutMs);
7541
7602
  });
7542
7603
  const workPromise = (async () => {
@@ -7544,7 +7605,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7544
7605
  console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
7545
7606
  bullmq.validate(bullmqJob.id, data);
7546
7607
  if (data.type === "embedder") {
7547
- console.log("[EXULU] running an embedder job.", logMetadata2(bullmqJob.name));
7608
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
7548
7609
  const label = `embedder-${bullmqJob.name}`;
7549
7610
  await db3.from("job_results").insert({
7550
7611
  job_id: bullmqJob.id,
@@ -7564,12 +7625,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7564
7625
  if (!embedder) {
7565
7626
  throw new Error(`Embedder ${data.embedder} not found in the registry.`);
7566
7627
  }
7567
- const result2 = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
7628
+ const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
7568
7629
  label: embedder.name,
7569
7630
  trigger: data.trigger
7570
7631
  }, data.role, bullmqJob.id);
7571
7632
  return {
7572
- result: result2,
7633
+ result,
7573
7634
  metadata: {}
7574
7635
  };
7575
7636
  }
@@ -7587,21 +7648,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7587
7648
  if (!context) {
7588
7649
  throw new Error(`Context ${data.context} not found in the registry.`);
7589
7650
  }
7590
- const field = context.fields.find((field2) => {
7591
- return field2.name.replace("_s3key", "") === data.inputs.field.replace("_s3key", "");
7592
- });
7593
- if (!field) {
7594
- throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
7595
- }
7596
- if (!field.processor) {
7597
- throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
7598
- }
7599
7651
  if (!data.inputs.id) {
7600
- throw new Error(`[EXULU] Item not set for processor ${field.name} in context ${context.id}, running in job ${bullmqJob.id}.`);
7652
+ throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
7653
+ }
7654
+ if (!context.processor) {
7655
+ throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
7601
7656
  }
7602
7657
  const exuluStorage = new ExuluStorage({ config });
7603
7658
  console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
7604
- const processorResult = await field.processor.execute({
7659
+ const processorResult = await context.processor.execute({
7605
7660
  item: data.inputs,
7606
7661
  user: data.user,
7607
7662
  role: data.role,
@@ -7611,16 +7666,17 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7611
7666
  exuluConfig: config
7612
7667
  });
7613
7668
  if (!processorResult) {
7614
- throw new Error(`[EXULU] Processor ${field.name} in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7669
+ throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7615
7670
  }
7616
7671
  delete processorResult.field;
7617
7672
  await db3.from(getTableName(context.id)).where({
7618
7673
  id: processorResult.id
7619
7674
  }).update({
7620
- ...processorResult
7675
+ ...processorResult,
7676
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
7621
7677
  });
7622
7678
  let jobs = [];
7623
- if (field.processor.config?.generateEmbeddings) {
7679
+ if (context.processor?.config?.generateEmbeddings) {
7624
7680
  const fullItem = await db3.from(getTableName(context.id)).where({
7625
7681
  id: processorResult.id
7626
7682
  }).first();
@@ -7646,7 +7702,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7646
7702
  };
7647
7703
  }
7648
7704
  if (data.type === "eval_run") {
7649
- console.log("[EXULU] running an eval run job.", logMetadata2(bullmqJob.name));
7705
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
7650
7706
  const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
7651
7707
  const existingResult = await db3.from("job_results").where({ label }).first();
7652
7708
  if (existingResult) {
@@ -7695,7 +7751,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7695
7751
  resolve(messages2);
7696
7752
  break;
7697
7753
  } catch (error) {
7698
- console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata2(bullmqJob.name, {
7754
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
7699
7755
  error: error instanceof Error ? error.message : String(error)
7700
7756
  }));
7701
7757
  attempts++;
@@ -7706,9 +7762,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7706
7762
  }
7707
7763
  }
7708
7764
  });
7709
- const result2 = await promise;
7710
- const messages = result2.messages;
7711
- const metadata = result2.metadata;
7765
+ const result = await promise;
7766
+ const messages = result.messages;
7767
+ const metadata = result.metadata;
7712
7768
  const evalFunctions = evalRun.eval_functions;
7713
7769
  let evalFunctionResults = [];
7714
7770
  for (const evalFunction of evalFunctions) {
@@ -7716,7 +7772,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7716
7772
  if (!evalMethod) {
7717
7773
  throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
7718
7774
  }
7719
- let result3;
7775
+ let result2;
7720
7776
  if (evalMethod.queue) {
7721
7777
  const queue2 = await evalMethod.queue;
7722
7778
  const jobData = {
@@ -7747,21 +7803,21 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7747
7803
  if (!job.id) {
7748
7804
  throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
7749
7805
  }
7750
- result3 = await pollJobResult({ queue: queue2, jobId: job.id });
7806
+ result2 = await pollJobResult({ queue: queue2, jobId: job.id });
7751
7807
  const evalFunctionResult = {
7752
7808
  test_case_id: testCase.id,
7753
7809
  eval_run_id: evalRun.id,
7754
7810
  eval_function_id: evalFunction.id,
7755
7811
  eval_function_name: evalFunction.name,
7756
7812
  eval_function_config: evalFunction.config || {},
7757
- result: result3 || 0
7813
+ result: result2 || 0
7758
7814
  };
7759
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result3}`, logMetadata2(bullmqJob.name, {
7760
- result: result3 || 0
7815
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7816
+ result: result2 || 0
7761
7817
  }));
7762
7818
  evalFunctionResults.push(evalFunctionResult);
7763
7819
  } else {
7764
- result3 = await evalMethod.run(
7820
+ result2 = await evalMethod.run(
7765
7821
  agentInstance,
7766
7822
  agentBackend,
7767
7823
  testCase,
@@ -7772,15 +7828,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7772
7828
  test_case_id: testCase.id,
7773
7829
  eval_run_id: evalRun.id,
7774
7830
  eval_function_id: evalFunction.id,
7775
- result: result3 || 0
7831
+ result: result2 || 0
7776
7832
  };
7777
7833
  evalFunctionResults.push(evalFunctionResult);
7778
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result3}`, logMetadata2(bullmqJob.name, {
7779
- result: result3 || 0
7834
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7835
+ result: result2 || 0
7780
7836
  }));
7781
7837
  }
7782
7838
  }
7783
- const scores = evalFunctionResults.map((result3) => result3.result);
7839
+ const scores = evalFunctionResults.map((result2) => result2.result);
7784
7840
  console.log("[EXULU] Exulu eval run scores for test case: " + testCase.id, scores);
7785
7841
  let score = 0;
7786
7842
  switch (data.scoring_method?.toLowerCase()) {
@@ -7812,7 +7868,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7812
7868
  };
7813
7869
  }
7814
7870
  if (data.type === "eval_function") {
7815
- console.log("[EXULU] running an eval function job.", logMetadata2(bullmqJob.name));
7871
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
7816
7872
  if (data.eval_functions?.length !== 1) {
7817
7873
  throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
7818
7874
  }
@@ -7845,30 +7901,30 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7845
7901
  messages: inputMessages
7846
7902
  } = await validateEvalPayload(data, agents);
7847
7903
  const evalFunctions = evalRun.eval_functions;
7848
- let result2;
7904
+ let result;
7849
7905
  for (const evalFunction of evalFunctions) {
7850
7906
  const evalMethod = evals.find((e) => e.id === evalFunction.id);
7851
7907
  if (!evalMethod) {
7852
7908
  throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
7853
7909
  }
7854
- result2 = await evalMethod.run(
7910
+ result = await evalMethod.run(
7855
7911
  agentInstance,
7856
7912
  backend,
7857
7913
  testCase,
7858
7914
  inputMessages,
7859
7915
  evalFunction.config || {}
7860
7916
  );
7861
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7862
- result: result2 || 0
7917
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
7918
+ result: result || 0
7863
7919
  }));
7864
7920
  }
7865
7921
  return {
7866
- result: result2,
7922
+ result,
7867
7923
  metadata: {}
7868
7924
  };
7869
7925
  }
7870
7926
  if (data.type === "source") {
7871
- console.log("[EXULU] running a source job.", logMetadata2(bullmqJob.name));
7927
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
7872
7928
  if (!data.source) {
7873
7929
  throw new Error(`No source id set for source job.`);
7874
7930
  }
@@ -7883,10 +7939,10 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7883
7939
  if (!source) {
7884
7940
  throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
7885
7941
  }
7886
- const result2 = await source.execute(data.inputs);
7942
+ const result = await source.execute(data.inputs);
7887
7943
  let jobs = [];
7888
7944
  let items = [];
7889
- for (const item of result2) {
7945
+ for (const item of result) {
7890
7946
  const { item: createdItem, job } = await context.createItem(
7891
7947
  item,
7892
7948
  config,
@@ -7896,14 +7952,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7896
7952
  );
7897
7953
  if (job) {
7898
7954
  jobs.push(job);
7899
- console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata2(bullmqJob.name, {
7955
+ console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
7900
7956
  item: createdItem,
7901
7957
  job
7902
7958
  }));
7903
7959
  }
7904
7960
  if (createdItem.id) {
7905
7961
  items.push(createdItem.id);
7906
- console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata2(bullmqJob.name, {
7962
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
7907
7963
  item: createdItem
7908
7964
  }));
7909
7965
  }
@@ -7918,7 +7974,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7918
7974
  role: data?.role
7919
7975
  });
7920
7976
  return {
7921
- result: result2,
7977
+ result,
7922
7978
  metadata: {
7923
7979
  jobs,
7924
7980
  items
@@ -7931,8 +7987,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7931
7987
  throw error;
7932
7988
  }
7933
7989
  })();
7934
- const result = await Promise.race([workPromise, timeoutPromise]);
7935
- return result;
7990
+ try {
7991
+ const result = await Promise.race([workPromise, timeoutPromise]);
7992
+ clearTimeout(timeoutHandle);
7993
+ return result;
7994
+ } catch (error) {
7995
+ clearTimeout(timeoutHandle);
7996
+ console.error(`[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`, error instanceof Error ? error.message : String(error));
7997
+ throw error;
7998
+ }
7936
7999
  },
7937
8000
  {
7938
8001
  autorun: true,
@@ -7967,7 +8030,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7967
8030
  });
7968
8031
  return;
7969
8032
  }
7970
- console.error(`[EXULU] job failed.`, job?.name ? logMetadata2(job.name, {
8033
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
7971
8034
  error: error instanceof Error ? error.message : String(error)
7972
8035
  }) : error);
7973
8036
  });
@@ -7975,7 +8038,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7975
8038
  console.error(`[EXULU] worker error.`, error);
7976
8039
  });
7977
8040
  worker.on("progress", (job, progress) => {
7978
- console.log(`[EXULU] job progress ${job.id}.`, logMetadata2(job.name, {
8041
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
7979
8042
  progress
7980
8043
  }));
7981
8044
  });
@@ -9141,7 +9204,7 @@ var ExuluQueues = class {
9141
9204
  // method of ExuluQueues we need to store the desired rate limit on the queue
9142
9205
  // here so we can use the value when creating workers for the queue instance
9143
9206
  // as there is no way to store a rate limit value natively on a bullm queue.
9144
- register = (name, concurrency, ratelimit = 1) => {
9207
+ register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
9145
9208
  const queueConcurrency = concurrency.queue || 1;
9146
9209
  const workerConcurrency = concurrency.worker || 1;
9147
9210
  const use = async () => {
@@ -9157,7 +9220,8 @@ var ExuluQueues = class {
9157
9220
  concurrency: {
9158
9221
  worker: workerConcurrency,
9159
9222
  queue: queueConcurrency
9160
- }
9223
+ },
9224
+ timeoutInSeconds
9161
9225
  };
9162
9226
  }
9163
9227
  if (!redisServer.host?.length || !redisServer.port?.length) {
@@ -9183,7 +9247,8 @@ var ExuluQueues = class {
9183
9247
  concurrency: {
9184
9248
  worker: workerConcurrency,
9185
9249
  queue: queueConcurrency
9186
- }
9250
+ },
9251
+ timeoutInSeconds
9187
9252
  });
9188
9253
  return {
9189
9254
  queue: newQueue,
@@ -9191,7 +9256,8 @@ var ExuluQueues = class {
9191
9256
  concurrency: {
9192
9257
  worker: workerConcurrency,
9193
9258
  queue: queueConcurrency
9194
- }
9259
+ },
9260
+ timeoutInSeconds
9195
9261
  };
9196
9262
  };
9197
9263
  this.list.set(name, {
@@ -9201,6 +9267,7 @@ var ExuluQueues = class {
9201
9267
  queue: queueConcurrency
9202
9268
  },
9203
9269
  ratelimit,
9270
+ timeoutInSeconds,
9204
9271
  use
9205
9272
  });
9206
9273
  return {
@@ -10266,7 +10333,7 @@ var ExuluApp = class {
10266
10333
  ...[previewPdfTool],
10267
10334
  ...todoTools,
10268
10335
  // Add contexts as tools
10269
- ...Object.values(contexts || {}).map((context) => context.tool())
10336
+ ...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
10270
10337
  // Because agents are stored in the database, we add those as tools
10271
10338
  // at request time, not during ExuluApp initialization. We add them
10272
10339
  // in the grahql tools resolver.
@@ -10428,10 +10495,7 @@ var ExuluApp = class {
10428
10495
  console.warn("[EXULU] No queue configured for source", source.name);
10429
10496
  continue;
10430
10497
  }
10431
- if (queue) {
10432
- if (!source.config?.schedule) {
10433
- throw new Error("Schedule is required for source when configuring a queue: " + source.name);
10434
- }
10498
+ if (queue && source.config?.schedule) {
10435
10499
  console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
10436
10500
  await queue.queue?.upsertJobScheduler(source.id, {
10437
10501
  pattern: source.config?.schedule