@exulu/backend 1.39.3 → 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");
@@ -67,7 +67,6 @@ var redisServer = {
67
67
  // src/redis/client.ts
68
68
  var client = {};
69
69
  async function redisClient() {
70
- console.log("[EXULU] redisServer:", redisServer);
71
70
  if (!redisServer.host || !redisServer.port) {
72
71
  return { client: null };
73
72
  }
@@ -148,7 +147,6 @@ var db = {};
148
147
  var databaseExistsChecked = false;
149
148
  var dbName = process.env.POSTGRES_DB_NAME || "exulu";
150
149
  async function ensureDatabaseExists() {
151
- console.log(`[EXULU] Ensuring ${dbName} database exists...`);
152
150
  const defaultKnex = (0, import_knex.default)({
153
151
  client: "pg",
154
152
  connection: {
@@ -192,16 +190,7 @@ async function ensureDatabaseExists() {
192
190
  async function postgresClient() {
193
191
  if (!db["exulu"]) {
194
192
  try {
195
- console.log(`[EXULU] Connecting to ${dbName} database.`);
196
- console.log("[EXULU] POSTGRES_DB_HOST:", process.env.POSTGRES_DB_HOST);
197
- console.log("[EXULU] POSTGRES_DB_PORT:", process.env.POSTGRES_DB_PORT);
198
- console.log("[EXULU] POSTGRES_DB_USER:", process.env.POSTGRES_DB_USER);
199
- console.log("[EXULU] POSTGRES_DB_PASSWORD:", process.env.POSTGRES_DB_PASSWORD);
200
- console.log("[EXULU] POSTGRES_DB_NAME:", dbName);
201
- console.log("[EXULU] POSTGRES_DB_SSL:", process.env.POSTGRES_DB_SSL);
202
- console.log("[EXULU] Database exists checked:", databaseExistsChecked);
203
193
  if (!databaseExistsChecked) {
204
- console.log(`[EXULU] Ensuring ${dbName} database exists...`);
205
194
  await ensureDatabaseExists();
206
195
  databaseExistsChecked = true;
207
196
  }
@@ -214,21 +203,37 @@ async function postgresClient() {
214
203
  database: dbName,
215
204
  password: process.env.POSTGRES_DB_PASSWORD,
216
205
  ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
217
- 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
218
215
  },
219
216
  pool: {
220
- min: 2,
221
- max: 20,
222
- // Increased from 20 to handle more concurrent operations
223
- 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
224
223
  createTimeoutMillis: 3e4,
225
- idleTimeoutMillis: 3e4,
224
+ idleTimeoutMillis: 6e4,
225
+ // Increased to keep connections alive longer
226
226
  reapIntervalMillis: 1e3,
227
227
  createRetryIntervalMillis: 200,
228
228
  // Log pool events to help debug connection issues
229
229
  afterCreate: (conn, done) => {
230
230
  console.log("[EXULU] New database connection created");
231
- 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
+ });
232
237
  }
233
238
  }
234
239
  });
@@ -526,7 +531,6 @@ var authentication = async ({
526
531
  }
527
532
  if (authtoken) {
528
533
  try {
529
- console.log("[EXULU] authtoken", authtoken);
530
534
  if (!authtoken?.email) {
531
535
  return {
532
536
  error: true,
@@ -1479,6 +1483,10 @@ var addCoreFields = (schema) => {
1479
1483
  field.name = field.name + "_s3key";
1480
1484
  }
1481
1485
  });
1486
+ schema.fields.push({
1487
+ name: "last_processed_at",
1488
+ type: "date"
1489
+ });
1482
1490
  if (schema.RBAC) {
1483
1491
  if (!schema.fields.some((field) => field.name === "rights_mode")) {
1484
1492
  schema.fields.push({
@@ -2361,33 +2369,38 @@ function createMutations(table, agents, contexts, tools, config) {
2361
2369
  }
2362
2370
  };
2363
2371
  if (table.type === "items") {
2364
- if (table.fields.some((field) => field.processor?.execute)) {
2365
- 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) => {
2366
2398
  if (!context.user?.super_admin) {
2367
2399
  throw new Error("You are not authorized to process fields via API, user must be super admin.");
2368
2400
  }
2369
- const exists = contexts.find((context2) => context2.id === table.id);
2370
- if (!exists) {
2371
- throw new Error(`Context ${table.id} not found.`);
2372
- }
2373
- if (!args.field) {
2374
- throw new Error("Field argument missing, the field argument is required.");
2375
- }
2376
2401
  if (!args.item) {
2377
2402
  throw new Error("Item argument missing, the item argument is required.");
2378
2403
  }
2379
- const name = args.field?.replace("_s3key", "");
2380
- console.log("[EXULU] name", name);
2381
- console.log("[EXULU] fields", exists.fields.map((field2) => field2.name));
2382
- const field = exists.fields.find((field2) => {
2383
- return field2.name.replace("_s3key", "") === name;
2384
- });
2385
- if (!field) {
2386
- throw new Error(`Field ${name} not found in context ${exists.id}].`);
2387
- }
2388
- if (!field.processor) {
2389
- throw new Error(`Processor not set for field ${args.field} in context ${exists.id}.`);
2390
- }
2391
2404
  const { db: db3 } = context;
2392
2405
  let query = db3.from(tableNamePlural).select("*").where({ id: args.item });
2393
2406
  query = applyAccessControl(table, query, context.user);
@@ -2395,21 +2408,38 @@ function createMutations(table, agents, contexts, tools, config) {
2395
2408
  if (!item) {
2396
2409
  throw new Error("Item not found, or your user does not have access to it.");
2397
2410
  }
2398
- const { job, result } = await exists.processField(
2399
- "api",
2400
- {
2401
- ...item,
2402
- field: args.field
2403
- },
2404
- 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,
2405
2440
  context.user.id,
2406
2441
  context.user.role?.id
2407
2442
  );
2408
- return {
2409
- message: job ? "Processing job scheduled." : "Item processed successfully.",
2410
- result,
2411
- job
2412
- };
2413
2443
  };
2414
2444
  }
2415
2445
  mutations[`${tableNameSingular}ExecuteSource`] = async (_, args, context, info) => {
@@ -2577,7 +2607,7 @@ function createMutations(table, agents, contexts, tools, config) {
2577
2607
  }
2578
2608
  return mutations;
2579
2609
  }
2580
- var applyAccessControl = (table, query, user) => {
2610
+ var applyAccessControl = (table, query, user, field_prefix) => {
2581
2611
  const tableNamePlural = table.name.plural.toLowerCase();
2582
2612
  if (table.name.plural !== "agent_sessions" && user?.super_admin === true) {
2583
2613
  return query;
@@ -2593,18 +2623,19 @@ var applyAccessControl = (table, query, user) => {
2593
2623
  if (!hasRBAC) {
2594
2624
  return query;
2595
2625
  }
2626
+ const prefix = field_prefix ? field_prefix + "." : "";
2596
2627
  try {
2597
2628
  query = query.where(function() {
2598
- this.where("rights_mode", "public");
2599
- this.orWhere("created_by", user.id);
2629
+ this.where(`${prefix}rights_mode`, "public");
2630
+ this.orWhere(`${prefix}created_by`, user.id);
2600
2631
  this.orWhere(function() {
2601
- this.where("rights_mode", "users").whereExists(function() {
2632
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2602
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);
2603
2634
  });
2604
2635
  });
2605
2636
  if (user.role) {
2606
2637
  this.orWhere(function() {
2607
- this.where("rights_mode", "roles").whereExists(function() {
2638
+ this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2608
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);
2609
2640
  });
2610
2641
  });
@@ -2616,9 +2647,11 @@ var applyAccessControl = (table, query, user) => {
2616
2647
  }
2617
2648
  return query;
2618
2649
  };
2619
- var converOperatorToQuery = (query, fieldName, operators, table) => {
2650
+ var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
2620
2651
  const field = table?.fields.find((f) => f.name === fieldName);
2621
2652
  const isJsonField = field?.type === "json";
2653
+ const prefix = field_prefix ? field_prefix + "." : "";
2654
+ fieldName = prefix + fieldName;
2622
2655
  if (operators.eq !== void 0) {
2623
2656
  if (isJsonField) {
2624
2657
  query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
@@ -2928,53 +2961,95 @@ var finalizeRequestedFields = async ({
2928
2961
  }
2929
2962
  const { db: db3 } = await postgresClient();
2930
2963
  const query = db3.from(getChunksTableName(context.id)).where({ source: result.id }).select("id", "content", "source", "chunk_index", "createdAt", "updatedAt");
2931
- query.select(
2932
- db3.raw("vector_dims(??) as embedding_size", [`embedding`])
2933
- );
2934
2964
  const chunks = await query;
2935
2965
  result.chunks = chunks.map((chunk) => ({
2936
- cosine_distance: 0,
2937
- fts_rank: 0,
2938
- hybrid_score: 0,
2939
- content: chunk.content,
2940
- source: chunk.source,
2966
+ chunk_content: chunk.content,
2967
+ chunk_source: chunk.source,
2941
2968
  chunk_index: chunk.chunk_index,
2942
2969
  chunk_id: chunk.id,
2943
2970
  chunk_created_at: chunk.createdAt,
2944
2971
  chunk_updated_at: chunk.updatedAt,
2945
- 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
2946
2977
  }));
2947
2978
  }
2948
2979
  }
2949
2980
  }
2950
2981
  return result;
2951
2982
  };
2952
- var applyFilters = (query, filters, table) => {
2983
+ var applyFilters = (query, filters, table, field_prefix) => {
2953
2984
  filters.forEach((filter) => {
2954
2985
  Object.entries(filter).forEach(([fieldName, operators]) => {
2955
2986
  if (operators) {
2956
2987
  if (operators.and !== void 0) {
2957
2988
  operators.and.forEach((operator) => {
2958
- query = converOperatorToQuery(query, fieldName, operator, table);
2989
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2959
2990
  });
2960
2991
  }
2961
2992
  if (operators.or !== void 0) {
2962
2993
  operators.or.forEach((operator) => {
2963
- query = converOperatorToQuery(query, fieldName, operator, table);
2994
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2964
2995
  });
2965
2996
  }
2966
- query = converOperatorToQuery(query, fieldName, operators, table);
2997
+ query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
2967
2998
  }
2968
2999
  });
2969
3000
  });
2970
3001
  return query;
2971
3002
  };
2972
- var applySorting = (query, sort) => {
3003
+ var applySorting = (query, sort, field_prefix) => {
3004
+ const prefix = field_prefix ? field_prefix + "." : "";
2973
3005
  if (sort) {
3006
+ sort.field = prefix + sort.field;
2974
3007
  query = query.orderBy(sort.field, sort.direction.toLowerCase());
2975
3008
  }
2976
3009
  return query;
2977
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
+ };
2978
3053
  function createQueries(table, agents, tools, contexts) {
2979
3054
  const tableNamePlural = table.name.plural.toLowerCase();
2980
3055
  const tableNameSingular = table.name.singular.toLowerCase();
@@ -3010,38 +3085,22 @@ function createQueries(table, agents, tools, contexts) {
3010
3085
  return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
3011
3086
  },
3012
3087
  [`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
3013
- const { limit = 10, page = 0, filters = [], sort } = args;
3014
3088
  const { db: db3 } = context;
3015
- if (limit > 500) {
3016
- throw new Error("Limit cannot be greater than 500.");
3017
- }
3018
- let countQuery = db3(tableNamePlural);
3019
- countQuery = applyFilters(countQuery, filters, table);
3020
- countQuery = applyAccessControl(table, countQuery, context.user);
3021
- const countResult = await countQuery.count("* as count");
3022
- const itemCount = Number(countResult[0]?.count || 0);
3023
- const pageCount = Math.ceil(itemCount / limit);
3024
- const currentPage = page;
3025
- const hasPreviousPage = currentPage > 1;
3026
- const hasNextPage = currentPage < pageCount - 1;
3027
- let dataQuery = db3(tableNamePlural);
3028
- dataQuery = applyFilters(dataQuery, filters, table);
3029
- dataQuery = applyAccessControl(table, dataQuery, context.user);
3089
+ const { limit = 10, page = 0, filters = [], sort } = args;
3030
3090
  const requestedFields = getRequestedFields(info);
3031
- dataQuery = applySorting(dataQuery, sort);
3032
- if (page > 1) {
3033
- dataQuery = dataQuery.offset((page - 1) * limit);
3034
- }
3035
3091
  const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
3036
- 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
+ });
3037
3102
  return {
3038
- pageInfo: {
3039
- pageCount,
3040
- itemCount,
3041
- currentPage,
3042
- hasPreviousPage,
3043
- hasNextPage
3044
- },
3103
+ pageInfo,
3045
3104
  items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
3046
3105
  };
3047
3106
  },
@@ -3090,7 +3149,7 @@ function createQueries(table, agents, tools, contexts) {
3090
3149
  }
3091
3150
  const { limit = 10, page = 0, filters = [], sort } = args;
3092
3151
  return await vectorSearch({
3093
- limit,
3152
+ limit: limit || exists.configuration.maxRetrievalResults || 10,
3094
3153
  page,
3095
3154
  filters,
3096
3155
  sort,
@@ -3149,106 +3208,111 @@ var vectorSearch = async ({
3149
3208
  }
3150
3209
  const mainTable = getTableName(id);
3151
3210
  const chunksTable = getChunksTableName(id);
3152
- let countQuery = db3(mainTable);
3153
- countQuery = applyFilters(countQuery, filters, table);
3154
- countQuery = applyAccessControl(table, countQuery, user);
3155
- const columns = await db3(mainTable).columnInfo();
3156
- let itemsQuery = db3(mainTable).select(Object.keys(columns).map((column) => mainTable + "." + column));
3157
- itemsQuery = applyFilters(itemsQuery, filters, table);
3158
- itemsQuery = applyAccessControl(table, itemsQuery, user);
3159
- 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");
3160
3231
  if (queryRewriter) {
3161
3232
  query = await queryRewriter(query);
3162
3233
  }
3163
- itemsQuery.limit(limit * 3);
3164
- itemsQuery.leftJoin(chunksTable, function() {
3165
- this.on(chunksTable + ".source", "=", mainTable + ".id");
3166
- });
3167
- itemsQuery.select(chunksTable + ".id as chunk_id");
3168
- itemsQuery.select(chunksTable + ".source");
3169
- itemsQuery.select(chunksTable + ".content");
3170
- itemsQuery.select(chunksTable + ".chunk_index");
3171
- itemsQuery.select(chunksTable + ".createdAt as chunk_created_at");
3172
- itemsQuery.select(chunksTable + ".updatedAt as chunk_updated_at");
3173
- itemsQuery.select(db3.raw("vector_dims(??) as embedding_size", [`${chunksTable}.embedding`]));
3174
- const { chunks } = await embedder.generateFromQuery(context.id, query, {
3234
+ const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
3175
3235
  label: table.name.singular,
3176
3236
  trigger
3177
3237
  }, user?.id, role);
3178
- if (!chunks?.[0]?.vector) {
3238
+ if (!queryChunks?.[0]?.vector) {
3179
3239
  throw new Error("No vector generated for query.");
3180
3240
  }
3181
- const vector = chunks[0].vector;
3241
+ const vector = queryChunks[0].vector;
3182
3242
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3183
3243
  const vectorExpr = `${vectorStr}::vector`;
3184
3244
  const language = configuration.language || "english";
3185
- let items = [];
3245
+ let resultChunks = [];
3186
3246
  switch (method) {
3187
3247
  case "tsvector":
3188
- itemsQuery.select(db3.raw(
3189
- `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`,
3190
3251
  [language, query]
3191
3252
  )).whereRaw(
3192
- `${chunksTable}.fts @@ websearch_to_tsquery(?, ?)`,
3253
+ `chunks.fts @@ websearch_to_tsquery(?, ?)`,
3193
3254
  [language, query]
3194
3255
  ).orderByRaw(`fts_rank DESC`);
3195
- items = await itemsQuery;
3256
+ resultChunks = await chunksQuery;
3196
3257
  break;
3197
3258
  case "cosineDistance":
3198
- default:
3199
- itemsQuery.whereNotNull(`${chunksTable}.embedding`);
3200
- itemsQuery.select(
3201
- 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`)
3202
3264
  );
3203
- itemsQuery.orderByRaw(
3204
- `${chunksTable}.embedding <=> ${vectorExpr} ASC NULLS LAST`
3265
+ chunksQuery.orderByRaw(
3266
+ `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3205
3267
  );
3206
- items = await itemsQuery;
3268
+ resultChunks = await chunksQuery;
3207
3269
  break;
3208
3270
  case "hybridSearch":
3209
- const matchCount = Math.min(limit * 5, 100);
3271
+ const matchCount = Math.min(limit * 2, 100);
3210
3272
  const fullTextWeight = 1;
3211
3273
  const semanticWeight = 1;
3212
3274
  const rrfK = 50;
3213
3275
  const hybridSQL = `
3214
3276
  WITH full_text AS (
3215
3277
  SELECT
3216
- c.id,
3217
- c.source,
3278
+ chunks.id,
3279
+ chunks.source,
3218
3280
  row_number() OVER (
3219
- ORDER BY ts_rank_cd(c.fts, websearch_to_tsquery(?, ?)) DESC
3281
+ ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
3220
3282
  ) AS rank_ix
3221
- FROM ${chunksTable} c
3222
- WHERE c.fts @@ websearch_to_tsquery(?, ?)
3283
+ FROM ${chunksTable} as chunks
3284
+ WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
3223
3285
  ORDER BY rank_ix
3224
3286
  LIMIT LEAST(?, 15) * 2
3225
3287
  ),
3226
3288
  semantic AS (
3227
3289
  SELECT
3228
- c.id,
3229
- c.source,
3290
+ chunks.id,
3291
+ chunks.source,
3230
3292
  row_number() OVER (
3231
- ORDER BY c.embedding <=> ${vectorExpr} ASC
3293
+ ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3232
3294
  ) AS rank_ix
3233
- FROM ${chunksTable} c
3234
- WHERE c.embedding IS NOT NULL
3295
+ FROM ${chunksTable} as chunks
3296
+ WHERE chunks.embedding IS NOT NULL
3235
3297
  ORDER BY rank_ix
3236
3298
  LIMIT LEAST(?, 50) * 2
3237
3299
  )
3238
3300
  SELECT
3239
- m.*,
3240
- c.id AS chunk_id,
3241
- c.source,
3242
- c.content,
3243
- c.chunk_index,
3244
- c.metadata,
3245
- c."createdAt" AS chunk_created_at,
3246
- c."updatedAt" AS chunk_updated_at,
3247
- vector_dims(c.embedding) as embedding_size,
3248
-
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,
3249
3313
  /* Per-signal scores for introspection */
3250
- ts_rank(c.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3251
- (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,
3252
3316
 
3253
3317
  /* Hybrid RRF score */
3254
3318
  (
@@ -3260,10 +3324,10 @@ var vectorSearch = async ({
3260
3324
  FROM full_text ft
3261
3325
  FULL OUTER JOIN semantic se
3262
3326
  ON ft.id = se.id
3263
- JOIN ${chunksTable} c
3264
- ON COALESCE(ft.id, se.id) = c.id
3265
- JOIN ${mainTable} m
3266
- 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
3267
3331
  ORDER BY hybrid_score DESC
3268
3332
  LIMIT LEAST(?, 50)
3269
3333
  OFFSET 0
@@ -3289,86 +3353,57 @@ var vectorSearch = async ({
3289
3353
  matchCount
3290
3354
  // final limit
3291
3355
  ];
3292
- items = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3293
- }
3294
- console.log("[EXULU] Vector search results:", items?.length);
3295
- const seenSources = /* @__PURE__ */ new Map();
3296
- items = items.reduce((acc, item) => {
3297
- if (!seenSources.has(item.source)) {
3298
- seenSources.set(item.source, {
3299
- ...Object.fromEntries(
3300
- Object.keys(item).filter(
3301
- (key) => key !== "cosine_distance" && // kept per chunk below
3302
- key !== "fts_rank" && // kept per chunk below
3303
- key !== "hybrid_score" && // we will compute per item below
3304
- key !== "content" && key !== "source" && key !== "chunk_index" && key !== "chunk_id" && key !== "chunk_created_at" && key !== "chunk_updated_at" && key !== "embedding_size"
3305
- ).map((key) => [key, item[key]])
3306
- ),
3307
- chunks: [{
3308
- content: item.content,
3309
- chunk_index: item.chunk_index,
3310
- chunk_id: item.chunk_id,
3311
- source: item.source,
3312
- metadata: item.metadata,
3313
- chunk_created_at: item.chunk_created_at,
3314
- chunk_updated_at: item.chunk_updated_at,
3315
- embedding_size: item.embedding_size,
3316
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3317
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3318
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3319
- }]
3320
- });
3321
- acc.push(seenSources.get(item.source));
3322
- } else {
3323
- seenSources.get(item.source).chunks.push({
3324
- content: item.content,
3325
- chunk_index: item.chunk_index,
3326
- chunk_id: item.chunk_id,
3327
- chunk_created_at: item.chunk_created_at,
3328
- embedding_size: item.embedding_size,
3329
- metadata: item.metadata,
3330
- source: item.source,
3331
- chunk_updated_at: item.chunk_updated_at,
3332
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3333
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3334
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3335
- });
3336
- }
3337
- return acc;
3338
- }, []);
3339
- console.log("[EXULU] Vector search results after deduplication:", items?.length);
3340
- items.forEach((item) => {
3341
- if (!item.chunks?.length) {
3342
- return;
3343
- }
3344
- if (method === "tsvector") {
3345
- const ranks = item.chunks.map((c) => typeof c.fts_rank === "number" ? c.fts_rank : 0);
3346
- const total = ranks.reduce((a, b) => a + b, 0);
3347
- const average = ranks.length ? total / ranks.length : 0;
3348
- item.averageRelevance = average;
3349
- item.totalRelevance = total;
3350
- } else if (method === "cosineDistance") {
3351
- let methodProperty = "cosine_distance";
3352
- const average = item.chunks.reduce((acc, item2) => {
3353
- return acc + item2[methodProperty];
3354
- }, 0) / item.chunks.length;
3355
- const total = item.chunks.reduce((acc, item2) => {
3356
- return acc + item2[methodProperty];
3357
- }, 0);
3358
- item.averageRelevance = average;
3359
- item.totalRelevance = total;
3360
- } else if (method === "hybridSearch") {
3361
- const scores = item.chunks.map((c) => typeof c.hybrid_score === "number" ? c.hybrid_score * 10 + 1 : 0);
3362
- const total = scores.reduce((a, b) => a + b, 0);
3363
- const average = scores.length ? total / scores.length : 0;
3364
- item.averageRelevance = average;
3365
- 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)})`);
3366
3401
  }
3367
- });
3402
+ }
3368
3403
  if (resultReranker && query) {
3369
- items = await resultReranker(items);
3404
+ resultChunks = await resultReranker(resultChunks);
3370
3405
  }
3371
- console.log("[EXULU] Vector search results after slicing:", items?.length);
3406
+ resultChunks = resultChunks.slice(0, limit);
3372
3407
  await updateStatistic({
3373
3408
  name: "count",
3374
3409
  label: table.name.singular,
@@ -3386,7 +3421,7 @@ var vectorSearch = async ({
3386
3421
  id: table.id || "",
3387
3422
  embedder: embedder.name
3388
3423
  },
3389
- items
3424
+ chunks: resultChunks
3390
3425
  };
3391
3426
  };
3392
3427
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3416,10 +3451,10 @@ var contextToTableDefinition = (context) => {
3416
3451
  plural: tableName?.endsWith("s") ? tableName : tableName + "s"
3417
3452
  },
3418
3453
  RBAC: true,
3454
+ processor: context.processor,
3419
3455
  fields: context.fields.map((field) => ({
3420
3456
  name: sanitizeName(field.name),
3421
3457
  type: field.type,
3422
- processor: field.processor,
3423
3458
  required: field.required,
3424
3459
  default: field.default,
3425
3460
  index: field.index,
@@ -3549,7 +3584,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3549
3584
  const tableNamePlural = table.name.plural.toLowerCase();
3550
3585
  const tableNameSingular = table.name.singular.toLowerCase();
3551
3586
  const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
3552
- const processorFields = table.fields.filter((field) => field.processor?.execute);
3553
3587
  typeDefs += `
3554
3588
  ${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
3555
3589
 
@@ -3576,9 +3610,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3576
3610
  ${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
3577
3611
  ${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
3578
3612
  `;
3579
- if (processorFields?.length > 0) {
3613
+ if (table.processor) {
3580
3614
  mutationDefs += `
3581
- ${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
3582
3617
  `;
3583
3618
  }
3584
3619
  modelDefs += `
@@ -3596,8 +3631,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3596
3631
 
3597
3632
  type ${tableNameSingular}ProcessItemFieldReturnPayload {
3598
3633
  message: String!
3599
- result: String!
3600
- job: String
3634
+ results: [String]
3635
+ jobs: [String]
3601
3636
  }
3602
3637
 
3603
3638
  type ${tableNameSingular}DeleteChunksReturnPayload {
@@ -3612,20 +3647,31 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3612
3647
  tsvector
3613
3648
  }
3614
3649
 
3615
- ${processorFields.length > 0 ? `
3616
- enum ${tableNameSingular}ProcessorFieldEnum {
3617
- ${processorFields.map((field) => field.name).join("\n")}
3618
- }
3619
- ` : ""}
3620
-
3621
-
3622
- type ${tableNameSingular}VectorSearchResult {
3623
- items: [${tableNameSingular}]!
3650
+ type ${tableNameSingular}VectorSearchResult {
3651
+ chunks: [${tableNameSingular}VectorSearchChunk!]!
3624
3652
  context: VectoSearchResultContext!
3625
3653
  filters: JSON!
3626
3654
  query: String!
3627
3655
  method: VectorMethodEnum!
3628
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
+ }
3629
3675
 
3630
3676
  type VectoSearchResultContext {
3631
3677
  name: String!
@@ -3730,7 +3776,11 @@ type PageInfo {
3730
3776
  const config2 = await queue.use();
3731
3777
  return {
3732
3778
  name: config2.queue.name,
3733
- concurrency: config2.concurrency,
3779
+ concurrency: {
3780
+ worker: config2.concurrency?.worker || void 0,
3781
+ queue: config2.concurrency?.queue || void 0
3782
+ },
3783
+ timeoutInSeconds: config2.timeoutInSeconds,
3734
3784
  ratelimit: config2.ratelimit,
3735
3785
  isMaxed: await config2.queue.isMaxed(),
3736
3786
  isPaused: await config2.queue.isPaused(),
@@ -3780,7 +3830,10 @@ type PageInfo {
3780
3830
  if (!agentInstance) {
3781
3831
  throw new Error("Agent instance not found for eval run.");
3782
3832
  }
3783
- const evalQueue = await queues.register("eval_runs", 1, 1).use();
3833
+ const evalQueue = await queues.register("eval_runs", {
3834
+ worker: 1,
3835
+ queue: 1
3836
+ }, 1).use();
3784
3837
  const jobIds = [];
3785
3838
  for (const testCase of testCases) {
3786
3839
  const jobData = {
@@ -3904,7 +3957,6 @@ type PageInfo {
3904
3957
  if (!client2) {
3905
3958
  throw new Error("Redis client not created properly");
3906
3959
  }
3907
- console.log("[EXULU] Jobs pagination args", args);
3908
3960
  const {
3909
3961
  jobs,
3910
3962
  count
@@ -3914,7 +3966,6 @@ type PageInfo {
3914
3966
  args.page || 1,
3915
3967
  args.limit || 100
3916
3968
  );
3917
- console.log("[EXULU] jobs", jobs.map((job) => job.name));
3918
3969
  const requestedFields = getRequestedFields(info);
3919
3970
  return {
3920
3971
  items: await Promise.all(jobs.map(async (job) => {
@@ -3943,6 +3994,21 @@ type PageInfo {
3943
3994
  };
3944
3995
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3945
3996
  const data = await Promise.all(contexts.map(async (context2) => {
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
+ }
3946
4012
  const sources = await Promise.all(context2.sources.map(async (source) => {
3947
4013
  let queueName = void 0;
3948
4014
  if (source.config) {
@@ -3974,6 +4040,7 @@ type PageInfo {
3974
4040
  slug: "/contexts/" + context2.id,
3975
4041
  active: context2.active,
3976
4042
  sources,
4043
+ processor,
3977
4044
  fields: context2.fields.map((field) => {
3978
4045
  return {
3979
4046
  ...field,
@@ -3999,6 +4066,21 @@ type PageInfo {
3999
4066
  if (!data) {
4000
4067
  return null;
4001
4068
  }
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
+ }
4002
4084
  const sources = await Promise.all(data.sources.map(async (source) => {
4003
4085
  let queueName = void 0;
4004
4086
  if (source.config) {
@@ -4035,35 +4117,18 @@ type PageInfo {
4035
4117
  slug: "/contexts/" + data.id,
4036
4118
  active: data.active,
4037
4119
  sources,
4120
+ processor,
4038
4121
  fields: await Promise.all(data.fields.map(async (field) => {
4039
4122
  const label = field.name?.replace("_s3key", "");
4040
4123
  if (field.type === "file" && !field.name.endsWith("_s3key")) {
4041
4124
  field.name = field.name + "_s3key";
4042
4125
  }
4043
- let queue = null;
4044
- if (field.processor?.config?.queue) {
4045
- queue = await field.processor.config.queue;
4046
- }
4047
4126
  return {
4048
4127
  ...field,
4049
4128
  name: sanitizeName(field.name),
4050
4129
  ...field.type === "file" ? {
4051
4130
  allowedFileTypes: field.allowedFileTypes
4052
4131
  } : {},
4053
- ...field.processor ? {
4054
- processor: {
4055
- description: field.processor?.description,
4056
- config: {
4057
- trigger: field.processor?.config?.trigger,
4058
- queue: {
4059
- name: queue?.queue.name || void 0,
4060
- ratelimit: queue?.ratelimit || void 0,
4061
- concurrency: queue?.concurrency || void 0
4062
- }
4063
- },
4064
- execute: "function"
4065
- }
4066
- } : {},
4067
4132
  label
4068
4133
  };
4069
4134
  })),
@@ -4131,13 +4196,20 @@ type PageInfo {
4131
4196
  modelDefs += `
4132
4197
  type QueueResult {
4133
4198
  name: String!
4134
- concurrency: Int!
4199
+ concurrency: QueueConcurrency!
4200
+ timeoutInSeconds: Int!
4135
4201
  ratelimit: Int!
4136
4202
  isMaxed: Boolean!
4137
4203
  isPaused: Boolean!
4138
4204
  jobs: QueueJobsCounts
4139
4205
  }
4140
4206
  `;
4207
+ modelDefs += `
4208
+ type QueueConcurrency {
4209
+ worker: Int
4210
+ queue: Int
4211
+ }
4212
+ `;
4141
4213
  modelDefs += `
4142
4214
  type QueueJobsCounts {
4143
4215
  paused: Int!
@@ -4207,17 +4279,12 @@ type AgentEvalFunctionConfig {
4207
4279
  }
4208
4280
 
4209
4281
  type ItemChunks {
4210
- cosine_distance: Float
4211
- fts_rank: Float
4212
- hybrid_score: Float
4213
- content: String
4214
- source: ID
4215
- chunk_index: Int
4216
- chunk_id: ID
4217
- chunk_created_at: Date
4218
- chunk_updated_at: Date
4219
- embedding_size: Float
4220
- 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!
4221
4288
  }
4222
4289
 
4223
4290
  type Provider {
@@ -4252,7 +4319,8 @@ type Context {
4252
4319
  active: Boolean
4253
4320
  fields: JSON
4254
4321
  configuration: JSON
4255
- sources: [ContextSource!]
4322
+ sources: [ContextSource]
4323
+ processor: ContextProcessor
4256
4324
  }
4257
4325
  type Embedder {
4258
4326
  name: String!
@@ -4265,6 +4333,14 @@ type EmbedderConfig {
4265
4333
  description: String
4266
4334
  default: String
4267
4335
  }
4336
+ type ContextProcessor {
4337
+ name: String!
4338
+ description: String
4339
+ queue: String
4340
+ trigger: String
4341
+ timeoutInSeconds: Int
4342
+ generateEmbeddings: Boolean
4343
+ }
4268
4344
 
4269
4345
  type ContextSource {
4270
4346
  id: String!
@@ -4322,6 +4398,9 @@ type Job {
4322
4398
  name: String!
4323
4399
  returnvalue: JSON
4324
4400
  stacktrace: [String]
4401
+ finishedOn: Date
4402
+ processedOn: Date
4403
+ attemptsMade: Int
4325
4404
  failedReason: String
4326
4405
  state: String!
4327
4406
  data: JSON
@@ -4383,10 +4462,7 @@ async function getJobsByQueueName(queueName, statusses, page, limit) {
4383
4462
  const config = await queue.use();
4384
4463
  const startIndex = (page || 1) - 1;
4385
4464
  const endIndex = startIndex - 1 + (limit || 100);
4386
- console.log("[EXULU] Jobs pagination startIndex", startIndex);
4387
- console.log("[EXULU] Jobs pagination endIndex", endIndex);
4388
4465
  const jobs = await config.queue.getJobs(statusses || [], startIndex, endIndex, false);
4389
- console.log("[EXULU] Jobs pagination jobs", jobs?.length);
4390
4466
  const counts = await config.queue.getJobCounts(...statusses || []);
4391
4467
  let total = 0;
4392
4468
  if (counts) {
@@ -4427,21 +4503,12 @@ function getS3Client(config) {
4427
4503
  });
4428
4504
  return s3Client;
4429
4505
  }
4430
- var getPresignedUrl = async (key, config) => {
4506
+ var getPresignedUrl = async (bucket, key, config) => {
4431
4507
  if (!config.fileUploads) {
4432
4508
  throw new Error("File uploads are not configured");
4433
4509
  }
4434
- let bucket = config.fileUploads.s3Bucket;
4435
- if (key.includes("[bucket:")) {
4436
- console.log("[EXULU] key includes [bucket:name]", key);
4437
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4438
- if (!bucket?.length) {
4439
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4440
- }
4441
- key = key.split("]")[1] || "";
4442
- console.log("[EXULU] bucket", bucket);
4443
- console.log("[EXULU] key", key);
4444
- }
4510
+ console.log("[EXULU] getting presigned url for bucket", bucket);
4511
+ console.log("[EXULU] getting presigned url for key", key);
4445
4512
  const url = await (0, import_s3_request_presigner.getSignedUrl)(
4446
4513
  getS3Client(config),
4447
4514
  new import_client_s3.GetObjectCommand({
@@ -4452,7 +4519,7 @@ var getPresignedUrl = async (key, config) => {
4452
4519
  );
4453
4520
  return url;
4454
4521
  };
4455
- var addPrefixToKey = (keyPath, config) => {
4522
+ var addGeneralPrefixToKey = (keyPath, config) => {
4456
4523
  if (!config.fileUploads) {
4457
4524
  throw new Error("File uploads are not configured");
4458
4525
  }
@@ -4465,58 +4532,50 @@ var addPrefixToKey = (keyPath, config) => {
4465
4532
  }
4466
4533
  return `${prefix}/${keyPath}`;
4467
4534
  };
4468
- var uploadFile = async (file, key, config, options = {}, user) => {
4535
+ var addUserPrefixToKey = (key, user) => {
4536
+ if (!user) {
4537
+ return key;
4538
+ }
4539
+ if (key.includes(`/user_${user}/`)) {
4540
+ return key;
4541
+ }
4542
+ return `user_${user}/${key}`;
4543
+ };
4544
+ var addBucketPrefixToKey = (key, bucket) => {
4545
+ if (key.includes(`/${bucket}/`)) {
4546
+ return key;
4547
+ }
4548
+ return `${bucket}/${key}`;
4549
+ };
4550
+ var uploadFile = async (file, fileName, config, options = {}, user, customBucket) => {
4469
4551
  if (!config.fileUploads) {
4470
4552
  throw new Error("File uploads are not configured (in the exported uploadFile function)");
4471
4553
  }
4472
4554
  const client2 = getS3Client(config);
4473
4555
  let defaultBucket = config.fileUploads.s3Bucket;
4474
- let customBucket = false;
4475
- if (key.includes("[bucket:")) {
4476
- console.log("[EXULU] key includes [bucket:name]", key);
4477
- customBucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4478
- if (!customBucket?.length) {
4479
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4480
- }
4481
- key = key.split("]")[1] || "";
4482
- console.log("[EXULU] custom bucket", customBucket);
4483
- }
4484
- let folder = user ? `${user}/` : "";
4485
- const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
4486
- console.log("[EXULU] uploading file to s3 into bucket", customBucket || defaultBucket, "with key", fullKey);
4556
+ let key = fileName;
4557
+ key = addGeneralPrefixToKey(key, config);
4558
+ key = addUserPrefixToKey(key, user || "api");
4559
+ console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
4487
4560
  const command = new import_client_s3.PutObjectCommand({
4488
4561
  Bucket: customBucket || defaultBucket,
4489
- Key: fullKey,
4562
+ Key: key,
4490
4563
  Body: file,
4491
4564
  ContentType: options.contentType,
4492
4565
  Metadata: options.metadata,
4493
4566
  ContentLength: file.byteLength
4494
4567
  });
4495
4568
  await client2.send(command);
4496
- console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", fullKey);
4497
- if (customBucket) {
4498
- return "[bucket:" + customBucket + "]" + fullKey;
4499
- }
4500
- return fullKey;
4569
+ console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
4570
+ return addBucketPrefixToKey(
4571
+ key,
4572
+ customBucket || defaultBucket
4573
+ );
4501
4574
  };
4502
- var createUppyRoutes = async (app, config) => {
4575
+ var createUppyRoutes = async (app, contexts, config) => {
4503
4576
  if (!config.fileUploads) {
4504
4577
  throw new Error("File uploads are not configured");
4505
4578
  }
4506
- const extractUserPrefix = (key) => {
4507
- if (!config.fileUploads) {
4508
- throw new Error("File uploads are not configured");
4509
- }
4510
- if (!config.fileUploads.s3prefix) {
4511
- return key.split("/")[0];
4512
- }
4513
- const prefix = config.fileUploads.s3prefix.replace(/\/$/, "");
4514
- if (key.startsWith(prefix + "/")) {
4515
- const keyWithoutPrefix = key.slice(prefix.length + 1);
4516
- return keyWithoutPrefix.split("/")[0];
4517
- }
4518
- return key.split("/")[0];
4519
- };
4520
4579
  const policy = {
4521
4580
  Version: "2012-10-17",
4522
4581
  Statement: [
@@ -4568,20 +4627,16 @@ var createUppyRoutes = async (app, config) => {
4568
4627
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4569
4628
  return;
4570
4629
  }
4630
+ const user = authenticationResult.user;
4571
4631
  let { key } = req.query;
4572
4632
  if (typeof key !== "string" || key.trim() === "") {
4573
4633
  res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4574
4634
  return;
4575
4635
  }
4576
- let bucket = config.fileUploads.s3Bucket;
4577
- if (key.includes("[bucket:")) {
4578
- console.log("[EXULU] key includes [bucket:name]", key);
4579
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4580
- if (!bucket?.length) {
4581
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4582
- }
4583
- key = key.split("]")[1] || "";
4584
- console.log("[EXULU] bucket", bucket);
4636
+ let bucket = key.split("/")[0];
4637
+ if (user.type !== "api" && !key.includes(`/user_${user.id}/`) && !user.super_admin) {
4638
+ res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
4639
+ return;
4585
4640
  }
4586
4641
  const client2 = getS3Client(config);
4587
4642
  const command = new import_client_s3.DeleteObjectCommand({
@@ -4609,13 +4664,32 @@ var createUppyRoutes = async (app, config) => {
4609
4664
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4610
4665
  return;
4611
4666
  }
4612
- const { key } = req.query;
4667
+ const user = authenticationResult.user;
4668
+ let { key } = req.query;
4669
+ if (!key || typeof key !== "string" || key.trim() === "") {
4670
+ res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4671
+ return;
4672
+ }
4673
+ let bucket = key.split("/")[0];
4674
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
4675
+ res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
4676
+ return;
4677
+ }
4678
+ key = key.split("/").slice(1).join("/");
4613
4679
  if (typeof key !== "string" || key.trim() === "") {
4614
4680
  res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4615
4681
  return;
4616
4682
  }
4683
+ let allowed = false;
4684
+ if (user.type === "api" || user.super_admin || key.includes(`user_${user.id}/`)) {
4685
+ allowed = true;
4686
+ }
4687
+ if (!allowed) {
4688
+ res.status(405).json({ error: "Not allowed to access the file based on authenticated user." });
4689
+ return;
4690
+ }
4617
4691
  try {
4618
- const url = await getPresignedUrl(key, config);
4692
+ const url = await getPresignedUrl(bucket, key, config);
4619
4693
  res.setHeader("Access-Control-Allow-Origin", "*");
4620
4694
  res.json({ url, method: "GET", expiresIn });
4621
4695
  } catch (err) {
@@ -4644,16 +4718,15 @@ var createUppyRoutes = async (app, config) => {
4644
4718
  return;
4645
4719
  }
4646
4720
  let { key } = req.body;
4647
- let bucket = config.fileUploads.s3Bucket;
4648
- if (key.includes("[bucket:")) {
4649
- console.log("[EXULU] key includes [bucket:name]", key);
4650
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4651
- console.log("[EXULU] bucket", bucket);
4652
- if (!bucket?.length) {
4653
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4654
- }
4655
- key = key.split("]")[1] || "";
4656
- console.log("[EXULU] key", key);
4721
+ let bucket = key.split("/")[0];
4722
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
4723
+ res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
4724
+ return;
4725
+ }
4726
+ key = key.split("/").slice(1).join("/");
4727
+ if (!key || typeof key !== "string" || key.trim() === "") {
4728
+ res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4729
+ return;
4657
4730
  }
4658
4731
  const client2 = getS3Client(config);
4659
4732
  const command = new import_client_s3.HeadObjectCommand({
@@ -4757,11 +4830,12 @@ var createUppyRoutes = async (app, config) => {
4757
4830
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4758
4831
  return;
4759
4832
  }
4833
+ const user = authenticationResult.user;
4760
4834
  const { filename, contentType } = extractFileParameters(req);
4761
4835
  validateFileParameters(filename, contentType);
4762
4836
  const key = generateS3Key2(filename);
4763
- let folder = `${authenticationResult.user.id}/`;
4764
- const fullKey = addPrefixToKey(folder + key, config);
4837
+ let fullKey = addGeneralPrefixToKey(key, config);
4838
+ fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
4765
4839
  (0, import_s3_request_presigner.getSignedUrl)(
4766
4840
  getS3Client(config),
4767
4841
  new import_client_s3.PutObjectCommand({
@@ -4805,6 +4879,7 @@ var createUppyRoutes = async (app, config) => {
4805
4879
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4806
4880
  return;
4807
4881
  }
4882
+ const user = authenticationResult.user;
4808
4883
  const client2 = getS3Client(config);
4809
4884
  const { type, metadata, filename } = req.body;
4810
4885
  if (typeof filename !== "string") {
@@ -4814,13 +4889,8 @@ var createUppyRoutes = async (app, config) => {
4814
4889
  return res.status(400).json({ error: "s3: content type must be a string" });
4815
4890
  }
4816
4891
  const key = `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
4817
- let folder = "";
4818
- if (authenticationResult.user.type === "api") {
4819
- folder = `api/`;
4820
- } else {
4821
- folder = `${authenticationResult.user.id}/`;
4822
- }
4823
- const fullKey = addPrefixToKey(folder + key, config);
4892
+ let fullKey = addGeneralPrefixToKey(key, config);
4893
+ fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
4824
4894
  const params = {
4825
4895
  Bucket: config.fileUploads.s3Bucket,
4826
4896
  Key: fullKey,
@@ -5023,7 +5093,7 @@ var createProjectRetrievalTool = async ({
5023
5093
  if (!context) {
5024
5094
  throw new Error("The item added to the project does not have a valid gid with the context id as the prefix before the first slash.");
5025
5095
  }
5026
- const id = item.split("/")[1];
5096
+ const id = item.split("/").slice(1).join("/");
5027
5097
  if (set[context]) {
5028
5098
  set[context].push(id);
5029
5099
  } else {
@@ -5969,12 +6039,20 @@ var ExuluStorage = class {
5969
6039
  this.config = config;
5970
6040
  }
5971
6041
  getPresignedUrl = async (key) => {
5972
- return await getPresignedUrl(key, this.config);
6042
+ const bucket = key.split("/")[0];
6043
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
6044
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
6045
+ }
6046
+ key = key.split("/").slice(1).join("/");
6047
+ if (!key || typeof key !== "string" || key.trim() === "") {
6048
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
6049
+ }
6050
+ return await getPresignedUrl(bucket, key, this.config);
5973
6051
  };
5974
- uploadFile = async (file, key, type, user, metadata) => {
6052
+ uploadFile = async (file, fileName, type, user, metadata, customBucket) => {
5975
6053
  return await uploadFile(
5976
6054
  file,
5977
- key,
6055
+ fileName,
5978
6056
  this.config,
5979
6057
  {
5980
6058
  contentType: type,
@@ -5983,7 +6061,8 @@ var ExuluStorage = class {
5983
6061
  type
5984
6062
  }
5985
6063
  },
5986
- user
6064
+ user,
6065
+ customBucket
5987
6066
  );
5988
6067
  };
5989
6068
  // todo add upload and delete methods
@@ -5996,12 +6075,12 @@ var ExuluContext = class {
5996
6075
  name;
5997
6076
  active;
5998
6077
  fields;
6078
+ processor;
5999
6079
  rateLimit;
6000
6080
  description;
6001
6081
  embedder;
6002
6082
  queryRewriter;
6003
6083
  resultReranker;
6004
- // todo typings
6005
6084
  configuration;
6006
6085
  sources = [];
6007
6086
  constructor({
@@ -6009,6 +6088,7 @@ var ExuluContext = class {
6009
6088
  name,
6010
6089
  description,
6011
6090
  embedder,
6091
+ processor,
6012
6092
  active,
6013
6093
  rateLimit,
6014
6094
  fields,
@@ -6021,10 +6101,12 @@ var ExuluContext = class {
6021
6101
  this.name = name;
6022
6102
  this.fields = fields || [];
6023
6103
  this.sources = sources || [];
6104
+ this.processor = processor;
6024
6105
  this.configuration = configuration || {
6025
6106
  calculateVectors: "manual",
6026
6107
  language: "english",
6027
- defaultRightsMode: "private"
6108
+ defaultRightsMode: "private",
6109
+ maxRetrievalResults: 10
6028
6110
  };
6029
6111
  this.description = description;
6030
6112
  this.embedder = embedder;
@@ -6034,23 +6116,18 @@ var ExuluContext = class {
6034
6116
  this.resultReranker = resultReranker;
6035
6117
  }
6036
6118
  processField = async (trigger, item, exuluConfig, user, role) => {
6037
- console.log("[EXULU] processing field", item.field, " in context", this.id);
6038
- console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
6039
- const field = this.fields.find((field2) => {
6040
- return field2.name.replace("_s3key", "") === item.field.replace("_s3key", "");
6041
- });
6042
- if (!field || !field.processor) {
6043
- console.error("[EXULU] field not found or processor not set for field", item.field, " in context", this.id);
6044
- throw new Error("Field not found or processor not set for field " + item.field + " in context " + this.id);
6045
- }
6119
+ console.log("[EXULU] processing item, ", item, " in context", this.id);
6046
6120
  const exuluStorage = new ExuluStorage({ config: exuluConfig });
6047
- 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;
6048
6125
  if (queue?.queue.name) {
6049
6126
  console.log("[EXULU] processor is in queue mode, scheduling job.");
6050
6127
  const job = await bullmqDecorator({
6051
- timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 600,
6052
- label: `${this.name} ${field.name} data processor`,
6053
- 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}`,
6054
6131
  context: this.id,
6055
6132
  inputs: item,
6056
6133
  item: item.id,
@@ -6065,27 +6142,33 @@ var ExuluContext = class {
6065
6142
  trigger
6066
6143
  });
6067
6144
  return {
6068
- result: "",
6145
+ result: void 0,
6069
6146
  job: job.id
6070
6147
  };
6071
6148
  }
6072
6149
  console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
6073
- const result = await field.processor.execute({
6150
+ const processorResult = await this.processor.execute({
6074
6151
  item,
6075
6152
  user,
6076
6153
  role,
6077
6154
  utils: {
6078
- storage: exuluStorage,
6079
- items: {
6080
- update: this.updateItem,
6081
- create: this.createItem,
6082
- delete: this.deleteItem
6083
- }
6155
+ storage: exuluStorage
6084
6156
  },
6085
6157
  exuluConfig
6086
6158
  });
6159
+ if (!processorResult) {
6160
+ throw new Error("Processor result is required for updating the item in the db.");
6161
+ }
6162
+ const { db: db3 } = await postgresClient();
6163
+ delete processorResult.field;
6164
+ await db3.from(getTableName(this.id)).where({
6165
+ id: processorResult.id
6166
+ }).update({
6167
+ ...processorResult,
6168
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
6169
+ });
6087
6170
  return {
6088
- result,
6171
+ result: processorResult,
6089
6172
  job: void 0
6090
6173
  };
6091
6174
  };
@@ -6096,7 +6179,8 @@ var ExuluContext = class {
6096
6179
  user: options.user,
6097
6180
  role: options.role,
6098
6181
  context: this,
6099
- db: db3
6182
+ db: db3,
6183
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10
6100
6184
  });
6101
6185
  return result;
6102
6186
  };
@@ -6170,6 +6254,8 @@ var ExuluContext = class {
6170
6254
  };
6171
6255
  };
6172
6256
  createItem = async (item, config, user, role, upsert, generateEmbeddingsOverwrite) => {
6257
+ console.log("[EXULU] creating item", item);
6258
+ console.log("[EXULU] upsert", upsert);
6173
6259
  if (upsert && (!item.id && !item.external_id)) {
6174
6260
  throw new Error("Item id or external id is required for upsert.");
6175
6261
  }
@@ -6197,10 +6283,9 @@ var ExuluContext = class {
6197
6283
  }
6198
6284
  console.log("[EXULU] context configuration", this.configuration);
6199
6285
  let jobs = [];
6200
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
6201
- for (const [key, value] of Object.entries(item)) {
6202
- console.log("[EXULU] Checking for processors for field", key);
6203
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6286
+ let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
6287
+ if (this.processor) {
6288
+ const processor = this.processor;
6204
6289
  console.log("[EXULU] Processor found", processor);
6205
6290
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6206
6291
  const {
@@ -6210,8 +6295,7 @@ var ExuluContext = class {
6210
6295
  "api",
6211
6296
  {
6212
6297
  ...item,
6213
- id: results[0].id,
6214
- field: key
6298
+ id: results[0].id
6215
6299
  },
6216
6300
  config,
6217
6301
  user,
@@ -6220,8 +6304,13 @@ var ExuluContext = class {
6220
6304
  if (processorJob) {
6221
6305
  jobs.push(processorJob);
6222
6306
  }
6223
- if (!processorJob && processor.config?.generateEmbeddings) {
6224
- shouldGenerateEmbeddings = true;
6307
+ if (!processorJob) {
6308
+ await db3.from(getTableName(this.id)).where({ id: results[0].id }).update({
6309
+ ...processorResult
6310
+ });
6311
+ if (processor.config?.generateEmbeddings) {
6312
+ shouldGenerateEmbeddings = true;
6313
+ }
6225
6314
  }
6226
6315
  }
6227
6316
  }
@@ -6247,6 +6336,7 @@ var ExuluContext = class {
6247
6336
  };
6248
6337
  };
6249
6338
  updateItem = async (item, config, user, role, generateEmbeddingsOverwrite) => {
6339
+ console.log("[EXULU] updating item", item);
6250
6340
  const { db: db3 } = await postgresClient();
6251
6341
  if (item.field) {
6252
6342
  delete item.field;
@@ -6272,8 +6362,8 @@ var ExuluContext = class {
6272
6362
  await mutation;
6273
6363
  let jobs = [];
6274
6364
  let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
6275
- for (const [key, value] of Object.entries(item)) {
6276
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6365
+ if (this.processor) {
6366
+ const processor = this.processor;
6277
6367
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6278
6368
  const {
6279
6369
  job: processorJob,
@@ -6282,8 +6372,7 @@ var ExuluContext = class {
6282
6372
  "api",
6283
6373
  {
6284
6374
  ...item,
6285
- id: record.id,
6286
- field: key
6375
+ id: record.id
6287
6376
  },
6288
6377
  config,
6289
6378
  user,
@@ -6292,8 +6381,13 @@ var ExuluContext = class {
6292
6381
  if (processorJob) {
6293
6382
  jobs.push(processorJob);
6294
6383
  }
6295
- if (!processorJob && processor.config?.generateEmbeddings) {
6296
- shouldGenerateEmbeddings = true;
6384
+ if (!processorJob) {
6385
+ await db3.from(getTableName(this.id)).where({ id: record.id }).update({
6386
+ ...processorResult
6387
+ });
6388
+ if (processor.config?.generateEmbeddings) {
6389
+ shouldGenerateEmbeddings = true;
6390
+ }
6297
6391
  }
6298
6392
  }
6299
6393
  }
@@ -6317,23 +6411,23 @@ var ExuluContext = class {
6317
6411
  };
6318
6412
  deleteItem = async (item, user, role) => {
6319
6413
  const { db: db3 } = await postgresClient();
6414
+ if (!item.id && !item.external_id) {
6415
+ throw new Error("Item id or external id is required for deleting an item.");
6416
+ }
6320
6417
  if (!item.id?.length && item?.external_id) {
6321
6418
  item = await db3.from(getTableName(this.id)).where({ external_id: item.external_id }).first();
6322
6419
  if (!item || !item.id) {
6323
6420
  throw new Error(`Item not found for external id ${item?.external_id || "undefined"}.`);
6324
6421
  }
6325
6422
  }
6326
- await db3.from(getTableName(this.id)).where({ id: item.id }).delete();
6327
- if (!this.embedder) {
6328
- return {
6329
- id: item.id,
6330
- job: void 0
6331
- };
6332
- }
6333
- const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
6334
- if (chunks.length > 0) {
6335
- await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
6423
+ const chunkTableExists = await this.chunksTableExists();
6424
+ if (chunkTableExists) {
6425
+ const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
6426
+ if (chunks.length > 0) {
6427
+ await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
6428
+ }
6336
6429
  }
6430
+ await db3.from(getTableName(this.id)).where({ id: item.id }).delete();
6337
6431
  return {
6338
6432
  id: item.id,
6339
6433
  job: void 0
@@ -6516,22 +6610,26 @@ var ExuluContext = class {
6516
6610
  };
6517
6611
  // Exports the context as a tool that can be used by an agent
6518
6612
  tool = () => {
6613
+ if (this.configuration.enableAsTool === false) {
6614
+ return null;
6615
+ }
6519
6616
  return new ExuluTool2({
6520
6617
  id: this.id,
6521
- name: `${this.name}`,
6618
+ name: `${this.name}_context_search`,
6522
6619
  type: "context",
6523
6620
  category: "contexts",
6524
6621
  inputSchema: import_zod.z.object({
6525
- 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.")
6526
6624
  }),
6527
6625
  config: [],
6528
6626
  description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
6529
- execute: async ({ query, user, role }) => {
6627
+ execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
6530
6628
  const { db: db3 } = await postgresClient();
6531
6629
  const result = await vectorSearch({
6532
6630
  page: 1,
6533
- limit: 50,
6534
- query,
6631
+ limit: this.configuration.maxRetrievalResults ?? 10,
6632
+ query: originalQuestion,
6535
6633
  filters: [],
6536
6634
  user,
6537
6635
  role,
@@ -6551,7 +6649,13 @@ var ExuluContext = class {
6551
6649
  role: user?.role?.id
6552
6650
  });
6553
6651
  return {
6554
- 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
+ })))
6555
6659
  };
6556
6660
  }
6557
6661
  });
@@ -7144,7 +7248,7 @@ Mood: friendly and intelligent.
7144
7248
  });
7145
7249
  });
7146
7250
  if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
7147
- await createUppyRoutes(app, config);
7251
+ await createUppyRoutes(app, contexts ?? [], config);
7148
7252
  } else {
7149
7253
  console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
7150
7254
  }
@@ -7418,7 +7522,7 @@ var import_ai3 = require("ai");
7418
7522
  var import_crypto_js4 = __toESM(require("crypto-js"), 1);
7419
7523
 
7420
7524
  // src/registry/log-metadata.ts
7421
- function logMetadata2(id, additionalMetadata) {
7525
+ function logMetadata(id, additionalMetadata) {
7422
7526
  return {
7423
7527
  __logMetadata: true,
7424
7528
  id,
@@ -7428,9 +7532,32 @@ function logMetadata2(id, additionalMetadata) {
7428
7532
 
7429
7533
  // src/registry/workers.ts
7430
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
+ };
7431
7557
  var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
7432
7558
  console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
7433
7559
  console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
7560
+ installGlobalErrorHandlers();
7434
7561
  if (!redisServer.host || !redisServer.port) {
7435
7562
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
7436
7563
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -7455,8 +7582,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7455
7582
  const worker = new import_bullmq4.Worker(
7456
7583
  `${queue.queue.name}`,
7457
7584
  async (bullmqJob) => {
7458
- console.log("[EXULU] starting execution for job", logMetadata2(bullmqJob.name, {
7585
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
7459
7586
  name: bullmqJob.name,
7587
+ jobId: bullmqJob.id,
7460
7588
  status: await bullmqJob.getState(),
7461
7589
  type: bullmqJob.data.type
7462
7590
  }));
@@ -7464,16 +7592,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7464
7592
  const data = bullmqJob.data;
7465
7593
  const timeoutInSeconds = data.timeoutInSeconds || 600;
7466
7594
  const timeoutMs = timeoutInSeconds * 1e3;
7595
+ let timeoutHandle;
7467
7596
  const timeoutPromise = new Promise((_, reject) => {
7468
- setTimeout(() => {
7469
- 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);
7470
7601
  }, timeoutMs);
7471
7602
  });
7472
7603
  const workPromise = (async () => {
7473
7604
  try {
7605
+ console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
7474
7606
  bullmq.validate(bullmqJob.id, data);
7475
7607
  if (data.type === "embedder") {
7476
- console.log("[EXULU] running an embedder job.", logMetadata2(bullmqJob.name));
7608
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
7477
7609
  const label = `embedder-${bullmqJob.name}`;
7478
7610
  await db3.from("job_results").insert({
7479
7611
  job_id: bullmqJob.id,
@@ -7503,7 +7635,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7503
7635
  };
7504
7636
  }
7505
7637
  if (data.type === "processor") {
7506
- console.log("[EXULU] running a processor job.", logMetadata2(bullmqJob.name));
7638
+ console.log("[EXULU] running a processor job, job name: ", bullmqJob.name, " job id: ", bullmqJob.id, " job data: ", data, " job queue: ", bullmqJob.queueName);
7507
7639
  const label = `processor-${bullmqJob.name}`;
7508
7640
  await db3.from("job_results").insert({
7509
7641
  job_id: bullmqJob.id,
@@ -7516,44 +7648,46 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7516
7648
  if (!context) {
7517
7649
  throw new Error(`Context ${data.context} not found in the registry.`);
7518
7650
  }
7519
- const field = context.fields.find((field2) => {
7520
- return field2.name.replace("_s3key", "") === data.inputs.field.replace("_s3key", "");
7521
- });
7522
- if (!field) {
7523
- throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
7651
+ if (!data.inputs.id) {
7652
+ throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
7524
7653
  }
7525
- if (!field.processor) {
7526
- throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
7654
+ if (!context.processor) {
7655
+ throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
7527
7656
  }
7528
7657
  const exuluStorage = new ExuluStorage({ config });
7529
- if (!data.user) {
7530
- throw new Error(`User not set for processor job.`);
7531
- }
7532
- if (!data.role) {
7533
- throw new Error(`Role not set for processor job.`);
7534
- }
7535
7658
  console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
7536
- const result = await field.processor.execute({
7659
+ const processorResult = await context.processor.execute({
7537
7660
  item: data.inputs,
7538
7661
  user: data.user,
7539
7662
  role: data.role,
7540
7663
  utils: {
7541
- storage: exuluStorage,
7542
- items: {
7543
- update: context.updateItem,
7544
- create: context.createItem,
7545
- delete: context.deleteItem
7546
- }
7664
+ storage: exuluStorage
7547
7665
  },
7548
- config
7666
+ exuluConfig: config
7667
+ });
7668
+ if (!processorResult) {
7669
+ throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7670
+ }
7671
+ delete processorResult.field;
7672
+ await db3.from(getTableName(context.id)).where({
7673
+ id: processorResult.id
7674
+ }).update({
7675
+ ...processorResult,
7676
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
7549
7677
  });
7550
7678
  let jobs = [];
7551
- if (field.processor.config?.generateEmbeddings) {
7679
+ if (context.processor?.config?.generateEmbeddings) {
7680
+ const fullItem = await db3.from(getTableName(context.id)).where({
7681
+ id: processorResult.id
7682
+ }).first();
7683
+ if (!fullItem) {
7684
+ throw new Error(`[EXULU] Item ${processorResult.id} not found after processor update in context ${context.id}`);
7685
+ }
7552
7686
  const { job: embeddingsJob } = await context.embeddings.generate.one({
7553
- item: data.inputs,
7687
+ item: fullItem,
7554
7688
  user: data.user,
7555
7689
  role: data.role,
7556
- trigger: "api",
7690
+ trigger: "processor",
7557
7691
  config
7558
7692
  });
7559
7693
  if (embeddingsJob) {
@@ -7561,14 +7695,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7561
7695
  }
7562
7696
  }
7563
7697
  return {
7564
- result,
7698
+ result: processorResult,
7565
7699
  metadata: {
7566
7700
  jobs: jobs.length > 0 ? jobs.join(",") : void 0
7567
7701
  }
7568
7702
  };
7569
7703
  }
7570
7704
  if (data.type === "eval_run") {
7571
- console.log("[EXULU] running an eval run job.", logMetadata2(bullmqJob.name));
7705
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
7572
7706
  const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
7573
7707
  const existingResult = await db3.from("job_results").where({ label }).first();
7574
7708
  if (existingResult) {
@@ -7617,7 +7751,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7617
7751
  resolve(messages2);
7618
7752
  break;
7619
7753
  } catch (error) {
7620
- 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, {
7621
7755
  error: error instanceof Error ? error.message : String(error)
7622
7756
  }));
7623
7757
  attempts++;
@@ -7678,7 +7812,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7678
7812
  eval_function_config: evalFunction.config || {},
7679
7813
  result: result2 || 0
7680
7814
  };
7681
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7815
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7682
7816
  result: result2 || 0
7683
7817
  }));
7684
7818
  evalFunctionResults.push(evalFunctionResult);
@@ -7697,7 +7831,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7697
7831
  result: result2 || 0
7698
7832
  };
7699
7833
  evalFunctionResults.push(evalFunctionResult);
7700
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7834
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7701
7835
  result: result2 || 0
7702
7836
  }));
7703
7837
  }
@@ -7734,7 +7868,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7734
7868
  };
7735
7869
  }
7736
7870
  if (data.type === "eval_function") {
7737
- console.log("[EXULU] running an eval function job.", logMetadata2(bullmqJob.name));
7871
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
7738
7872
  if (data.eval_functions?.length !== 1) {
7739
7873
  throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
7740
7874
  }
@@ -7780,7 +7914,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7780
7914
  inputMessages,
7781
7915
  evalFunction.config || {}
7782
7916
  );
7783
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata2(bullmqJob.name, {
7917
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
7784
7918
  result: result || 0
7785
7919
  }));
7786
7920
  }
@@ -7790,7 +7924,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7790
7924
  };
7791
7925
  }
7792
7926
  if (data.type === "source") {
7793
- console.log("[EXULU] running a source job.", logMetadata2(bullmqJob.name));
7927
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
7794
7928
  if (!data.source) {
7795
7929
  throw new Error(`No source id set for source job.`);
7796
7930
  }
@@ -7818,14 +7952,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7818
7952
  );
7819
7953
  if (job) {
7820
7954
  jobs.push(job);
7821
- 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, {
7822
7956
  item: createdItem,
7823
7957
  job
7824
7958
  }));
7825
7959
  }
7826
7960
  if (createdItem.id) {
7827
7961
  items.push(createdItem.id);
7828
- 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, {
7829
7963
  item: createdItem
7830
7964
  }));
7831
7965
  }
@@ -7853,11 +7987,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7853
7987
  throw error;
7854
7988
  }
7855
7989
  })();
7856
- return Promise.race([workPromise, timeoutPromise]);
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
+ }
7857
7999
  },
7858
8000
  {
7859
8001
  autorun: true,
7860
8002
  connection: redisConnection,
8003
+ concurrency: queue.concurrency?.worker || 1,
7861
8004
  removeOnComplete: { count: 1e3 },
7862
8005
  removeOnFail: { count: 5e3 },
7863
8006
  ...queue.ratelimit && {
@@ -7887,7 +8030,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7887
8030
  });
7888
8031
  return;
7889
8032
  }
7890
- console.error(`[EXULU] job failed.`, job?.name ? logMetadata2(job.name, {
8033
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
7891
8034
  error: error instanceof Error ? error.message : String(error)
7892
8035
  }) : error);
7893
8036
  });
@@ -7895,7 +8038,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7895
8038
  console.error(`[EXULU] worker error.`, error);
7896
8039
  });
7897
8040
  worker.on("progress", (job, progress) => {
7898
- console.log(`[EXULU] job progress ${job.id}.`, logMetadata2(job.name, {
8041
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
7899
8042
  progress
7900
8043
  }));
7901
8044
  });
@@ -9061,18 +9204,24 @@ var ExuluQueues = class {
9061
9204
  // method of ExuluQueues we need to store the desired rate limit on the queue
9062
9205
  // here so we can use the value when creating workers for the queue instance
9063
9206
  // as there is no way to store a rate limit value natively on a bullm queue.
9064
- register = (name, concurrency = 1, ratelimit = 1) => {
9207
+ register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
9208
+ const queueConcurrency = concurrency.queue || 1;
9209
+ const workerConcurrency = concurrency.worker || 1;
9065
9210
  const use = async () => {
9066
9211
  const existing = this.queues.find((x) => x.queue?.name === name);
9067
9212
  if (existing) {
9068
9213
  const globalConcurrency = await existing.queue.getGlobalConcurrency();
9069
- if (globalConcurrency !== concurrency) {
9070
- await existing.queue.setGlobalConcurrency(concurrency);
9214
+ if (globalConcurrency !== queueConcurrency) {
9215
+ await existing.queue.setGlobalConcurrency(queueConcurrency);
9071
9216
  }
9072
9217
  return {
9073
9218
  queue: existing.queue,
9074
9219
  ratelimit,
9075
- concurrency
9220
+ concurrency: {
9221
+ worker: workerConcurrency,
9222
+ queue: queueConcurrency
9223
+ },
9224
+ timeoutInSeconds
9076
9225
  };
9077
9226
  }
9078
9227
  if (!redisServer.host?.length || !redisServer.port?.length) {
@@ -9091,22 +9240,34 @@ var ExuluQueues = class {
9091
9240
  telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
9092
9241
  }
9093
9242
  );
9094
- await newQueue.setGlobalConcurrency(concurrency);
9243
+ await newQueue.setGlobalConcurrency(queueConcurrency);
9095
9244
  this.queues.push({
9096
9245
  queue: newQueue,
9097
9246
  ratelimit,
9098
- concurrency
9247
+ concurrency: {
9248
+ worker: workerConcurrency,
9249
+ queue: queueConcurrency
9250
+ },
9251
+ timeoutInSeconds
9099
9252
  });
9100
9253
  return {
9101
9254
  queue: newQueue,
9102
9255
  ratelimit,
9103
- concurrency
9256
+ concurrency: {
9257
+ worker: workerConcurrency,
9258
+ queue: queueConcurrency
9259
+ },
9260
+ timeoutInSeconds
9104
9261
  };
9105
9262
  };
9106
9263
  this.list.set(name, {
9107
9264
  name,
9108
- concurrency,
9265
+ concurrency: {
9266
+ worker: workerConcurrency,
9267
+ queue: queueConcurrency
9268
+ },
9109
9269
  ratelimit,
9270
+ timeoutInSeconds,
9110
9271
  use
9111
9272
  });
9112
9273
  return {
@@ -9161,7 +9322,10 @@ var llmAsJudgeEval = () => {
9161
9322
  name: "prompt",
9162
9323
  description: "The prompt to send to the LLM as a judge, make sure to instruct the LLM to output a numerical score between 0 and 100. Add {actual_output} to the prompt to replace with the last message content, and {expected_output} to replace with the expected output."
9163
9324
  }],
9164
- queue: queues.register("llm_as_judge", 1, 1).use(),
9325
+ queue: queues.register("llm_as_judge", {
9326
+ worker: 1,
9327
+ queue: 1
9328
+ }, 1).use(),
9165
9329
  llm: true
9166
9330
  });
9167
9331
  }
@@ -9796,11 +9960,16 @@ var previewPdfTool = new ExuluTool2({
9796
9960
  type: "function",
9797
9961
  config: [],
9798
9962
  inputSchema: import_zod5.z.object({
9799
- s3key: import_zod5.z.string().describe("The S3 key of the PDF file to preview, can also optionally include a [bucket:name] to specify the bucket."),
9963
+ s3key: import_zod5.z.string().describe("The S3 key of the PDF file to preview."),
9800
9964
  page: import_zod5.z.number().describe("The page number to preview, defaults to 1.").optional()
9801
9965
  }),
9802
9966
  execute: async ({ s3key, page, exuluConfig }) => {
9803
- const url = await getPresignedUrl(s3key, exuluConfig);
9967
+ const bucket = s3key.split("/")[0];
9968
+ const key = s3key.split("/").slice(1).join("/");
9969
+ if (!bucket || !key) {
9970
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
9971
+ }
9972
+ const url = await getPresignedUrl(bucket, key, exuluConfig);
9804
9973
  if (!url) {
9805
9974
  throw new Error("No URL provided for PDF preview");
9806
9975
  }
@@ -10164,7 +10333,7 @@ var ExuluApp = class {
10164
10333
  ...[previewPdfTool],
10165
10334
  ...todoTools,
10166
10335
  // Add contexts as tools
10167
- ...Object.values(contexts || {}).map((context) => context.tool())
10336
+ ...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
10168
10337
  // Because agents are stored in the database, we add those as tools
10169
10338
  // at request time, not during ExuluApp initialization. We add them
10170
10339
  // in the grahql tools resolver.
@@ -10194,7 +10363,10 @@ var ExuluApp = class {
10194
10363
  }
10195
10364
  const queueSet = /* @__PURE__ */ new Set();
10196
10365
  if (redisServer.host?.length && redisServer.port?.length) {
10197
- queues.register(global_queues.eval_runs, 1, 1);
10366
+ queues.register(global_queues.eval_runs, {
10367
+ worker: 1,
10368
+ queue: 1
10369
+ }, 1);
10198
10370
  for (const queue of queues.list.values()) {
10199
10371
  const config2 = await queue.use();
10200
10372
  queueSet.add(config2);
@@ -10323,10 +10495,7 @@ var ExuluApp = class {
10323
10495
  console.warn("[EXULU] No queue configured for source", source.name);
10324
10496
  continue;
10325
10497
  }
10326
- if (queue) {
10327
- if (!source.config?.schedule) {
10328
- throw new Error("Schedule is required for source when configuring a queue: " + source.name);
10329
- }
10498
+ if (queue && source.config?.schedule) {
10330
10499
  console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
10331
10500
  await queue.queue?.upsertJobScheduler(source.id, {
10332
10501
  pattern: source.config?.schedule