@exulu/backend 1.40.0 → 1.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 +2607,14 @@ 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;
2572
2614
  }
2573
2615
  console.log("[EXULU] user.role", user?.role);
2574
2616
  console.log("[EXULU] table.name.plural", table.name.plural);
2575
- if (!user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2617
+ if (user && !user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2576
2618
  console.error("==== Access control error: no role found or no access to entity type. ====");
2577
2619
  throw new Error("Access control error: no role found or no access to entity type.");
2578
2620
  }
@@ -2581,18 +2623,25 @@ var applyAccessControl = (table, query, user) => {
2581
2623
  if (!hasRBAC) {
2582
2624
  return query;
2583
2625
  }
2626
+ if (user?.super_admin) {
2627
+ return query;
2628
+ }
2629
+ const prefix = field_prefix ? field_prefix + "." : "";
2630
+ console.log("[EXULU] applying access control with this prefix", prefix);
2584
2631
  try {
2585
2632
  query = query.where(function() {
2586
- this.where("rights_mode", "public");
2587
- this.orWhere("created_by", user.id);
2588
- this.orWhere(function() {
2589
- this.where("rights_mode", "users").whereExists(function() {
2590
- 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);
2633
+ this.where(`${prefix}rights_mode`, "public");
2634
+ if (user) {
2635
+ this.orWhere(`${prefix}created_by`, user.id);
2636
+ this.orWhere(function() {
2637
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2638
+ 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);
2639
+ });
2591
2640
  });
2592
- });
2593
- if (user.role) {
2641
+ }
2642
+ if (user?.role) {
2594
2643
  this.orWhere(function() {
2595
- this.where("rights_mode", "roles").whereExists(function() {
2644
+ this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2596
2645
  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
2646
  });
2598
2647
  });
@@ -2604,9 +2653,11 @@ var applyAccessControl = (table, query, user) => {
2604
2653
  }
2605
2654
  return query;
2606
2655
  };
2607
- var converOperatorToQuery = (query, fieldName, operators, table) => {
2656
+ var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
2608
2657
  const field = table?.fields.find((f) => f.name === fieldName);
2609
2658
  const isJsonField = field?.type === "json";
2659
+ const prefix = field_prefix ? field_prefix + "." : "";
2660
+ fieldName = prefix + fieldName;
2610
2661
  if (operators.eq !== void 0) {
2611
2662
  if (isJsonField) {
2612
2663
  query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
@@ -2916,53 +2967,95 @@ var finalizeRequestedFields = async ({
2916
2967
  }
2917
2968
  const { db: db3 } = await postgresClient();
2918
2969
  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
2970
  const chunks = await query;
2923
2971
  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,
2972
+ chunk_content: chunk.content,
2973
+ chunk_source: chunk.source,
2929
2974
  chunk_index: chunk.chunk_index,
2930
2975
  chunk_id: chunk.id,
2931
2976
  chunk_created_at: chunk.createdAt,
2932
2977
  chunk_updated_at: chunk.updatedAt,
2933
- embedding_size: chunk.embedding_size
2978
+ item_updated_at: chunk.item_updated_at,
2979
+ item_created_at: chunk.item_created_at,
2980
+ item_id: chunk.item_id,
2981
+ item_external_id: chunk.item_external_id,
2982
+ item_name: chunk.item_name
2934
2983
  }));
2935
2984
  }
2936
2985
  }
2937
2986
  }
2938
2987
  return result;
2939
2988
  };
2940
- var applyFilters = (query, filters, table) => {
2989
+ var applyFilters = (query, filters, table, field_prefix) => {
2941
2990
  filters.forEach((filter) => {
2942
2991
  Object.entries(filter).forEach(([fieldName, operators]) => {
2943
2992
  if (operators) {
2944
2993
  if (operators.and !== void 0) {
2945
2994
  operators.and.forEach((operator) => {
2946
- query = converOperatorToQuery(query, fieldName, operator, table);
2995
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2947
2996
  });
2948
2997
  }
2949
2998
  if (operators.or !== void 0) {
2950
2999
  operators.or.forEach((operator) => {
2951
- query = converOperatorToQuery(query, fieldName, operator, table);
3000
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2952
3001
  });
2953
3002
  }
2954
- query = converOperatorToQuery(query, fieldName, operators, table);
3003
+ query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
2955
3004
  }
2956
3005
  });
2957
3006
  });
2958
3007
  return query;
2959
3008
  };
2960
- var applySorting = (query, sort) => {
3009
+ var applySorting = (query, sort, field_prefix) => {
3010
+ const prefix = field_prefix ? field_prefix + "." : "";
2961
3011
  if (sort) {
3012
+ sort.field = prefix + sort.field;
2962
3013
  query = query.orderBy(sort.field, sort.direction.toLowerCase());
2963
3014
  }
2964
3015
  return query;
2965
3016
  };
3017
+ var paginationRequest = async ({
3018
+ db: db3,
3019
+ limit,
3020
+ page,
3021
+ filters,
3022
+ sort,
3023
+ table,
3024
+ user,
3025
+ fields
3026
+ }) => {
3027
+ if (limit > 1e4) {
3028
+ throw new Error("Limit cannot be greater than 10.000.");
3029
+ }
3030
+ const tableName = table.name.plural.toLowerCase();
3031
+ let countQuery = db3(tableName);
3032
+ countQuery = applyFilters(countQuery, filters, table);
3033
+ countQuery = applyAccessControl(table, countQuery, user);
3034
+ const countResult = await countQuery.count("* as count");
3035
+ const itemCount = Number(countResult[0]?.count || 0);
3036
+ const pageCount = Math.ceil(itemCount / limit);
3037
+ const currentPage = page;
3038
+ const hasPreviousPage = currentPage > 1;
3039
+ const hasNextPage = currentPage < pageCount - 1;
3040
+ let dataQuery = db3(tableName);
3041
+ dataQuery = applyFilters(dataQuery, filters, table);
3042
+ dataQuery = applyAccessControl(table, dataQuery, user);
3043
+ dataQuery = applySorting(dataQuery, sort);
3044
+ if (page > 1) {
3045
+ dataQuery = dataQuery.offset((page - 1) * limit);
3046
+ }
3047
+ let items = await dataQuery.select(fields ? fields : "*").limit(limit);
3048
+ return {
3049
+ items,
3050
+ pageInfo: {
3051
+ pageCount,
3052
+ itemCount,
3053
+ currentPage,
3054
+ hasPreviousPage,
3055
+ hasNextPage
3056
+ }
3057
+ };
3058
+ };
2966
3059
  function createQueries(table, agents, tools, contexts) {
2967
3060
  const tableNamePlural = table.name.plural.toLowerCase();
2968
3061
  const tableNameSingular = table.name.singular.toLowerCase();
@@ -2998,38 +3091,22 @@ function createQueries(table, agents, tools, contexts) {
2998
3091
  return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
2999
3092
  },
3000
3093
  [`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
3001
- const { limit = 10, page = 0, filters = [], sort } = args;
3002
3094
  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);
3095
+ const { limit = 10, page = 0, filters = [], sort } = args;
3018
3096
  const requestedFields = getRequestedFields(info);
3019
- dataQuery = applySorting(dataQuery, sort);
3020
- if (page > 1) {
3021
- dataQuery = dataQuery.offset((page - 1) * limit);
3022
- }
3023
3097
  const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
3024
- let items = await dataQuery.select(sanitizedFields).limit(limit);
3098
+ const { items, pageInfo } = await paginationRequest({
3099
+ db: db3,
3100
+ limit,
3101
+ page,
3102
+ filters,
3103
+ sort,
3104
+ table,
3105
+ user: context.user,
3106
+ fields: sanitizedFields
3107
+ });
3025
3108
  return {
3026
- pageInfo: {
3027
- pageCount,
3028
- itemCount,
3029
- currentPage,
3030
- hasPreviousPage,
3031
- hasNextPage
3032
- },
3109
+ pageInfo,
3033
3110
  items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
3034
3111
  };
3035
3112
  },
@@ -3078,7 +3155,7 @@ function createQueries(table, agents, tools, contexts) {
3078
3155
  }
3079
3156
  const { limit = 10, page = 0, filters = [], sort } = args;
3080
3157
  return await vectorSearch({
3081
- limit,
3158
+ limit: limit || exists.configuration.maxRetrievalResults || 10,
3082
3159
  page,
3083
3160
  filters,
3084
3161
  sort,
@@ -3088,7 +3165,9 @@ function createQueries(table, agents, tools, contexts) {
3088
3165
  method: args.method,
3089
3166
  user: context.user,
3090
3167
  role: context.user?.role?.id,
3091
- trigger: "api"
3168
+ trigger: "api",
3169
+ cutoffs: args.cutoffs,
3170
+ expand: args.expand
3092
3171
  });
3093
3172
  };
3094
3173
  }
@@ -3105,7 +3184,9 @@ var vectorSearch = async ({
3105
3184
  method,
3106
3185
  user,
3107
3186
  role,
3108
- trigger
3187
+ trigger,
3188
+ cutoffs,
3189
+ expand
3109
3190
  }) => {
3110
3191
  const table = contextToTableDefinition(context);
3111
3192
  console.log("[EXULU] Called vector search.", {
@@ -3117,10 +3198,12 @@ var vectorSearch = async ({
3117
3198
  query,
3118
3199
  method,
3119
3200
  user,
3120
- role
3201
+ role,
3202
+ cutoffs,
3203
+ expand
3121
3204
  });
3122
- if (limit > 50) {
3123
- throw new Error("Limit cannot be greater than 50.");
3205
+ if (limit > 250) {
3206
+ throw new Error("Limit cannot be greater than 1000.");
3124
3207
  }
3125
3208
  if (!query) {
3126
3209
  throw new Error("Query is required.");
@@ -3137,106 +3220,135 @@ var vectorSearch = async ({
3137
3220
  }
3138
3221
  const mainTable = getTableName(id);
3139
3222
  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);
3223
+ cutoffs = {
3224
+ cosineDistance: cutoffs?.cosineDistance || context.configuration?.cutoffs?.cosineDistance || 0,
3225
+ tsvector: cutoffs?.tsvector || context.configuration?.cutoffs?.tsvector || 0,
3226
+ hybrid: cutoffs?.hybrid ? (cutoffs?.hybrid ?? 0) / 100 : context.configuration?.cutoffs ? (context.configuration?.cutoffs?.hybrid ?? 0) / 100 : 0
3227
+ };
3228
+ expand = {
3229
+ before: expand?.before || context.configuration?.expand?.before || 0,
3230
+ after: expand?.after || context.configuration?.expand?.after || 0
3231
+ };
3232
+ let chunksQuery = db3(chunksTable + " as chunks").select([
3233
+ "chunks.id as chunk_id",
3234
+ "chunks.source",
3235
+ "chunks.content",
3236
+ "chunks.chunk_index",
3237
+ db3.raw('chunks."createdAt" as chunk_created_at'),
3238
+ db3.raw('chunks."updatedAt" as chunk_updated_at'),
3239
+ "chunks.metadata",
3240
+ "items.id as item_id",
3241
+ "items.name as item_name",
3242
+ "items.external_id as item_external_id",
3243
+ db3.raw('items."updatedAt" as item_updated_at'),
3244
+ db3.raw('items."createdAt" as item_created_at')
3245
+ ]);
3246
+ chunksQuery.leftJoin(mainTable + " as items", function() {
3247
+ this.on("chunks.source", "=", "items.id");
3248
+ });
3249
+ chunksQuery = applyFilters(chunksQuery, filters, table, "items");
3250
+ chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
3251
+ chunksQuery = applySorting(chunksQuery, sort, "items");
3148
3252
  if (queryRewriter) {
3149
3253
  query = await queryRewriter(query);
3150
3254
  }
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, {
3255
+ const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
3163
3256
  label: table.name.singular,
3164
3257
  trigger
3165
3258
  }, user?.id, role);
3166
- if (!chunks?.[0]?.vector) {
3259
+ if (!queryChunks?.[0]?.vector) {
3167
3260
  throw new Error("No vector generated for query.");
3168
3261
  }
3169
- const vector = chunks[0].vector;
3262
+ const vector = queryChunks[0].vector;
3170
3263
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3171
3264
  const vectorExpr = `${vectorStr}::vector`;
3172
3265
  const language = configuration.language || "english";
3173
- let items = [];
3266
+ console.log("[EXULU] Vector search params:", { method, query, cutoffs });
3267
+ let resultChunks = [];
3174
3268
  switch (method) {
3175
3269
  case "tsvector":
3176
- itemsQuery.select(db3.raw(
3177
- `ts_rank(${chunksTable}.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3178
- [language, query]
3270
+ chunksQuery.limit(limit * 2);
3271
+ const tokens = query.trim().split(/\s+/).filter((t) => t.length > 0);
3272
+ const sanitizedTokens = tokens.flatMap((t) => {
3273
+ return t.split(/[^\w]+/).filter((part) => part.length > 0);
3274
+ });
3275
+ const orQuery = sanitizedTokens.join(" | ");
3276
+ console.log("[EXULU] FTS query transformation:", { original: query, tokens, sanitizedTokens, orQuery, cutoff: cutoffs?.tsvector });
3277
+ chunksQuery.select(db3.raw(
3278
+ `ts_rank(chunks.fts, to_tsquery(?, ?)) as fts_rank`,
3279
+ [language, orQuery]
3179
3280
  )).whereRaw(
3180
- `${chunksTable}.fts @@ websearch_to_tsquery(?, ?)`,
3181
- [language, query]
3281
+ `(chunks.fts @@ to_tsquery(?, ?)) AND (items.archived IS FALSE OR items.archived IS NULL)`,
3282
+ [language, orQuery]
3182
3283
  ).orderByRaw(`fts_rank DESC`);
3183
- items = await itemsQuery;
3284
+ console.log("[EXULU] FTS query SQL:", chunksQuery.toQuery());
3285
+ resultChunks = await chunksQuery;
3184
3286
  break;
3185
3287
  case "cosineDistance":
3186
- default:
3187
- itemsQuery.whereNotNull(`${chunksTable}.embedding`);
3188
- itemsQuery.select(
3189
- db3.raw(`1 - (${chunksTable}.embedding <=> ${vectorExpr}) AS cosine_distance`)
3288
+ chunksQuery.limit(limit * 2);
3289
+ chunksQuery.whereNotNull(`chunks.embedding`).whereRaw(`(items.archived IS FALSE OR items.archived IS NULL)`);
3290
+ console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
3291
+ chunksQuery.select(
3292
+ db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
3190
3293
  );
3191
- itemsQuery.orderByRaw(
3192
- `${chunksTable}.embedding <=> ${vectorExpr} ASC NULLS LAST`
3294
+ chunksQuery.orderByRaw(
3295
+ `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3193
3296
  );
3194
- items = await itemsQuery;
3297
+ chunksQuery.whereRaw(`(1 - (chunks.embedding <=> ${vectorExpr}) >= ?)`, [cutoffs?.cosineDistance || 0]);
3298
+ resultChunks = await chunksQuery;
3195
3299
  break;
3196
3300
  case "hybridSearch":
3197
- const matchCount = Math.min(limit * 5, 100);
3198
- const fullTextWeight = 1;
3301
+ const matchCount = Math.min(limit * 2);
3302
+ const fullTextWeight = 2;
3199
3303
  const semanticWeight = 1;
3200
3304
  const rrfK = 50;
3201
3305
  const hybridSQL = `
3202
3306
  WITH full_text AS (
3203
3307
  SELECT
3204
- c.id,
3205
- c.source,
3308
+ chunks.id,
3309
+ chunks.source,
3206
3310
  row_number() OVER (
3207
- ORDER BY ts_rank_cd(c.fts, websearch_to_tsquery(?, ?)) DESC
3311
+ ORDER BY ts_rank(chunks.fts, plainto_tsquery(?, ?)) DESC
3208
3312
  ) AS rank_ix
3209
- FROM ${chunksTable} c
3210
- WHERE c.fts @@ websearch_to_tsquery(?, ?)
3313
+ FROM ${chunksTable} as chunks
3314
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3315
+ WHERE chunks.fts @@ plainto_tsquery(?, ?)
3316
+ AND ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?
3317
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3211
3318
  ORDER BY rank_ix
3212
- LIMIT LEAST(?, 15) * 2
3319
+ LIMIT LEAST(?, 250) * 2
3213
3320
  ),
3214
3321
  semantic AS (
3215
3322
  SELECT
3216
- c.id,
3217
- c.source,
3323
+ chunks.id,
3324
+ chunks.source,
3218
3325
  row_number() OVER (
3219
- ORDER BY c.embedding <=> ${vectorExpr} ASC
3326
+ ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3220
3327
  ) AS rank_ix
3221
- FROM ${chunksTable} c
3222
- WHERE c.embedding IS NOT NULL
3328
+ FROM ${chunksTable} as chunks
3329
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3330
+ WHERE chunks.embedding IS NOT NULL
3331
+ AND (1 - (chunks.embedding <=> ${vectorExpr})) >= ?
3332
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3223
3333
  ORDER BY rank_ix
3224
- LIMIT LEAST(?, 50) * 2
3334
+ LIMIT LEAST(?, 250) * 2
3225
3335
  )
3226
3336
  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
-
3337
+ items.id as item_id,
3338
+ items.name as item_name,
3339
+ items.external_id as item_external_id,
3340
+ chunks.id AS chunk_id,
3341
+ chunks.source,
3342
+ chunks.content,
3343
+ chunks.chunk_index,
3344
+ chunks.metadata,
3345
+ chunks."createdAt" as chunk_created_at,
3346
+ chunks."updatedAt" as chunk_updated_at,
3347
+ items."updatedAt" as item_updated_at,
3348
+ items."createdAt" as item_created_at,
3237
3349
  /* Per-signal scores for introspection */
3238
- ts_rank(c.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3239
- (1 - (c.embedding <=> ${vectorExpr})) AS cosine_distance,
3350
+ ts_rank(chunks.fts, plainto_tsquery(?, ?)) AS fts_rank,
3351
+ (1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
3240
3352
 
3241
3353
  /* Hybrid RRF score */
3242
3354
  (
@@ -3248,22 +3360,35 @@ var vectorSearch = async ({
3248
3360
  FROM full_text ft
3249
3361
  FULL OUTER JOIN semantic se
3250
3362
  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
3363
+ JOIN ${chunksTable} as chunks
3364
+ ON COALESCE(ft.id, se.id) = chunks.id
3365
+ JOIN ${mainTable} as items
3366
+ ON items.id = chunks.source
3367
+ WHERE (
3368
+ COALESCE(1.0 / (? + ft.rank_ix), 0.0) * ?
3369
+ +
3370
+ COALESCE(1.0 / (? + se.rank_ix), 0.0) * ?
3371
+ ) >= ?
3372
+ AND (chunks.fts IS NULL OR ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?)
3373
+ AND (chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)
3255
3374
  ORDER BY hybrid_score DESC
3256
- LIMIT LEAST(?, 50)
3375
+ LIMIT LEAST(?, 250)
3257
3376
  OFFSET 0
3258
3377
  `;
3259
3378
  const bindings = [
3260
- // full_text: websearch_to_tsquery(lang, query) in rank and where
3379
+ // full_text: plainto_tsquery(lang, query) in rank and where
3380
+ language,
3381
+ query,
3261
3382
  language,
3262
3383
  query,
3263
3384
  language,
3264
3385
  query,
3386
+ cutoffs?.tsvector || 0,
3387
+ // full_text tsvector cutoff
3265
3388
  matchCount,
3266
3389
  // full_text limit
3390
+ cutoffs?.cosineDistance || 0,
3391
+ // semantic cosine distance cutoff
3267
3392
  matchCount,
3268
3393
  // semantic limit
3269
3394
  // fts_rank (ts_rank) call
@@ -3274,89 +3399,171 @@ var vectorSearch = async ({
3274
3399
  fullTextWeight,
3275
3400
  rrfK,
3276
3401
  semanticWeight,
3402
+ // WHERE clause hybrid_score filter
3403
+ rrfK,
3404
+ fullTextWeight,
3405
+ rrfK,
3406
+ semanticWeight,
3407
+ cutoffs?.hybrid || 0,
3408
+ // Additional cutoff filters in main WHERE clause
3409
+ language,
3410
+ query,
3411
+ cutoffs?.tsvector || 0,
3412
+ // tsvector cutoff for results from semantic CTE
3413
+ cutoffs?.cosineDistance || 0,
3414
+ // cosine distance cutoff for results from full_text CTE
3277
3415
  matchCount
3278
3416
  // final limit
3279
3417
  ];
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
- });
3418
+ resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3419
+ }
3420
+ console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
3421
+ let results = resultChunks.map((chunk) => ({
3422
+ chunk_content: chunk.content,
3423
+ chunk_index: chunk.chunk_index,
3424
+ chunk_id: chunk.chunk_id,
3425
+ chunk_source: chunk.source,
3426
+ chunk_metadata: chunk.metadata,
3427
+ chunk_created_at: chunk.chunk_created_at,
3428
+ chunk_updated_at: chunk.chunk_updated_at,
3429
+ item_updated_at: chunk.item_updated_at,
3430
+ item_created_at: chunk.item_created_at,
3431
+ item_id: chunk.item_id,
3432
+ item_external_id: chunk.item_external_id,
3433
+ item_name: chunk.item_name,
3434
+ context: {
3435
+ name: table.name.singular,
3436
+ id: table.id || ""
3437
+ },
3438
+ ...(method === "cosineDistance" || method === "hybridSearch") && { chunk_cosine_distance: chunk.cosine_distance },
3439
+ ...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
3440
+ ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score * 1e4 / 100 }
3441
+ }));
3442
+ if (results.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3443
+ const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
3444
+ const topScore = results[0]?.[scoreKey];
3445
+ const bottomScore = results[results.length - 1]?.[scoreKey];
3446
+ const medianScore = results[Math.floor(results.length / 2)]?.[scoreKey];
3447
+ console.log("[EXULU] Score distribution:", {
3448
+ method,
3449
+ count: results.length,
3450
+ topScore: topScore?.toFixed(4),
3451
+ bottomScore: bottomScore?.toFixed(4),
3452
+ medianScore: medianScore?.toFixed(4)
3453
+ });
3454
+ const adaptiveThreshold = topScore ? topScore * 0.6 : 0;
3455
+ const beforeFilterCount = results.length;
3456
+ results = results.filter((chunk) => {
3457
+ const score = chunk[scoreKey];
3458
+ return score !== void 0 && score >= adaptiveThreshold;
3459
+ });
3460
+ const filteredCount = beforeFilterCount - results.length;
3461
+ if (filteredCount > 0) {
3462
+ console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
3324
3463
  }
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;
3464
+ }
3465
+ if (resultReranker && query) {
3466
+ }
3467
+ results = results.slice(0, limit);
3468
+ if (expand?.before || expand?.after) {
3469
+ const expandedMap = /* @__PURE__ */ new Map();
3470
+ for (const chunk of results) {
3471
+ expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
3472
+ }
3473
+ if (expand?.before) {
3474
+ for (const chunk of results) {
3475
+ const indicesToFetch = Array.from(
3476
+ { length: expand.before },
3477
+ (_, i) => chunk.chunk_index - expand.before + i
3478
+ ).filter((index) => index >= 0);
3479
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3480
+ await Promise.all(indicesToFetch.map(async (index) => {
3481
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3482
+ return;
3483
+ }
3484
+ const expandedChunk = await db3(chunksTable).where({
3485
+ source: chunk.item_id,
3486
+ chunk_index: index
3487
+ }).first();
3488
+ if (expandedChunk) {
3489
+ if (expandedChunk) {
3490
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3491
+ chunk_content: expandedChunk.content,
3492
+ chunk_index: expandedChunk.chunk_index,
3493
+ chunk_id: expandedChunk.id,
3494
+ chunk_source: expandedChunk.source,
3495
+ chunk_metadata: expandedChunk.metadata,
3496
+ chunk_created_at: expandedChunk.createdAt,
3497
+ chunk_updated_at: expandedChunk.updatedAt,
3498
+ item_updated_at: chunk.item_updated_at,
3499
+ item_created_at: chunk.item_created_at,
3500
+ item_id: chunk.item_id,
3501
+ item_external_id: chunk.item_external_id,
3502
+ item_name: chunk.item_name,
3503
+ chunk_cosine_distance: 0,
3504
+ chunk_fts_rank: 0,
3505
+ chunk_hybrid_score: 0,
3506
+ context: {
3507
+ name: table.name.singular,
3508
+ id: table.id || ""
3509
+ }
3510
+ });
3511
+ }
3512
+ }
3513
+ }));
3514
+ }
3331
3515
  }
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;
3516
+ if (expand?.after) {
3517
+ for (const chunk of results) {
3518
+ const indicesToFetch = Array.from(
3519
+ { length: expand.after },
3520
+ (_, i) => chunk.chunk_index + i + 1
3521
+ );
3522
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3523
+ await Promise.all(indicesToFetch.map(async (index) => {
3524
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3525
+ return;
3526
+ }
3527
+ const expandedChunk = await db3(chunksTable).where({
3528
+ source: chunk.item_id,
3529
+ chunk_index: index
3530
+ }).first();
3531
+ if (expandedChunk) {
3532
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3533
+ chunk_content: expandedChunk.content,
3534
+ chunk_index: expandedChunk.chunk_index,
3535
+ chunk_id: expandedChunk.id,
3536
+ chunk_source: expandedChunk.source,
3537
+ chunk_metadata: expandedChunk.metadata,
3538
+ chunk_created_at: expandedChunk.createdAt,
3539
+ chunk_updated_at: expandedChunk.updatedAt,
3540
+ item_updated_at: chunk.item_updated_at,
3541
+ item_created_at: chunk.item_created_at,
3542
+ item_id: chunk.item_id,
3543
+ item_external_id: chunk.item_external_id,
3544
+ item_name: chunk.item_name,
3545
+ chunk_cosine_distance: 0,
3546
+ chunk_fts_rank: 0,
3547
+ chunk_hybrid_score: 0,
3548
+ context: {
3549
+ name: table.name.singular,
3550
+ id: table.id || ""
3551
+ }
3552
+ });
3553
+ }
3554
+ }));
3555
+ }
3354
3556
  }
3355
- });
3356
- if (resultReranker && query) {
3357
- items = await resultReranker(items);
3557
+ results = Array.from(expandedMap.values());
3558
+ results = results.sort((a, b) => {
3559
+ if (a.item_id !== b.item_id) {
3560
+ return a.item_id.localeCompare(b.item_id);
3561
+ }
3562
+ const aIndex = Number(a.chunk_index);
3563
+ const bIndex = Number(b.chunk_index);
3564
+ return aIndex - bIndex;
3565
+ });
3358
3566
  }
3359
- console.log("[EXULU] Vector search results after slicing:", items?.length);
3360
3567
  await updateStatistic({
3361
3568
  name: "count",
3362
3569
  label: table.name.singular,
@@ -3374,7 +3581,7 @@ var vectorSearch = async ({
3374
3581
  id: table.id || "",
3375
3582
  embedder: embedder.name
3376
3583
  },
3377
- items
3584
+ chunks: results
3378
3585
  };
3379
3586
  };
3380
3587
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3404,10 +3611,10 @@ var contextToTableDefinition = (context) => {
3404
3611
  plural: tableName?.endsWith("s") ? tableName : tableName + "s"
3405
3612
  },
3406
3613
  RBAC: true,
3614
+ processor: context.processor,
3407
3615
  fields: context.fields.map((field) => ({
3408
3616
  name: sanitizeName(field.name),
3409
3617
  type: field.type,
3410
- processor: field.processor,
3411
3618
  required: field.required,
3412
3619
  default: field.default,
3413
3620
  index: field.index,
@@ -3537,7 +3744,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3537
3744
  const tableNamePlural = table.name.plural.toLowerCase();
3538
3745
  const tableNameSingular = table.name.singular.toLowerCase();
3539
3746
  const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
3540
- const processorFields = table.fields.filter((field) => field.processor?.execute);
3541
3747
  typeDefs += `
3542
3748
  ${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
3543
3749
 
@@ -3548,7 +3754,7 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3548
3754
  `;
3549
3755
  if (table.type === "items") {
3550
3756
  typeDefs += `
3551
- ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}VectorSearchResult
3757
+ ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}], cutoffs: SearchCutoffs, expand: SearchExpand): ${tableNameSingular}VectorSearchResult
3552
3758
  `;
3553
3759
  }
3554
3760
  mutationDefs += `
@@ -3564,9 +3770,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3564
3770
  ${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
3565
3771
  ${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
3566
3772
  `;
3567
- if (processorFields?.length > 0) {
3773
+ if (table.processor) {
3568
3774
  mutationDefs += `
3569
- ${tableNameSingular}ProcessItemField(item: ID!, field: ${tableNameSingular}ProcessorFieldEnum!): ${tableNameSingular}ProcessItemFieldReturnPayload
3775
+ ${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
3776
+ ${tableNameSingular}ProcessItems(limit: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}ProcessItemFieldReturnPayload
3570
3777
  `;
3571
3778
  }
3572
3779
  modelDefs += `
@@ -3584,8 +3791,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3584
3791
 
3585
3792
  type ${tableNameSingular}ProcessItemFieldReturnPayload {
3586
3793
  message: String!
3587
- result: String!
3588
- job: String
3794
+ results: [String]
3795
+ jobs: [String]
3589
3796
  }
3590
3797
 
3591
3798
  type ${tableNameSingular}DeleteChunksReturnPayload {
@@ -3600,20 +3807,42 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3600
3807
  tsvector
3601
3808
  }
3602
3809
 
3603
- ${processorFields.length > 0 ? `
3604
- enum ${tableNameSingular}ProcessorFieldEnum {
3605
- ${processorFields.map((field) => field.name).join("\n")}
3810
+ input SearchCutoffs {
3811
+ cosineDistance: Float
3812
+ hybrid: Float
3813
+ tsvector: Float
3814
+ }
3815
+
3816
+ input SearchExpand {
3817
+ before: Int
3818
+ after: Int
3606
3819
  }
3607
- ` : ""}
3608
-
3609
3820
 
3610
- type ${tableNameSingular}VectorSearchResult {
3611
- items: [${tableNameSingular}]!
3821
+ type ${tableNameSingular}VectorSearchResult {
3822
+ chunks: [${tableNameSingular}VectorSearchChunk!]!
3612
3823
  context: VectoSearchResultContext!
3613
3824
  filters: JSON!
3614
3825
  query: String!
3615
3826
  method: VectorMethodEnum!
3616
3827
  }
3828
+
3829
+ type ${tableNameSingular}VectorSearchChunk {
3830
+ chunk_content: String
3831
+ chunk_index: Int
3832
+ chunk_id: String
3833
+ chunk_source: String
3834
+ chunk_metadata: JSON
3835
+ chunk_created_at: Date
3836
+ chunk_updated_at: Date
3837
+ item_updated_at: Date
3838
+ item_created_at: Date
3839
+ item_id: String!
3840
+ item_external_id: String
3841
+ item_name: String!
3842
+ chunk_cosine_distance: Float
3843
+ chunk_fts_rank: Float
3844
+ chunk_hybrid_score: Float
3845
+ }
3617
3846
 
3618
3847
  type VectoSearchResultContext {
3619
3848
  name: String!
@@ -3722,6 +3951,7 @@ type PageInfo {
3722
3951
  worker: config2.concurrency?.worker || void 0,
3723
3952
  queue: config2.concurrency?.queue || void 0
3724
3953
  },
3954
+ timeoutInSeconds: config2.timeoutInSeconds,
3725
3955
  ratelimit: config2.ratelimit,
3726
3956
  isMaxed: await config2.queue.isMaxed(),
3727
3957
  isPaused: await config2.queue.isPaused(),
@@ -3935,25 +4165,21 @@ type PageInfo {
3935
4165
  };
3936
4166
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3937
4167
  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);
4168
+ let processor = null;
4169
+ if (context2.processor) {
4170
+ processor = await new Promise(async (resolve, reject) => {
4171
+ const config2 = await context2.processor?.config;
4172
+ const queue = await config2?.queue;
4173
+ resolve({
4174
+ name: context2.processor.name,
4175
+ description: context2.processor.description,
4176
+ queue: queue?.queue?.name || void 0,
4177
+ trigger: context2.processor?.config?.trigger || "manual",
4178
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
4179
+ generateEmbeddings: context2.processor?.config?.generateEmbeddings || false
4180
+ });
4181
+ });
4182
+ }
3957
4183
  const sources = await Promise.all(context2.sources.map(async (source) => {
3958
4184
  let queueName = void 0;
3959
4185
  if (source.config) {
@@ -3985,7 +4211,7 @@ type PageInfo {
3985
4211
  slug: "/contexts/" + context2.id,
3986
4212
  active: context2.active,
3987
4213
  sources,
3988
- processors,
4214
+ processor,
3989
4215
  fields: context2.fields.map((field) => {
3990
4216
  return {
3991
4217
  ...field,
@@ -4011,25 +4237,21 @@ type PageInfo {
4011
4237
  if (!data) {
4012
4238
  return null;
4013
4239
  }
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);
4240
+ let processor = null;
4241
+ if (data.processor) {
4242
+ processor = await new Promise(async (resolve, reject) => {
4243
+ const config2 = await data.processor?.config;
4244
+ const queue = await config2?.queue;
4245
+ resolve({
4246
+ name: data.processor.name,
4247
+ description: data.processor.description,
4248
+ queue: queue?.queue?.name || void 0,
4249
+ trigger: data.processor?.config?.trigger || "manual",
4250
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
4251
+ generateEmbeddings: data.processor?.config?.generateEmbeddings || false
4252
+ });
4253
+ });
4254
+ }
4033
4255
  const sources = await Promise.all(data.sources.map(async (source) => {
4034
4256
  let queueName = void 0;
4035
4257
  if (source.config) {
@@ -4066,39 +4288,18 @@ type PageInfo {
4066
4288
  slug: "/contexts/" + data.id,
4067
4289
  active: data.active,
4068
4290
  sources,
4069
- processors,
4291
+ processor,
4070
4292
  fields: await Promise.all(data.fields.map(async (field) => {
4071
4293
  const label = field.name?.replace("_s3key", "");
4072
4294
  if (field.type === "file" && !field.name.endsWith("_s3key")) {
4073
4295
  field.name = field.name + "_s3key";
4074
4296
  }
4075
- let queue = null;
4076
- if (field.processor?.config?.queue) {
4077
- queue = await field.processor.config.queue;
4078
- }
4079
4297
  return {
4080
4298
  ...field,
4081
4299
  name: sanitizeName(field.name),
4082
4300
  ...field.type === "file" ? {
4083
4301
  allowedFileTypes: field.allowedFileTypes
4084
4302
  } : {},
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
4303
  label
4103
4304
  };
4104
4305
  })),
@@ -4167,6 +4368,7 @@ type PageInfo {
4167
4368
  type QueueResult {
4168
4369
  name: String!
4169
4370
  concurrency: QueueConcurrency!
4371
+ timeoutInSeconds: Int!
4170
4372
  ratelimit: Int!
4171
4373
  isMaxed: Boolean!
4172
4374
  isPaused: Boolean!
@@ -4248,17 +4450,12 @@ type AgentEvalFunctionConfig {
4248
4450
  }
4249
4451
 
4250
4452
  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
4453
+ chunk_id: String!
4454
+ chunk_index: Int!
4455
+ chunk_content: String!
4456
+ chunk_source: String!
4457
+ chunk_created_at: Date!
4458
+ chunk_updated_at: Date!
4262
4459
  }
4263
4460
 
4264
4461
  type Provider {
@@ -4294,7 +4491,7 @@ type Context {
4294
4491
  fields: JSON
4295
4492
  configuration: JSON
4296
4493
  sources: [ContextSource]
4297
- processors: [ContextProcessor]
4494
+ processor: ContextProcessor
4298
4495
  }
4299
4496
  type Embedder {
4300
4497
  name: String!
@@ -4308,7 +4505,7 @@ type EmbedderConfig {
4308
4505
  default: String
4309
4506
  }
4310
4507
  type ContextProcessor {
4311
- field: String!
4508
+ name: String!
4312
4509
  description: String
4313
4510
  queue: String
4314
4511
  trigger: String
@@ -4372,6 +4569,9 @@ type Job {
4372
4569
  name: String!
4373
4570
  returnvalue: JSON
4374
4571
  stacktrace: [String]
4572
+ finishedOn: Date
4573
+ processedOn: Date
4574
+ attemptsMade: Int
4375
4575
  failedReason: String
4376
4576
  state: String!
4377
4577
  data: JSON
@@ -4478,6 +4678,8 @@ var getPresignedUrl = async (bucket, key, config) => {
4478
4678
  if (!config.fileUploads) {
4479
4679
  throw new Error("File uploads are not configured");
4480
4680
  }
4681
+ console.log("[EXULU] getting presigned url for bucket", bucket);
4682
+ console.log("[EXULU] getting presigned url for key", key);
4481
4683
  const url = await (0, import_s3_request_presigner.getSignedUrl)(
4482
4684
  getS3Client(config),
4483
4685
  new import_client_s3.GetObjectCommand({
@@ -4488,6 +4690,18 @@ var getPresignedUrl = async (bucket, key, config) => {
4488
4690
  );
4489
4691
  return url;
4490
4692
  };
4693
+ function sanitizeMetadata(metadata) {
4694
+ if (!metadata) return void 0;
4695
+ const sanitized = {};
4696
+ for (const [key, value] of Object.entries(metadata)) {
4697
+ if (typeof value === "string") {
4698
+ sanitized[key] = encodeURIComponent(value);
4699
+ } else {
4700
+ sanitized[key] = String(value);
4701
+ }
4702
+ }
4703
+ return sanitized;
4704
+ }
4491
4705
  var addGeneralPrefixToKey = (keyPath, config) => {
4492
4706
  if (!config.fileUploads) {
4493
4707
  throw new Error("File uploads are not configured");
@@ -4523,19 +4737,41 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
4523
4737
  const client2 = getS3Client(config);
4524
4738
  let defaultBucket = config.fileUploads.s3Bucket;
4525
4739
  let key = fileName;
4526
- key = addGeneralPrefixToKey(key, config);
4527
4740
  key = addUserPrefixToKey(key, user || "api");
4528
- console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
4741
+ key = addGeneralPrefixToKey(key, config);
4742
+ const sanitizedMetadata = sanitizeMetadata(options.metadata);
4529
4743
  const command = new import_client_s3.PutObjectCommand({
4530
4744
  Bucket: customBucket || defaultBucket,
4531
4745
  Key: key,
4532
4746
  Body: file,
4533
4747
  ContentType: options.contentType,
4534
- Metadata: options.metadata,
4748
+ Metadata: sanitizedMetadata,
4535
4749
  ContentLength: file.byteLength
4536
4750
  });
4537
- await client2.send(command);
4538
- console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
4751
+ const maxRetries = 3;
4752
+ let lastError = null;
4753
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
4754
+ try {
4755
+ await client2.send(command);
4756
+ break;
4757
+ } catch (error) {
4758
+ lastError = error;
4759
+ if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
4760
+ if (attempt < maxRetries) {
4761
+ const backoffMs = Math.pow(2, attempt) * 1e3;
4762
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
4763
+ s3Client = void 0;
4764
+ getS3Client(config);
4765
+ continue;
4766
+ }
4767
+ } else {
4768
+ throw error;
4769
+ }
4770
+ }
4771
+ }
4772
+ if (lastError) {
4773
+ throw lastError;
4774
+ }
4539
4775
  return addBucketPrefixToKey(
4540
4776
  key,
4541
4777
  customBucket || defaultBucket
@@ -4607,6 +4843,8 @@ var createUppyRoutes = async (app, contexts, config) => {
4607
4843
  res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
4608
4844
  return;
4609
4845
  }
4846
+ key = key.replace(`${bucket}/`, "");
4847
+ console.log("[EXULU] deleting file from s3 into bucket", bucket, "with key", key);
4610
4848
  const client2 = getS3Client(config);
4611
4849
  const command = new import_client_s3.DeleteObjectCommand({
4612
4850
  Bucket: bucket,
@@ -4730,7 +4968,7 @@ var createUppyRoutes = async (app, contexts, config) => {
4730
4968
  const client2 = getS3Client(config);
4731
4969
  const command = new import_client_s3.ListObjectsV2Command({
4732
4970
  Bucket: config.fileUploads.s3Bucket,
4733
- Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}${authenticationResult.user.id}`,
4971
+ Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}user_${authenticationResult.user.id}`,
4734
4972
  MaxKeys: 9,
4735
4973
  ...req.query.continuationToken && { ContinuationToken: req.query.continuationToken }
4736
4974
  });
@@ -4742,7 +4980,17 @@ var createUppyRoutes = async (app, contexts, config) => {
4742
4980
  search.toLowerCase()
4743
4981
  ));
4744
4982
  }
4745
- res.json(response);
4983
+ res.json({
4984
+ ...response,
4985
+ Contents: response.Contents?.map((content) => {
4986
+ return {
4987
+ ...content,
4988
+ // For consistency and to support multi-bucket environments
4989
+ // we prepend the bucket name to the key here.
4990
+ Key: `${config.fileUploads?.s3Bucket}/${content.Key}`
4991
+ };
4992
+ })
4993
+ });
4746
4994
  res.end();
4747
4995
  });
4748
4996
  app.get("/s3/sts", (req, res, next) => {
@@ -4803,8 +5051,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4803
5051
  const { filename, contentType } = extractFileParameters(req);
4804
5052
  validateFileParameters(filename, contentType);
4805
5053
  const key = generateS3Key2(filename);
4806
- let fullKey = addGeneralPrefixToKey(key, config);
4807
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5054
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5055
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5056
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4808
5057
  (0, import_s3_request_presigner.getSignedUrl)(
4809
5058
  getS3Client(config),
4810
5059
  new import_client_s3.PutObjectCommand({
@@ -4858,8 +5107,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4858
5107
  return res.status(400).json({ error: "s3: content type must be a string" });
4859
5108
  }
4860
5109
  const key = `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
4861
- let fullKey = addGeneralPrefixToKey(key, config);
4862
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5110
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5111
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5112
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4863
5113
  const params = {
4864
5114
  Bucket: config.fileUploads.s3Bucket,
4865
5115
  Key: fullKey,
@@ -5110,7 +5360,10 @@ var createProjectRetrievalTool = async ({
5110
5360
  };
5111
5361
  var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project) => {
5112
5362
  if (!currentTools) return {};
5113
- if (!allExuluTools) return {};
5363
+ if (!allExuluTools) {
5364
+ allExuluTools = [];
5365
+ }
5366
+ ;
5114
5367
  if (!contexts) {
5115
5368
  contexts = [];
5116
5369
  }
@@ -5144,6 +5397,7 @@ var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, pro
5144
5397
  ...cur.tool,
5145
5398
  description,
5146
5399
  async *execute(inputs, options) {
5400
+ console.log("[EXULU] Executing tool", cur.name, "with inputs", inputs, "and options", options);
5147
5401
  if (!cur.tool?.execute) {
5148
5402
  console.error("[EXULU] Tool execute function is undefined.", cur.tool);
5149
5403
  throw new Error("Tool execute function is undefined.");
@@ -5995,6 +6249,68 @@ var ExuluTool2 = class {
5995
6249
  execute: execute2
5996
6250
  });
5997
6251
  }
6252
+ execute = async ({
6253
+ agent,
6254
+ config,
6255
+ user,
6256
+ inputs,
6257
+ project
6258
+ }) => {
6259
+ const agentInstance = await loadAgent(agent);
6260
+ if (!agentInstance) {
6261
+ throw new Error("Agent not found.");
6262
+ }
6263
+ const { db: db3 } = await postgresClient();
6264
+ let providerapikey;
6265
+ const variableName = agentInstance.providerapikey;
6266
+ if (variableName) {
6267
+ console.log("[EXULU] provider api key variable name", variableName);
6268
+ const variable = await db3.from("variables").where({ name: variableName }).first();
6269
+ if (!variable) {
6270
+ throw new Error("Provider API key variable not found for " + agentInstance.name + " (" + agentInstance.id + ").");
6271
+ }
6272
+ providerapikey = variable.value;
6273
+ if (!variable.encrypted) {
6274
+ throw new Error("Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys.");
6275
+ }
6276
+ if (variable.encrypted) {
6277
+ const bytes = import_crypto_js2.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6278
+ providerapikey = bytes.toString(import_crypto_js2.default.enc.Utf8);
6279
+ }
6280
+ }
6281
+ const tools = await convertToolsArrayToObject(
6282
+ [this],
6283
+ [],
6284
+ agentInstance.tools,
6285
+ providerapikey,
6286
+ void 0,
6287
+ user,
6288
+ config,
6289
+ void 0,
6290
+ void 0,
6291
+ project
6292
+ );
6293
+ const tool2 = tools[sanitizeName(this.name)] || tools[this.name] || tools[this.id];
6294
+ if (!tool2?.execute) {
6295
+ throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
6296
+ }
6297
+ console.log("[EXULU] Tool found", this.name);
6298
+ const generator = tool2.execute(inputs, {
6299
+ toolCallId: this.id + "_" + (0, import_node_crypto2.randomUUID)(),
6300
+ messages: []
6301
+ });
6302
+ let lastValue;
6303
+ for await (const chunk of generator) {
6304
+ lastValue = chunk;
6305
+ }
6306
+ if (typeof lastValue === "string") {
6307
+ lastValue = JSON.parse(lastValue);
6308
+ }
6309
+ if (lastValue?.result && typeof lastValue.result === "string") {
6310
+ lastValue.result = JSON.parse(lastValue.result);
6311
+ }
6312
+ return lastValue;
6313
+ };
5998
6314
  };
5999
6315
  var getTableName = (id) => {
6000
6316
  return sanitizeName(id) + "_items";
@@ -6044,12 +6360,12 @@ var ExuluContext = class {
6044
6360
  name;
6045
6361
  active;
6046
6362
  fields;
6363
+ processor;
6047
6364
  rateLimit;
6048
6365
  description;
6049
6366
  embedder;
6050
6367
  queryRewriter;
6051
6368
  resultReranker;
6052
- // todo typings
6053
6369
  configuration;
6054
6370
  sources = [];
6055
6371
  constructor({
@@ -6057,6 +6373,7 @@ var ExuluContext = class {
6057
6373
  name,
6058
6374
  description,
6059
6375
  embedder,
6376
+ processor,
6060
6377
  active,
6061
6378
  rateLimit,
6062
6379
  fields,
@@ -6069,10 +6386,21 @@ var ExuluContext = class {
6069
6386
  this.name = name;
6070
6387
  this.fields = fields || [];
6071
6388
  this.sources = sources || [];
6389
+ this.processor = processor;
6072
6390
  this.configuration = configuration || {
6073
6391
  calculateVectors: "manual",
6074
6392
  language: "english",
6075
- defaultRightsMode: "private"
6393
+ defaultRightsMode: "private",
6394
+ maxRetrievalResults: 10,
6395
+ expand: {
6396
+ before: 0,
6397
+ after: 0
6398
+ },
6399
+ cutoffs: {
6400
+ cosineDistance: 0.5,
6401
+ tsvector: 0.5,
6402
+ hybrid: 0.5
6403
+ }
6076
6404
  };
6077
6405
  this.description = description;
6078
6406
  this.embedder = embedder;
@@ -6082,26 +6410,35 @@ var ExuluContext = class {
6082
6410
  this.resultReranker = resultReranker;
6083
6411
  }
6084
6412
  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.");
6413
+ console.log("[EXULU] processing item, ", item, " in context", this.id);
6414
+ const exuluStorage = new ExuluStorage({ config: exuluConfig });
6415
+ if (!this.processor) {
6416
+ throw new Error(`Processor is not set for this context: ${this.id}.`);
6087
6417
  }
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);
6418
+ if (this.processor.filter) {
6419
+ const result = await this.processor.filter({
6420
+ item,
6421
+ user,
6422
+ role,
6423
+ utils: {
6424
+ storage: exuluStorage
6425
+ },
6426
+ exuluConfig
6427
+ });
6428
+ if (!result) {
6429
+ return {
6430
+ result: void 0,
6431
+ job: void 0
6432
+ };
6433
+ }
6096
6434
  }
6097
- const exuluStorage = new ExuluStorage({ config: exuluConfig });
6098
- const queue = await field.processor.config?.queue;
6435
+ const queue = await this.processor.config?.queue;
6099
6436
  if (queue?.queue.name) {
6100
6437
  console.log("[EXULU] processor is in queue mode, scheduling job.");
6101
6438
  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}`,
6439
+ timeoutInSeconds: this.processor.config?.timeoutInSeconds || 600,
6440
+ label: `${this.name} ${this.processor.name} data processor`,
6441
+ processor: `${this.id}-${this.processor.name}`,
6105
6442
  context: this.id,
6106
6443
  inputs: item,
6107
6444
  item: item.id,
@@ -6116,12 +6453,12 @@ var ExuluContext = class {
6116
6453
  trigger
6117
6454
  });
6118
6455
  return {
6119
- result: {},
6456
+ result: void 0,
6120
6457
  job: job.id
6121
6458
  };
6122
6459
  }
6123
6460
  console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
6124
- const processorResult = await field.processor.execute({
6461
+ const processorResult = await this.processor.execute({
6125
6462
  item,
6126
6463
  user,
6127
6464
  role,
@@ -6138,7 +6475,8 @@ var ExuluContext = class {
6138
6475
  await db3.from(getTableName(this.id)).where({
6139
6476
  id: processorResult.id
6140
6477
  }).update({
6141
- ...processorResult
6478
+ ...processorResult,
6479
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
6142
6480
  });
6143
6481
  return {
6144
6482
  result: processorResult,
@@ -6152,7 +6490,10 @@ var ExuluContext = class {
6152
6490
  user: options.user,
6153
6491
  role: options.role,
6154
6492
  context: this,
6155
- db: db3
6493
+ db: db3,
6494
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10,
6495
+ cutoffs: options.cutoffs,
6496
+ expand: options.expand
6156
6497
  });
6157
6498
  return result;
6158
6499
  };
@@ -6256,9 +6597,8 @@ var ExuluContext = class {
6256
6597
  console.log("[EXULU] context configuration", this.configuration);
6257
6598
  let jobs = [];
6258
6599
  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;
6600
+ if (this.processor) {
6601
+ const processor = this.processor;
6262
6602
  console.log("[EXULU] Processor found", processor);
6263
6603
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6264
6604
  const {
@@ -6268,8 +6608,7 @@ var ExuluContext = class {
6268
6608
  "api",
6269
6609
  {
6270
6610
  ...item,
6271
- id: results[0].id,
6272
- field: key
6611
+ id: results[0].id
6273
6612
  },
6274
6613
  config,
6275
6614
  user,
@@ -6336,8 +6675,8 @@ var ExuluContext = class {
6336
6675
  await mutation;
6337
6676
  let jobs = [];
6338
6677
  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;
6678
+ if (this.processor) {
6679
+ const processor = this.processor;
6341
6680
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6342
6681
  const {
6343
6682
  job: processorJob,
@@ -6346,8 +6685,7 @@ var ExuluContext = class {
6346
6685
  "api",
6347
6686
  {
6348
6687
  ...item,
6349
- id: record.id,
6350
- field: key
6688
+ id: record.id
6351
6689
  },
6352
6690
  config,
6353
6691
  user,
@@ -6585,22 +6923,26 @@ var ExuluContext = class {
6585
6923
  };
6586
6924
  // Exports the context as a tool that can be used by an agent
6587
6925
  tool = () => {
6926
+ if (this.configuration.enableAsTool === false) {
6927
+ return null;
6928
+ }
6588
6929
  return new ExuluTool2({
6589
6930
  id: this.id,
6590
- name: `${this.name}`,
6931
+ name: `${this.name}_context_search`,
6591
6932
  type: "context",
6592
6933
  category: "contexts",
6593
6934
  inputSchema: import_zod.z.object({
6594
- query: import_zod.z.string()
6935
+ originalQuestion: import_zod.z.string().describe("The original question that the user asked"),
6936
+ 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
6937
  }),
6596
6938
  config: [],
6597
6939
  description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
6598
- execute: async ({ query, user, role }) => {
6940
+ execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
6599
6941
  const { db: db3 } = await postgresClient();
6600
6942
  const result = await vectorSearch({
6601
6943
  page: 1,
6602
- limit: 50,
6603
- query,
6944
+ limit: this.configuration.maxRetrievalResults ?? 10,
6945
+ query: originalQuestion,
6604
6946
  filters: [],
6605
6947
  user,
6606
6948
  role,
@@ -6620,7 +6962,13 @@ var ExuluContext = class {
6620
6962
  role: user?.role?.id
6621
6963
  });
6622
6964
  return {
6623
- items: result.items
6965
+ result: JSON.stringify(result.chunks.map((chunk) => ({
6966
+ ...chunk,
6967
+ context: {
6968
+ name: this.name,
6969
+ id: this.id
6970
+ }
6971
+ })))
6624
6972
  };
6625
6973
  }
6626
6974
  });
@@ -7487,7 +7835,7 @@ var import_ai3 = require("ai");
7487
7835
  var import_crypto_js4 = __toESM(require("crypto-js"), 1);
7488
7836
 
7489
7837
  // src/registry/log-metadata.ts
7490
- function logMetadata2(id, additionalMetadata) {
7838
+ function logMetadata(id, additionalMetadata) {
7491
7839
  return {
7492
7840
  __logMetadata: true,
7493
7841
  id,
@@ -7497,9 +7845,32 @@ function logMetadata2(id, additionalMetadata) {
7497
7845
 
7498
7846
  // src/registry/workers.ts
7499
7847
  var redisConnection;
7848
+ var unhandledRejectionHandlerInstalled = false;
7849
+ var installGlobalErrorHandlers = () => {
7850
+ if (unhandledRejectionHandlerInstalled) return;
7851
+ process.on("unhandledRejection", (reason, promise) => {
7852
+ console.error("[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.", {
7853
+ reason: reason instanceof Error ? reason.message : String(reason),
7854
+ stack: reason instanceof Error ? reason.stack : void 0
7855
+ });
7856
+ });
7857
+ process.on("uncaughtException", (error) => {
7858
+ console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
7859
+ error: error.message,
7860
+ stack: error.stack
7861
+ });
7862
+ if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
7863
+ console.error("[EXULU] Fatal error detected, exiting process.");
7864
+ process.exit(1);
7865
+ }
7866
+ });
7867
+ unhandledRejectionHandlerInstalled = true;
7868
+ console.log("[EXULU] Global error handlers installed to prevent worker crashes");
7869
+ };
7500
7870
  var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
7501
7871
  console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
7502
7872
  console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
7873
+ installGlobalErrorHandlers();
7503
7874
  if (!redisServer.host || !redisServer.port) {
7504
7875
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
7505
7876
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -7524,7 +7895,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7524
7895
  const worker = new import_bullmq4.Worker(
7525
7896
  `${queue.queue.name}`,
7526
7897
  async (bullmqJob) => {
7527
- console.log("[EXULU] starting execution for job", logMetadata2(bullmqJob.name, {
7898
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
7528
7899
  name: bullmqJob.name,
7529
7900
  jobId: bullmqJob.id,
7530
7901
  status: await bullmqJob.getState(),
@@ -7534,9 +7905,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7534
7905
  const data = bullmqJob.data;
7535
7906
  const timeoutInSeconds = data.timeoutInSeconds || 600;
7536
7907
  const timeoutMs = timeoutInSeconds * 1e3;
7908
+ let timeoutHandle;
7537
7909
  const timeoutPromise = new Promise((_, reject) => {
7538
- setTimeout(() => {
7539
- reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`));
7910
+ timeoutHandle = setTimeout(() => {
7911
+ const timeoutError = new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`);
7912
+ console.error(`[EXULU] ${timeoutError.message}`);
7913
+ reject(timeoutError);
7540
7914
  }, timeoutMs);
7541
7915
  });
7542
7916
  const workPromise = (async () => {
@@ -7544,7 +7918,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7544
7918
  console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
7545
7919
  bullmq.validate(bullmqJob.id, data);
7546
7920
  if (data.type === "embedder") {
7547
- console.log("[EXULU] running an embedder job.", logMetadata2(bullmqJob.name));
7921
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
7548
7922
  const label = `embedder-${bullmqJob.name}`;
7549
7923
  await db3.from("job_results").insert({
7550
7924
  job_id: bullmqJob.id,
@@ -7564,12 +7938,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7564
7938
  if (!embedder) {
7565
7939
  throw new Error(`Embedder ${data.embedder} not found in the registry.`);
7566
7940
  }
7567
- const result2 = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
7941
+ const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
7568
7942
  label: embedder.name,
7569
7943
  trigger: data.trigger
7570
7944
  }, data.role, bullmqJob.id);
7571
7945
  return {
7572
- result: result2,
7946
+ result,
7573
7947
  metadata: {}
7574
7948
  };
7575
7949
  }
@@ -7587,21 +7961,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7587
7961
  if (!context) {
7588
7962
  throw new Error(`Context ${data.context} not found in the registry.`);
7589
7963
  }
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
7964
  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}.`);
7965
+ throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
7966
+ }
7967
+ if (!context.processor) {
7968
+ throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
7601
7969
  }
7602
7970
  const exuluStorage = new ExuluStorage({ config });
7603
7971
  console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
7604
- const processorResult = await field.processor.execute({
7972
+ const processorResult = await context.processor.execute({
7605
7973
  item: data.inputs,
7606
7974
  user: data.user,
7607
7975
  role: data.role,
@@ -7611,16 +7979,17 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7611
7979
  exuluConfig: config
7612
7980
  });
7613
7981
  if (!processorResult) {
7614
- throw new Error(`[EXULU] Processor ${field.name} in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7982
+ throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7615
7983
  }
7616
7984
  delete processorResult.field;
7617
7985
  await db3.from(getTableName(context.id)).where({
7618
7986
  id: processorResult.id
7619
7987
  }).update({
7620
- ...processorResult
7988
+ ...processorResult,
7989
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
7621
7990
  });
7622
7991
  let jobs = [];
7623
- if (field.processor.config?.generateEmbeddings) {
7992
+ if (context.processor?.config?.generateEmbeddings) {
7624
7993
  const fullItem = await db3.from(getTableName(context.id)).where({
7625
7994
  id: processorResult.id
7626
7995
  }).first();
@@ -7646,7 +8015,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7646
8015
  };
7647
8016
  }
7648
8017
  if (data.type === "eval_run") {
7649
- console.log("[EXULU] running an eval run job.", logMetadata2(bullmqJob.name));
8018
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
7650
8019
  const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
7651
8020
  const existingResult = await db3.from("job_results").where({ label }).first();
7652
8021
  if (existingResult) {
@@ -7695,7 +8064,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7695
8064
  resolve(messages2);
7696
8065
  break;
7697
8066
  } catch (error) {
7698
- console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata2(bullmqJob.name, {
8067
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
7699
8068
  error: error instanceof Error ? error.message : String(error)
7700
8069
  }));
7701
8070
  attempts++;
@@ -7706,9 +8075,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7706
8075
  }
7707
8076
  }
7708
8077
  });
7709
- const result2 = await promise;
7710
- const messages = result2.messages;
7711
- const metadata = result2.metadata;
8078
+ const result = await promise;
8079
+ const messages = result.messages;
8080
+ const metadata = result.metadata;
7712
8081
  const evalFunctions = evalRun.eval_functions;
7713
8082
  let evalFunctionResults = [];
7714
8083
  for (const evalFunction of evalFunctions) {
@@ -7716,7 +8085,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7716
8085
  if (!evalMethod) {
7717
8086
  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
8087
  }
7719
- let result3;
8088
+ let result2;
7720
8089
  if (evalMethod.queue) {
7721
8090
  const queue2 = await evalMethod.queue;
7722
8091
  const jobData = {
@@ -7747,21 +8116,21 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7747
8116
  if (!job.id) {
7748
8117
  throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
7749
8118
  }
7750
- result3 = await pollJobResult({ queue: queue2, jobId: job.id });
8119
+ result2 = await pollJobResult({ queue: queue2, jobId: job.id });
7751
8120
  const evalFunctionResult = {
7752
8121
  test_case_id: testCase.id,
7753
8122
  eval_run_id: evalRun.id,
7754
8123
  eval_function_id: evalFunction.id,
7755
8124
  eval_function_name: evalFunction.name,
7756
8125
  eval_function_config: evalFunction.config || {},
7757
- result: result3 || 0
8126
+ result: result2 || 0
7758
8127
  };
7759
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result3}`, logMetadata2(bullmqJob.name, {
7760
- result: result3 || 0
8128
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
8129
+ result: result2 || 0
7761
8130
  }));
7762
8131
  evalFunctionResults.push(evalFunctionResult);
7763
8132
  } else {
7764
- result3 = await evalMethod.run(
8133
+ result2 = await evalMethod.run(
7765
8134
  agentInstance,
7766
8135
  agentBackend,
7767
8136
  testCase,
@@ -7772,15 +8141,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7772
8141
  test_case_id: testCase.id,
7773
8142
  eval_run_id: evalRun.id,
7774
8143
  eval_function_id: evalFunction.id,
7775
- result: result3 || 0
8144
+ result: result2 || 0
7776
8145
  };
7777
8146
  evalFunctionResults.push(evalFunctionResult);
7778
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result3}`, logMetadata2(bullmqJob.name, {
7779
- result: result3 || 0
8147
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
8148
+ result: result2 || 0
7780
8149
  }));
7781
8150
  }
7782
8151
  }
7783
- const scores = evalFunctionResults.map((result3) => result3.result);
8152
+ const scores = evalFunctionResults.map((result2) => result2.result);
7784
8153
  console.log("[EXULU] Exulu eval run scores for test case: " + testCase.id, scores);
7785
8154
  let score = 0;
7786
8155
  switch (data.scoring_method?.toLowerCase()) {
@@ -7812,7 +8181,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7812
8181
  };
7813
8182
  }
7814
8183
  if (data.type === "eval_function") {
7815
- console.log("[EXULU] running an eval function job.", logMetadata2(bullmqJob.name));
8184
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
7816
8185
  if (data.eval_functions?.length !== 1) {
7817
8186
  throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
7818
8187
  }
@@ -7845,30 +8214,30 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7845
8214
  messages: inputMessages
7846
8215
  } = await validateEvalPayload(data, agents);
7847
8216
  const evalFunctions = evalRun.eval_functions;
7848
- let result2;
8217
+ let result;
7849
8218
  for (const evalFunction of evalFunctions) {
7850
8219
  const evalMethod = evals.find((e) => e.id === evalFunction.id);
7851
8220
  if (!evalMethod) {
7852
8221
  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
8222
  }
7854
- result2 = await evalMethod.run(
8223
+ result = await evalMethod.run(
7855
8224
  agentInstance,
7856
8225
  backend,
7857
8226
  testCase,
7858
8227
  inputMessages,
7859
8228
  evalFunction.config || {}
7860
8229
  );
7861
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7862
- result: result2 || 0
8230
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
8231
+ result: result || 0
7863
8232
  }));
7864
8233
  }
7865
8234
  return {
7866
- result: result2,
8235
+ result,
7867
8236
  metadata: {}
7868
8237
  };
7869
8238
  }
7870
8239
  if (data.type === "source") {
7871
- console.log("[EXULU] running a source job.", logMetadata2(bullmqJob.name));
8240
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
7872
8241
  if (!data.source) {
7873
8242
  throw new Error(`No source id set for source job.`);
7874
8243
  }
@@ -7883,10 +8252,10 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7883
8252
  if (!source) {
7884
8253
  throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
7885
8254
  }
7886
- const result2 = await source.execute(data.inputs);
8255
+ const result = await source.execute(data.inputs);
7887
8256
  let jobs = [];
7888
8257
  let items = [];
7889
- for (const item of result2) {
8258
+ for (const item of result) {
7890
8259
  const { item: createdItem, job } = await context.createItem(
7891
8260
  item,
7892
8261
  config,
@@ -7896,14 +8265,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7896
8265
  );
7897
8266
  if (job) {
7898
8267
  jobs.push(job);
7899
- console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata2(bullmqJob.name, {
8268
+ console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
7900
8269
  item: createdItem,
7901
8270
  job
7902
8271
  }));
7903
8272
  }
7904
8273
  if (createdItem.id) {
7905
8274
  items.push(createdItem.id);
7906
- console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata2(bullmqJob.name, {
8275
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
7907
8276
  item: createdItem
7908
8277
  }));
7909
8278
  }
@@ -7918,7 +8287,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7918
8287
  role: data?.role
7919
8288
  });
7920
8289
  return {
7921
- result: result2,
8290
+ result,
7922
8291
  metadata: {
7923
8292
  jobs,
7924
8293
  items
@@ -7931,8 +8300,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7931
8300
  throw error;
7932
8301
  }
7933
8302
  })();
7934
- const result = await Promise.race([workPromise, timeoutPromise]);
7935
- return result;
8303
+ try {
8304
+ const result = await Promise.race([workPromise, timeoutPromise]);
8305
+ clearTimeout(timeoutHandle);
8306
+ return result;
8307
+ } catch (error) {
8308
+ clearTimeout(timeoutHandle);
8309
+ console.error(`[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`, error instanceof Error ? error.message : String(error));
8310
+ throw error;
8311
+ }
7936
8312
  },
7937
8313
  {
7938
8314
  autorun: true,
@@ -7967,7 +8343,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7967
8343
  });
7968
8344
  return;
7969
8345
  }
7970
- console.error(`[EXULU] job failed.`, job?.name ? logMetadata2(job.name, {
8346
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
7971
8347
  error: error instanceof Error ? error.message : String(error)
7972
8348
  }) : error);
7973
8349
  });
@@ -7975,7 +8351,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7975
8351
  console.error(`[EXULU] worker error.`, error);
7976
8352
  });
7977
8353
  worker.on("progress", (job, progress) => {
7978
- console.log(`[EXULU] job progress ${job.id}.`, logMetadata2(job.name, {
8354
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
7979
8355
  progress
7980
8356
  }));
7981
8357
  });
@@ -9141,7 +9517,7 @@ var ExuluQueues = class {
9141
9517
  // method of ExuluQueues we need to store the desired rate limit on the queue
9142
9518
  // here so we can use the value when creating workers for the queue instance
9143
9519
  // as there is no way to store a rate limit value natively on a bullm queue.
9144
- register = (name, concurrency, ratelimit = 1) => {
9520
+ register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
9145
9521
  const queueConcurrency = concurrency.queue || 1;
9146
9522
  const workerConcurrency = concurrency.worker || 1;
9147
9523
  const use = async () => {
@@ -9157,7 +9533,8 @@ var ExuluQueues = class {
9157
9533
  concurrency: {
9158
9534
  worker: workerConcurrency,
9159
9535
  queue: queueConcurrency
9160
- }
9536
+ },
9537
+ timeoutInSeconds
9161
9538
  };
9162
9539
  }
9163
9540
  if (!redisServer.host?.length || !redisServer.port?.length) {
@@ -9183,7 +9560,8 @@ var ExuluQueues = class {
9183
9560
  concurrency: {
9184
9561
  worker: workerConcurrency,
9185
9562
  queue: queueConcurrency
9186
- }
9563
+ },
9564
+ timeoutInSeconds
9187
9565
  });
9188
9566
  return {
9189
9567
  queue: newQueue,
@@ -9191,7 +9569,8 @@ var ExuluQueues = class {
9191
9569
  concurrency: {
9192
9570
  worker: workerConcurrency,
9193
9571
  queue: queueConcurrency
9194
- }
9572
+ },
9573
+ timeoutInSeconds
9195
9574
  };
9196
9575
  };
9197
9576
  this.list.set(name, {
@@ -9201,6 +9580,7 @@ var ExuluQueues = class {
9201
9580
  queue: queueConcurrency
9202
9581
  },
9203
9582
  ratelimit,
9583
+ timeoutInSeconds,
9204
9584
  use
9205
9585
  });
9206
9586
  return {
@@ -10266,7 +10646,7 @@ var ExuluApp = class {
10266
10646
  ...[previewPdfTool],
10267
10647
  ...todoTools,
10268
10648
  // Add contexts as tools
10269
- ...Object.values(contexts || {}).map((context) => context.tool())
10649
+ ...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
10270
10650
  // Because agents are stored in the database, we add those as tools
10271
10651
  // at request time, not during ExuluApp initialization. We add them
10272
10652
  // in the grahql tools resolver.
@@ -10428,10 +10808,7 @@ var ExuluApp = class {
10428
10808
  console.warn("[EXULU] No queue configured for source", source.name);
10429
10809
  continue;
10430
10810
  }
10431
- if (queue) {
10432
- if (!source.config?.schedule) {
10433
- throw new Error("Schedule is required for source when configuring a queue: " + source.name);
10434
- }
10811
+ if (queue && source.config?.schedule) {
10435
10812
  console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
10436
10813
  await queue.queue?.upsertJobScheduler(source.id, {
10437
10814
  pattern: source.config?.schedule