@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.js CHANGED
@@ -15,7 +15,6 @@ var redisServer = {
15
15
  // src/redis/client.ts
16
16
  var client = {};
17
17
  async function redisClient() {
18
- console.log("[EXULU] redisServer:", redisServer);
19
18
  if (!redisServer.host || !redisServer.port) {
20
19
  return { client: null };
21
20
  }
@@ -96,7 +95,6 @@ var db = {};
96
95
  var databaseExistsChecked = false;
97
96
  var dbName = process.env.POSTGRES_DB_NAME || "exulu";
98
97
  async function ensureDatabaseExists() {
99
- console.log(`[EXULU] Ensuring ${dbName} database exists...`);
100
98
  const defaultKnex = Knex({
101
99
  client: "pg",
102
100
  connection: {
@@ -140,16 +138,7 @@ async function ensureDatabaseExists() {
140
138
  async function postgresClient() {
141
139
  if (!db["exulu"]) {
142
140
  try {
143
- console.log(`[EXULU] Connecting to ${dbName} database.`);
144
- console.log("[EXULU] POSTGRES_DB_HOST:", process.env.POSTGRES_DB_HOST);
145
- console.log("[EXULU] POSTGRES_DB_PORT:", process.env.POSTGRES_DB_PORT);
146
- console.log("[EXULU] POSTGRES_DB_USER:", process.env.POSTGRES_DB_USER);
147
- console.log("[EXULU] POSTGRES_DB_PASSWORD:", process.env.POSTGRES_DB_PASSWORD);
148
- console.log("[EXULU] POSTGRES_DB_NAME:", dbName);
149
- console.log("[EXULU] POSTGRES_DB_SSL:", process.env.POSTGRES_DB_SSL);
150
- console.log("[EXULU] Database exists checked:", databaseExistsChecked);
151
141
  if (!databaseExistsChecked) {
152
- console.log(`[EXULU] Ensuring ${dbName} database exists...`);
153
142
  await ensureDatabaseExists();
154
143
  databaseExistsChecked = true;
155
144
  }
@@ -162,21 +151,37 @@ async function postgresClient() {
162
151
  database: dbName,
163
152
  password: process.env.POSTGRES_DB_PASSWORD,
164
153
  ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
165
- connectionTimeoutMillis: 1e4
154
+ connectionTimeoutMillis: 3e4,
155
+ // Increased from 10s to 30s to handle connection spikes
156
+ // PostgreSQL statement timeout (in milliseconds) - kills queries that run too long
157
+ // This prevents runaway queries from blocking connections
158
+ statement_timeout: 18e5,
159
+ // 30 minutes - should be longer than max job timeout (1200s = 20m)
160
+ // Connection idle timeout - how long pg client waits before timing out
161
+ query_timeout: 18e5
162
+ // 30 minutes
166
163
  },
167
164
  pool: {
168
- min: 2,
169
- max: 20,
170
- // Increased from 20 to handle more concurrent operations
171
- acquireTimeoutMillis: 3e4,
165
+ min: 5,
166
+ // Increased from 2 to ensure enough connections available
167
+ max: 50,
168
+ // Increased from 20 to handle more concurrent operations with processor jobs
169
+ acquireTimeoutMillis: 6e4,
170
+ // Increased from 30s to 60s to handle pool contention
172
171
  createTimeoutMillis: 3e4,
173
- idleTimeoutMillis: 3e4,
172
+ idleTimeoutMillis: 6e4,
173
+ // Increased to keep connections alive longer
174
174
  reapIntervalMillis: 1e3,
175
175
  createRetryIntervalMillis: 200,
176
176
  // Log pool events to help debug connection issues
177
177
  afterCreate: (conn, done) => {
178
178
  console.log("[EXULU] New database connection created");
179
- done(null, conn);
179
+ conn.query("SET statement_timeout = 1800000", (err) => {
180
+ if (err) {
181
+ console.error("[EXULU] Error setting statement_timeout:", err);
182
+ }
183
+ done(err, conn);
184
+ });
180
185
  }
181
186
  }
182
187
  });
@@ -474,7 +479,6 @@ var authentication = async ({
474
479
  }
475
480
  if (authtoken) {
476
481
  try {
477
- console.log("[EXULU] authtoken", authtoken);
478
482
  if (!authtoken?.email) {
479
483
  return {
480
484
  error: true,
@@ -1427,6 +1431,10 @@ var addCoreFields = (schema) => {
1427
1431
  field.name = field.name + "_s3key";
1428
1432
  }
1429
1433
  });
1434
+ schema.fields.push({
1435
+ name: "last_processed_at",
1436
+ type: "date"
1437
+ });
1430
1438
  if (schema.RBAC) {
1431
1439
  if (!schema.fields.some((field) => field.name === "rights_mode")) {
1432
1440
  schema.fields.push({
@@ -2309,33 +2317,38 @@ function createMutations(table, agents, contexts, tools, config) {
2309
2317
  }
2310
2318
  };
2311
2319
  if (table.type === "items") {
2312
- if (table.fields.some((field) => field.processor?.execute)) {
2313
- mutations[`${tableNameSingular}ProcessItemField`] = async (_, args, context, info) => {
2320
+ if (table.processor) {
2321
+ const contextItemProcessorMutation = async (context, items, user, role) => {
2322
+ let jobs = [];
2323
+ let results = [];
2324
+ await Promise.all(items.map(async (item) => {
2325
+ const result = await context.processField(
2326
+ "api",
2327
+ item,
2328
+ config,
2329
+ user,
2330
+ role
2331
+ );
2332
+ if (result.job) {
2333
+ jobs.push(result.job);
2334
+ }
2335
+ if (result.result) {
2336
+ results.push(result.result);
2337
+ }
2338
+ }));
2339
+ return {
2340
+ message: jobs.length > 0 ? "Processing job scheduled." : "Items processed successfully.",
2341
+ results: results.map((result) => JSON.stringify(result)),
2342
+ jobs
2343
+ };
2344
+ };
2345
+ mutations[`${tableNameSingular}ProcessItem`] = async (_, args, context, info) => {
2314
2346
  if (!context.user?.super_admin) {
2315
2347
  throw new Error("You are not authorized to process fields via API, user must be super admin.");
2316
2348
  }
2317
- const exists = contexts.find((context2) => context2.id === table.id);
2318
- if (!exists) {
2319
- throw new Error(`Context ${table.id} not found.`);
2320
- }
2321
- if (!args.field) {
2322
- throw new Error("Field argument missing, the field argument is required.");
2323
- }
2324
2349
  if (!args.item) {
2325
2350
  throw new Error("Item argument missing, the item argument is required.");
2326
2351
  }
2327
- const name = args.field?.replace("_s3key", "");
2328
- console.log("[EXULU] name", name);
2329
- console.log("[EXULU] fields", exists.fields.map((field2) => field2.name));
2330
- const field = exists.fields.find((field2) => {
2331
- return field2.name.replace("_s3key", "") === name;
2332
- });
2333
- if (!field) {
2334
- throw new Error(`Field ${name} not found in context ${exists.id}].`);
2335
- }
2336
- if (!field.processor) {
2337
- throw new Error(`Processor not set for field ${args.field} in context ${exists.id}.`);
2338
- }
2339
2352
  const { db: db3 } = context;
2340
2353
  let query = db3.from(tableNamePlural).select("*").where({ id: args.item });
2341
2354
  query = applyAccessControl(table, query, context.user);
@@ -2343,21 +2356,38 @@ function createMutations(table, agents, contexts, tools, config) {
2343
2356
  if (!item) {
2344
2357
  throw new Error("Item not found, or your user does not have access to it.");
2345
2358
  }
2346
- const { job, result } = await exists.processField(
2347
- "api",
2348
- {
2349
- ...item,
2350
- field: args.field
2351
- },
2352
- config,
2359
+ const exists = contexts.find((context2) => context2.id === table.id);
2360
+ if (!exists) {
2361
+ throw new Error(`Context ${table.id} not found.`);
2362
+ }
2363
+ return contextItemProcessorMutation(exists, [item], context.user.id, context.user.role?.id);
2364
+ };
2365
+ mutations[`${tableNameSingular}ProcessItems`] = async (_, args, context, info) => {
2366
+ if (!context.user?.super_admin) {
2367
+ throw new Error("You are not authorized to process fields via API, user must be super admin.");
2368
+ }
2369
+ const { limit = 10, filters = [], sort } = args;
2370
+ const { db: db3 } = context;
2371
+ const { items } = await paginationRequest({
2372
+ db: db3,
2373
+ limit,
2374
+ page: 0,
2375
+ filters,
2376
+ sort,
2377
+ table,
2378
+ user: context.user,
2379
+ fields: "*"
2380
+ });
2381
+ const exists = contexts.find((context2) => context2.id === table.id);
2382
+ if (!exists) {
2383
+ throw new Error(`Context ${table.id} not found.`);
2384
+ }
2385
+ return contextItemProcessorMutation(
2386
+ exists,
2387
+ items,
2353
2388
  context.user.id,
2354
2389
  context.user.role?.id
2355
2390
  );
2356
- return {
2357
- message: job ? "Processing job scheduled." : "Item processed successfully.",
2358
- result,
2359
- job
2360
- };
2361
2391
  };
2362
2392
  }
2363
2393
  mutations[`${tableNameSingular}ExecuteSource`] = async (_, args, context, info) => {
@@ -2525,7 +2555,7 @@ function createMutations(table, agents, contexts, tools, config) {
2525
2555
  }
2526
2556
  return mutations;
2527
2557
  }
2528
- var applyAccessControl = (table, query, user) => {
2558
+ var applyAccessControl = (table, query, user, field_prefix) => {
2529
2559
  const tableNamePlural = table.name.plural.toLowerCase();
2530
2560
  if (table.name.plural !== "agent_sessions" && user?.super_admin === true) {
2531
2561
  return query;
@@ -2541,18 +2571,19 @@ var applyAccessControl = (table, query, user) => {
2541
2571
  if (!hasRBAC) {
2542
2572
  return query;
2543
2573
  }
2574
+ const prefix = field_prefix ? field_prefix + "." : "";
2544
2575
  try {
2545
2576
  query = query.where(function() {
2546
- this.where("rights_mode", "public");
2547
- this.orWhere("created_by", user.id);
2577
+ this.where(`${prefix}rights_mode`, "public");
2578
+ this.orWhere(`${prefix}created_by`, user.id);
2548
2579
  this.orWhere(function() {
2549
- this.where("rights_mode", "users").whereExists(function() {
2580
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2550
2581
  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);
2551
2582
  });
2552
2583
  });
2553
2584
  if (user.role) {
2554
2585
  this.orWhere(function() {
2555
- this.where("rights_mode", "roles").whereExists(function() {
2586
+ this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2556
2587
  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);
2557
2588
  });
2558
2589
  });
@@ -2564,9 +2595,11 @@ var applyAccessControl = (table, query, user) => {
2564
2595
  }
2565
2596
  return query;
2566
2597
  };
2567
- var converOperatorToQuery = (query, fieldName, operators, table) => {
2598
+ var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
2568
2599
  const field = table?.fields.find((f) => f.name === fieldName);
2569
2600
  const isJsonField = field?.type === "json";
2601
+ const prefix = field_prefix ? field_prefix + "." : "";
2602
+ fieldName = prefix + fieldName;
2570
2603
  if (operators.eq !== void 0) {
2571
2604
  if (isJsonField) {
2572
2605
  query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
@@ -2876,53 +2909,95 @@ var finalizeRequestedFields = async ({
2876
2909
  }
2877
2910
  const { db: db3 } = await postgresClient();
2878
2911
  const query = db3.from(getChunksTableName(context.id)).where({ source: result.id }).select("id", "content", "source", "chunk_index", "createdAt", "updatedAt");
2879
- query.select(
2880
- db3.raw("vector_dims(??) as embedding_size", [`embedding`])
2881
- );
2882
2912
  const chunks = await query;
2883
2913
  result.chunks = chunks.map((chunk) => ({
2884
- cosine_distance: 0,
2885
- fts_rank: 0,
2886
- hybrid_score: 0,
2887
- content: chunk.content,
2888
- source: chunk.source,
2914
+ chunk_content: chunk.content,
2915
+ chunk_source: chunk.source,
2889
2916
  chunk_index: chunk.chunk_index,
2890
2917
  chunk_id: chunk.id,
2891
2918
  chunk_created_at: chunk.createdAt,
2892
2919
  chunk_updated_at: chunk.updatedAt,
2893
- embedding_size: chunk.embedding_size
2920
+ item_updated_at: chunk.item_updated_at,
2921
+ item_created_at: chunk.item_created_at,
2922
+ item_id: chunk.item_id,
2923
+ item_external_id: chunk.item_external_id,
2924
+ item_name: chunk.item_name
2894
2925
  }));
2895
2926
  }
2896
2927
  }
2897
2928
  }
2898
2929
  return result;
2899
2930
  };
2900
- var applyFilters = (query, filters, table) => {
2931
+ var applyFilters = (query, filters, table, field_prefix) => {
2901
2932
  filters.forEach((filter) => {
2902
2933
  Object.entries(filter).forEach(([fieldName, operators]) => {
2903
2934
  if (operators) {
2904
2935
  if (operators.and !== void 0) {
2905
2936
  operators.and.forEach((operator) => {
2906
- query = converOperatorToQuery(query, fieldName, operator, table);
2937
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2907
2938
  });
2908
2939
  }
2909
2940
  if (operators.or !== void 0) {
2910
2941
  operators.or.forEach((operator) => {
2911
- query = converOperatorToQuery(query, fieldName, operator, table);
2942
+ query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
2912
2943
  });
2913
2944
  }
2914
- query = converOperatorToQuery(query, fieldName, operators, table);
2945
+ query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
2915
2946
  }
2916
2947
  });
2917
2948
  });
2918
2949
  return query;
2919
2950
  };
2920
- var applySorting = (query, sort) => {
2951
+ var applySorting = (query, sort, field_prefix) => {
2952
+ const prefix = field_prefix ? field_prefix + "." : "";
2921
2953
  if (sort) {
2954
+ sort.field = prefix + sort.field;
2922
2955
  query = query.orderBy(sort.field, sort.direction.toLowerCase());
2923
2956
  }
2924
2957
  return query;
2925
2958
  };
2959
+ var paginationRequest = async ({
2960
+ db: db3,
2961
+ limit,
2962
+ page,
2963
+ filters,
2964
+ sort,
2965
+ table,
2966
+ user,
2967
+ fields
2968
+ }) => {
2969
+ if (limit > 1e4) {
2970
+ throw new Error("Limit cannot be greater than 10.000.");
2971
+ }
2972
+ const tableName = table.name.plural.toLowerCase();
2973
+ let countQuery = db3(tableName);
2974
+ countQuery = applyFilters(countQuery, filters, table);
2975
+ countQuery = applyAccessControl(table, countQuery, user);
2976
+ const countResult = await countQuery.count("* as count");
2977
+ const itemCount = Number(countResult[0]?.count || 0);
2978
+ const pageCount = Math.ceil(itemCount / limit);
2979
+ const currentPage = page;
2980
+ const hasPreviousPage = currentPage > 1;
2981
+ const hasNextPage = currentPage < pageCount - 1;
2982
+ let dataQuery = db3(tableName);
2983
+ dataQuery = applyFilters(dataQuery, filters, table);
2984
+ dataQuery = applyAccessControl(table, dataQuery, user);
2985
+ dataQuery = applySorting(dataQuery, sort);
2986
+ if (page > 1) {
2987
+ dataQuery = dataQuery.offset((page - 1) * limit);
2988
+ }
2989
+ let items = await dataQuery.select(fields ? fields : "*").limit(limit);
2990
+ return {
2991
+ items,
2992
+ pageInfo: {
2993
+ pageCount,
2994
+ itemCount,
2995
+ currentPage,
2996
+ hasPreviousPage,
2997
+ hasNextPage
2998
+ }
2999
+ };
3000
+ };
2926
3001
  function createQueries(table, agents, tools, contexts) {
2927
3002
  const tableNamePlural = table.name.plural.toLowerCase();
2928
3003
  const tableNameSingular = table.name.singular.toLowerCase();
@@ -2958,38 +3033,22 @@ function createQueries(table, agents, tools, contexts) {
2958
3033
  return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
2959
3034
  },
2960
3035
  [`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
2961
- const { limit = 10, page = 0, filters = [], sort } = args;
2962
3036
  const { db: db3 } = context;
2963
- if (limit > 500) {
2964
- throw new Error("Limit cannot be greater than 500.");
2965
- }
2966
- let countQuery = db3(tableNamePlural);
2967
- countQuery = applyFilters(countQuery, filters, table);
2968
- countQuery = applyAccessControl(table, countQuery, context.user);
2969
- const countResult = await countQuery.count("* as count");
2970
- const itemCount = Number(countResult[0]?.count || 0);
2971
- const pageCount = Math.ceil(itemCount / limit);
2972
- const currentPage = page;
2973
- const hasPreviousPage = currentPage > 1;
2974
- const hasNextPage = currentPage < pageCount - 1;
2975
- let dataQuery = db3(tableNamePlural);
2976
- dataQuery = applyFilters(dataQuery, filters, table);
2977
- dataQuery = applyAccessControl(table, dataQuery, context.user);
3037
+ const { limit = 10, page = 0, filters = [], sort } = args;
2978
3038
  const requestedFields = getRequestedFields(info);
2979
- dataQuery = applySorting(dataQuery, sort);
2980
- if (page > 1) {
2981
- dataQuery = dataQuery.offset((page - 1) * limit);
2982
- }
2983
3039
  const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
2984
- let items = await dataQuery.select(sanitizedFields).limit(limit);
3040
+ const { items, pageInfo } = await paginationRequest({
3041
+ db: db3,
3042
+ limit,
3043
+ page,
3044
+ filters,
3045
+ sort,
3046
+ table,
3047
+ user: context.user,
3048
+ fields: sanitizedFields
3049
+ });
2985
3050
  return {
2986
- pageInfo: {
2987
- pageCount,
2988
- itemCount,
2989
- currentPage,
2990
- hasPreviousPage,
2991
- hasNextPage
2992
- },
3051
+ pageInfo,
2993
3052
  items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
2994
3053
  };
2995
3054
  },
@@ -3038,7 +3097,7 @@ function createQueries(table, agents, tools, contexts) {
3038
3097
  }
3039
3098
  const { limit = 10, page = 0, filters = [], sort } = args;
3040
3099
  return await vectorSearch({
3041
- limit,
3100
+ limit: limit || exists.configuration.maxRetrievalResults || 10,
3042
3101
  page,
3043
3102
  filters,
3044
3103
  sort,
@@ -3097,106 +3156,111 @@ var vectorSearch = async ({
3097
3156
  }
3098
3157
  const mainTable = getTableName(id);
3099
3158
  const chunksTable = getChunksTableName(id);
3100
- let countQuery = db3(mainTable);
3101
- countQuery = applyFilters(countQuery, filters, table);
3102
- countQuery = applyAccessControl(table, countQuery, user);
3103
- const columns = await db3(mainTable).columnInfo();
3104
- let itemsQuery = db3(mainTable).select(Object.keys(columns).map((column) => mainTable + "." + column));
3105
- itemsQuery = applyFilters(itemsQuery, filters, table);
3106
- itemsQuery = applyAccessControl(table, itemsQuery, user);
3107
- itemsQuery = applySorting(itemsQuery, sort);
3159
+ let chunksQuery = db3(chunksTable + " as chunks").select([
3160
+ "chunks.id as chunk_id",
3161
+ "chunks.source",
3162
+ "chunks.content",
3163
+ "chunks.chunk_index",
3164
+ db3.raw('chunks."createdAt" as chunk_created_at'),
3165
+ db3.raw('chunks."updatedAt" as chunk_updated_at'),
3166
+ "chunks.metadata",
3167
+ "items.id as item_id",
3168
+ "items.name as item_name",
3169
+ "items.external_id as item_external_id",
3170
+ db3.raw('items."updatedAt" as item_updated_at'),
3171
+ db3.raw('items."createdAt" as item_created_at')
3172
+ ]);
3173
+ chunksQuery.leftJoin(mainTable + " as items", function() {
3174
+ this.on("chunks.source", "=", "items.id");
3175
+ });
3176
+ chunksQuery = applyFilters(chunksQuery, filters, table, "items");
3177
+ chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
3178
+ chunksQuery = applySorting(chunksQuery, sort, "items");
3108
3179
  if (queryRewriter) {
3109
3180
  query = await queryRewriter(query);
3110
3181
  }
3111
- itemsQuery.limit(limit * 3);
3112
- itemsQuery.leftJoin(chunksTable, function() {
3113
- this.on(chunksTable + ".source", "=", mainTable + ".id");
3114
- });
3115
- itemsQuery.select(chunksTable + ".id as chunk_id");
3116
- itemsQuery.select(chunksTable + ".source");
3117
- itemsQuery.select(chunksTable + ".content");
3118
- itemsQuery.select(chunksTable + ".chunk_index");
3119
- itemsQuery.select(chunksTable + ".createdAt as chunk_created_at");
3120
- itemsQuery.select(chunksTable + ".updatedAt as chunk_updated_at");
3121
- itemsQuery.select(db3.raw("vector_dims(??) as embedding_size", [`${chunksTable}.embedding`]));
3122
- const { chunks } = await embedder.generateFromQuery(context.id, query, {
3182
+ const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
3123
3183
  label: table.name.singular,
3124
3184
  trigger
3125
3185
  }, user?.id, role);
3126
- if (!chunks?.[0]?.vector) {
3186
+ if (!queryChunks?.[0]?.vector) {
3127
3187
  throw new Error("No vector generated for query.");
3128
3188
  }
3129
- const vector = chunks[0].vector;
3189
+ const vector = queryChunks[0].vector;
3130
3190
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3131
3191
  const vectorExpr = `${vectorStr}::vector`;
3132
3192
  const language = configuration.language || "english";
3133
- let items = [];
3193
+ let resultChunks = [];
3134
3194
  switch (method) {
3135
3195
  case "tsvector":
3136
- itemsQuery.select(db3.raw(
3137
- `ts_rank(${chunksTable}.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3196
+ chunksQuery.limit(limit * 2);
3197
+ chunksQuery.select(db3.raw(
3198
+ `ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3138
3199
  [language, query]
3139
3200
  )).whereRaw(
3140
- `${chunksTable}.fts @@ websearch_to_tsquery(?, ?)`,
3201
+ `chunks.fts @@ websearch_to_tsquery(?, ?)`,
3141
3202
  [language, query]
3142
3203
  ).orderByRaw(`fts_rank DESC`);
3143
- items = await itemsQuery;
3204
+ resultChunks = await chunksQuery;
3144
3205
  break;
3145
3206
  case "cosineDistance":
3146
- default:
3147
- itemsQuery.whereNotNull(`${chunksTable}.embedding`);
3148
- itemsQuery.select(
3149
- db3.raw(`1 - (${chunksTable}.embedding <=> ${vectorExpr}) AS cosine_distance`)
3207
+ chunksQuery.limit(limit * 2);
3208
+ chunksQuery.whereNotNull(`chunks.embedding`);
3209
+ console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
3210
+ chunksQuery.select(
3211
+ db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
3150
3212
  );
3151
- itemsQuery.orderByRaw(
3152
- `${chunksTable}.embedding <=> ${vectorExpr} ASC NULLS LAST`
3213
+ chunksQuery.orderByRaw(
3214
+ `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3153
3215
  );
3154
- items = await itemsQuery;
3216
+ resultChunks = await chunksQuery;
3155
3217
  break;
3156
3218
  case "hybridSearch":
3157
- const matchCount = Math.min(limit * 5, 100);
3219
+ const matchCount = Math.min(limit * 2, 100);
3158
3220
  const fullTextWeight = 1;
3159
3221
  const semanticWeight = 1;
3160
3222
  const rrfK = 50;
3161
3223
  const hybridSQL = `
3162
3224
  WITH full_text AS (
3163
3225
  SELECT
3164
- c.id,
3165
- c.source,
3226
+ chunks.id,
3227
+ chunks.source,
3166
3228
  row_number() OVER (
3167
- ORDER BY ts_rank_cd(c.fts, websearch_to_tsquery(?, ?)) DESC
3229
+ ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
3168
3230
  ) AS rank_ix
3169
- FROM ${chunksTable} c
3170
- WHERE c.fts @@ websearch_to_tsquery(?, ?)
3231
+ FROM ${chunksTable} as chunks
3232
+ WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
3171
3233
  ORDER BY rank_ix
3172
3234
  LIMIT LEAST(?, 15) * 2
3173
3235
  ),
3174
3236
  semantic AS (
3175
3237
  SELECT
3176
- c.id,
3177
- c.source,
3238
+ chunks.id,
3239
+ chunks.source,
3178
3240
  row_number() OVER (
3179
- ORDER BY c.embedding <=> ${vectorExpr} ASC
3241
+ ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3180
3242
  ) AS rank_ix
3181
- FROM ${chunksTable} c
3182
- WHERE c.embedding IS NOT NULL
3243
+ FROM ${chunksTable} as chunks
3244
+ WHERE chunks.embedding IS NOT NULL
3183
3245
  ORDER BY rank_ix
3184
3246
  LIMIT LEAST(?, 50) * 2
3185
3247
  )
3186
3248
  SELECT
3187
- m.*,
3188
- c.id AS chunk_id,
3189
- c.source,
3190
- c.content,
3191
- c.chunk_index,
3192
- c.metadata,
3193
- c."createdAt" AS chunk_created_at,
3194
- c."updatedAt" AS chunk_updated_at,
3195
- vector_dims(c.embedding) as embedding_size,
3196
-
3249
+ items.id as item_id,
3250
+ items.name as item_name,
3251
+ items.external_id as item_external_id,
3252
+ chunks.id AS chunk_id,
3253
+ chunks.source,
3254
+ chunks.content,
3255
+ chunks.chunk_index,
3256
+ chunks.metadata,
3257
+ chunks."createdAt" as chunk_created_at,
3258
+ chunks."updatedAt" as chunk_updated_at,
3259
+ items."updatedAt" as item_updated_at,
3260
+ items."createdAt" as item_created_at,
3197
3261
  /* Per-signal scores for introspection */
3198
- ts_rank(c.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3199
- (1 - (c.embedding <=> ${vectorExpr})) AS cosine_distance,
3262
+ ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3263
+ (1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
3200
3264
 
3201
3265
  /* Hybrid RRF score */
3202
3266
  (
@@ -3208,10 +3272,10 @@ var vectorSearch = async ({
3208
3272
  FROM full_text ft
3209
3273
  FULL OUTER JOIN semantic se
3210
3274
  ON ft.id = se.id
3211
- JOIN ${chunksTable} c
3212
- ON COALESCE(ft.id, se.id) = c.id
3213
- JOIN ${mainTable} m
3214
- ON m.id = c.source
3275
+ JOIN ${chunksTable} as chunks
3276
+ ON COALESCE(ft.id, se.id) = chunks.id
3277
+ JOIN ${mainTable} as items
3278
+ ON items.id = chunks.source
3215
3279
  ORDER BY hybrid_score DESC
3216
3280
  LIMIT LEAST(?, 50)
3217
3281
  OFFSET 0
@@ -3237,86 +3301,57 @@ var vectorSearch = async ({
3237
3301
  matchCount
3238
3302
  // final limit
3239
3303
  ];
3240
- items = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3241
- }
3242
- console.log("[EXULU] Vector search results:", items?.length);
3243
- const seenSources = /* @__PURE__ */ new Map();
3244
- items = items.reduce((acc, item) => {
3245
- if (!seenSources.has(item.source)) {
3246
- seenSources.set(item.source, {
3247
- ...Object.fromEntries(
3248
- Object.keys(item).filter(
3249
- (key) => key !== "cosine_distance" && // kept per chunk below
3250
- key !== "fts_rank" && // kept per chunk below
3251
- key !== "hybrid_score" && // we will compute per item below
3252
- key !== "content" && key !== "source" && key !== "chunk_index" && key !== "chunk_id" && key !== "chunk_created_at" && key !== "chunk_updated_at" && key !== "embedding_size"
3253
- ).map((key) => [key, item[key]])
3254
- ),
3255
- chunks: [{
3256
- content: item.content,
3257
- chunk_index: item.chunk_index,
3258
- chunk_id: item.chunk_id,
3259
- source: item.source,
3260
- metadata: item.metadata,
3261
- chunk_created_at: item.chunk_created_at,
3262
- chunk_updated_at: item.chunk_updated_at,
3263
- embedding_size: item.embedding_size,
3264
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3265
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3266
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3267
- }]
3268
- });
3269
- acc.push(seenSources.get(item.source));
3270
- } else {
3271
- seenSources.get(item.source).chunks.push({
3272
- content: item.content,
3273
- chunk_index: item.chunk_index,
3274
- chunk_id: item.chunk_id,
3275
- chunk_created_at: item.chunk_created_at,
3276
- embedding_size: item.embedding_size,
3277
- metadata: item.metadata,
3278
- source: item.source,
3279
- chunk_updated_at: item.chunk_updated_at,
3280
- ...method === "cosineDistance" && { cosine_distance: item.cosine_distance },
3281
- ...(method === "tsvector" || method === "hybridSearch") && { fts_rank: item.fts_rank },
3282
- ...method === "hybridSearch" && { hybrid_score: item.hybrid_score }
3283
- });
3284
- }
3285
- return acc;
3286
- }, []);
3287
- console.log("[EXULU] Vector search results after deduplication:", items?.length);
3288
- items.forEach((item) => {
3289
- if (!item.chunks?.length) {
3290
- return;
3291
- }
3292
- if (method === "tsvector") {
3293
- const ranks = item.chunks.map((c) => typeof c.fts_rank === "number" ? c.fts_rank : 0);
3294
- const total = ranks.reduce((a, b) => a + b, 0);
3295
- const average = ranks.length ? total / ranks.length : 0;
3296
- item.averageRelevance = average;
3297
- item.totalRelevance = total;
3298
- } else if (method === "cosineDistance") {
3299
- let methodProperty = "cosine_distance";
3300
- const average = item.chunks.reduce((acc, item2) => {
3301
- return acc + item2[methodProperty];
3302
- }, 0) / item.chunks.length;
3303
- const total = item.chunks.reduce((acc, item2) => {
3304
- return acc + item2[methodProperty];
3305
- }, 0);
3306
- item.averageRelevance = average;
3307
- item.totalRelevance = total;
3308
- } else if (method === "hybridSearch") {
3309
- const scores = item.chunks.map((c) => typeof c.hybrid_score === "number" ? c.hybrid_score * 10 + 1 : 0);
3310
- const total = scores.reduce((a, b) => a + b, 0);
3311
- const average = scores.length ? total / scores.length : 0;
3312
- item.averageRelevance = average;
3313
- item.totalRelevance = total;
3304
+ resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3305
+ }
3306
+ console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
3307
+ resultChunks = resultChunks.map((chunk) => ({
3308
+ chunk_content: chunk.content,
3309
+ chunk_index: chunk.chunk_index,
3310
+ chunk_id: chunk.chunk_id,
3311
+ chunk_source: chunk.source,
3312
+ chunk_metadata: chunk.metadata,
3313
+ chunk_created_at: chunk.chunk_created_at,
3314
+ chunk_updated_at: chunk.chunk_updated_at,
3315
+ item_updated_at: chunk.item_updated_at,
3316
+ item_created_at: chunk.item_created_at,
3317
+ item_id: chunk.item_id,
3318
+ item_external_id: chunk.item_external_id,
3319
+ item_name: chunk.item_name,
3320
+ context: {
3321
+ name: table.name.singular,
3322
+ id: table.id || ""
3323
+ },
3324
+ ...method === "cosineDistance" && { chunk_cosine_distance: chunk.cosine_distance },
3325
+ ...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
3326
+ ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score }
3327
+ }));
3328
+ if (resultChunks.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3329
+ const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
3330
+ const topScore = resultChunks[0][scoreKey];
3331
+ const bottomScore = resultChunks[resultChunks.length - 1][scoreKey];
3332
+ const medianScore = resultChunks[Math.floor(resultChunks.length / 2)][scoreKey];
3333
+ console.log("[EXULU] Score distribution:", {
3334
+ method,
3335
+ count: resultChunks.length,
3336
+ topScore: topScore?.toFixed(4),
3337
+ bottomScore: bottomScore?.toFixed(4),
3338
+ medianScore: medianScore?.toFixed(4)
3339
+ });
3340
+ const adaptiveThreshold = topScore * 0.7;
3341
+ const beforeFilterCount = resultChunks.length;
3342
+ resultChunks = resultChunks.filter((chunk) => {
3343
+ const score = chunk[scoreKey];
3344
+ return score !== void 0 && score >= adaptiveThreshold;
3345
+ });
3346
+ const filteredCount = beforeFilterCount - resultChunks.length;
3347
+ if (filteredCount > 0) {
3348
+ console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
3314
3349
  }
3315
- });
3350
+ }
3316
3351
  if (resultReranker && query) {
3317
- items = await resultReranker(items);
3352
+ resultChunks = await resultReranker(resultChunks);
3318
3353
  }
3319
- console.log("[EXULU] Vector search results after slicing:", items?.length);
3354
+ resultChunks = resultChunks.slice(0, limit);
3320
3355
  await updateStatistic({
3321
3356
  name: "count",
3322
3357
  label: table.name.singular,
@@ -3334,7 +3369,7 @@ var vectorSearch = async ({
3334
3369
  id: table.id || "",
3335
3370
  embedder: embedder.name
3336
3371
  },
3337
- items
3372
+ chunks: resultChunks
3338
3373
  };
3339
3374
  };
3340
3375
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3364,10 +3399,10 @@ var contextToTableDefinition = (context) => {
3364
3399
  plural: tableName?.endsWith("s") ? tableName : tableName + "s"
3365
3400
  },
3366
3401
  RBAC: true,
3402
+ processor: context.processor,
3367
3403
  fields: context.fields.map((field) => ({
3368
3404
  name: sanitizeName(field.name),
3369
3405
  type: field.type,
3370
- processor: field.processor,
3371
3406
  required: field.required,
3372
3407
  default: field.default,
3373
3408
  index: field.index,
@@ -3497,7 +3532,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3497
3532
  const tableNamePlural = table.name.plural.toLowerCase();
3498
3533
  const tableNameSingular = table.name.singular.toLowerCase();
3499
3534
  const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
3500
- const processorFields = table.fields.filter((field) => field.processor?.execute);
3501
3535
  typeDefs += `
3502
3536
  ${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
3503
3537
 
@@ -3524,9 +3558,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3524
3558
  ${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
3525
3559
  ${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
3526
3560
  `;
3527
- if (processorFields?.length > 0) {
3561
+ if (table.processor) {
3528
3562
  mutationDefs += `
3529
- ${tableNameSingular}ProcessItemField(item: ID!, field: ${tableNameSingular}ProcessorFieldEnum!): ${tableNameSingular}ProcessItemFieldReturnPayload
3563
+ ${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
3564
+ ${tableNameSingular}ProcessItems(limit: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}ProcessItemFieldReturnPayload
3530
3565
  `;
3531
3566
  }
3532
3567
  modelDefs += `
@@ -3544,8 +3579,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3544
3579
 
3545
3580
  type ${tableNameSingular}ProcessItemFieldReturnPayload {
3546
3581
  message: String!
3547
- result: String!
3548
- job: String
3582
+ results: [String]
3583
+ jobs: [String]
3549
3584
  }
3550
3585
 
3551
3586
  type ${tableNameSingular}DeleteChunksReturnPayload {
@@ -3560,20 +3595,31 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3560
3595
  tsvector
3561
3596
  }
3562
3597
 
3563
- ${processorFields.length > 0 ? `
3564
- enum ${tableNameSingular}ProcessorFieldEnum {
3565
- ${processorFields.map((field) => field.name).join("\n")}
3566
- }
3567
- ` : ""}
3568
-
3569
-
3570
- type ${tableNameSingular}VectorSearchResult {
3571
- items: [${tableNameSingular}]!
3598
+ type ${tableNameSingular}VectorSearchResult {
3599
+ chunks: [${tableNameSingular}VectorSearchChunk!]!
3572
3600
  context: VectoSearchResultContext!
3573
3601
  filters: JSON!
3574
3602
  query: String!
3575
3603
  method: VectorMethodEnum!
3576
3604
  }
3605
+
3606
+ type ${tableNameSingular}VectorSearchChunk {
3607
+ chunk_content: String
3608
+ chunk_index: Int
3609
+ chunk_id: String
3610
+ chunk_source: String
3611
+ chunk_metadata: JSON
3612
+ chunk_created_at: Date
3613
+ chunk_updated_at: Date
3614
+ item_updated_at: Date
3615
+ item_created_at: Date
3616
+ item_id: String!
3617
+ item_external_id: String
3618
+ item_name: String!
3619
+ chunk_cosine_distance: Float
3620
+ chunk_fts_rank: Float
3621
+ chunk_hybrid_score: Float
3622
+ }
3577
3623
 
3578
3624
  type VectoSearchResultContext {
3579
3625
  name: String!
@@ -3678,7 +3724,11 @@ type PageInfo {
3678
3724
  const config2 = await queue.use();
3679
3725
  return {
3680
3726
  name: config2.queue.name,
3681
- concurrency: config2.concurrency,
3727
+ concurrency: {
3728
+ worker: config2.concurrency?.worker || void 0,
3729
+ queue: config2.concurrency?.queue || void 0
3730
+ },
3731
+ timeoutInSeconds: config2.timeoutInSeconds,
3682
3732
  ratelimit: config2.ratelimit,
3683
3733
  isMaxed: await config2.queue.isMaxed(),
3684
3734
  isPaused: await config2.queue.isPaused(),
@@ -3728,7 +3778,10 @@ type PageInfo {
3728
3778
  if (!agentInstance) {
3729
3779
  throw new Error("Agent instance not found for eval run.");
3730
3780
  }
3731
- const evalQueue = await queues.register("eval_runs", 1, 1).use();
3781
+ const evalQueue = await queues.register("eval_runs", {
3782
+ worker: 1,
3783
+ queue: 1
3784
+ }, 1).use();
3732
3785
  const jobIds = [];
3733
3786
  for (const testCase of testCases) {
3734
3787
  const jobData = {
@@ -3852,7 +3905,6 @@ type PageInfo {
3852
3905
  if (!client2) {
3853
3906
  throw new Error("Redis client not created properly");
3854
3907
  }
3855
- console.log("[EXULU] Jobs pagination args", args);
3856
3908
  const {
3857
3909
  jobs,
3858
3910
  count
@@ -3862,7 +3914,6 @@ type PageInfo {
3862
3914
  args.page || 1,
3863
3915
  args.limit || 100
3864
3916
  );
3865
- console.log("[EXULU] jobs", jobs.map((job) => job.name));
3866
3917
  const requestedFields = getRequestedFields(info);
3867
3918
  return {
3868
3919
  items: await Promise.all(jobs.map(async (job) => {
@@ -3891,6 +3942,21 @@ type PageInfo {
3891
3942
  };
3892
3943
  resolvers.Query["contexts"] = async (_, args, context, info) => {
3893
3944
  const data = await Promise.all(contexts.map(async (context2) => {
3945
+ let processor = null;
3946
+ if (context2.processor) {
3947
+ processor = await new Promise(async (resolve, reject) => {
3948
+ const config2 = await context2.processor?.config;
3949
+ const queue = await config2?.queue;
3950
+ resolve({
3951
+ name: context2.processor.name,
3952
+ description: context2.processor.description,
3953
+ queue: queue?.queue?.name || void 0,
3954
+ trigger: context2.processor?.config?.trigger || "manual",
3955
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
3956
+ generateEmbeddings: context2.processor?.config?.generateEmbeddings || false
3957
+ });
3958
+ });
3959
+ }
3894
3960
  const sources = await Promise.all(context2.sources.map(async (source) => {
3895
3961
  let queueName = void 0;
3896
3962
  if (source.config) {
@@ -3922,6 +3988,7 @@ type PageInfo {
3922
3988
  slug: "/contexts/" + context2.id,
3923
3989
  active: context2.active,
3924
3990
  sources,
3991
+ processor,
3925
3992
  fields: context2.fields.map((field) => {
3926
3993
  return {
3927
3994
  ...field,
@@ -3947,6 +4014,21 @@ type PageInfo {
3947
4014
  if (!data) {
3948
4015
  return null;
3949
4016
  }
4017
+ let processor = null;
4018
+ if (data.processor) {
4019
+ processor = await new Promise(async (resolve, reject) => {
4020
+ const config2 = await data.processor?.config;
4021
+ const queue = await config2?.queue;
4022
+ resolve({
4023
+ name: data.processor.name,
4024
+ description: data.processor.description,
4025
+ queue: queue?.queue?.name || void 0,
4026
+ trigger: data.processor?.config?.trigger || "manual",
4027
+ timeoutInSeconds: queue?.timeoutInSeconds || 600,
4028
+ generateEmbeddings: data.processor?.config?.generateEmbeddings || false
4029
+ });
4030
+ });
4031
+ }
3950
4032
  const sources = await Promise.all(data.sources.map(async (source) => {
3951
4033
  let queueName = void 0;
3952
4034
  if (source.config) {
@@ -3983,35 +4065,18 @@ type PageInfo {
3983
4065
  slug: "/contexts/" + data.id,
3984
4066
  active: data.active,
3985
4067
  sources,
4068
+ processor,
3986
4069
  fields: await Promise.all(data.fields.map(async (field) => {
3987
4070
  const label = field.name?.replace("_s3key", "");
3988
4071
  if (field.type === "file" && !field.name.endsWith("_s3key")) {
3989
4072
  field.name = field.name + "_s3key";
3990
4073
  }
3991
- let queue = null;
3992
- if (field.processor?.config?.queue) {
3993
- queue = await field.processor.config.queue;
3994
- }
3995
4074
  return {
3996
4075
  ...field,
3997
4076
  name: sanitizeName(field.name),
3998
4077
  ...field.type === "file" ? {
3999
4078
  allowedFileTypes: field.allowedFileTypes
4000
4079
  } : {},
4001
- ...field.processor ? {
4002
- processor: {
4003
- description: field.processor?.description,
4004
- config: {
4005
- trigger: field.processor?.config?.trigger,
4006
- queue: {
4007
- name: queue?.queue.name || void 0,
4008
- ratelimit: queue?.ratelimit || void 0,
4009
- concurrency: queue?.concurrency || void 0
4010
- }
4011
- },
4012
- execute: "function"
4013
- }
4014
- } : {},
4015
4080
  label
4016
4081
  };
4017
4082
  })),
@@ -4079,13 +4144,20 @@ type PageInfo {
4079
4144
  modelDefs += `
4080
4145
  type QueueResult {
4081
4146
  name: String!
4082
- concurrency: Int!
4147
+ concurrency: QueueConcurrency!
4148
+ timeoutInSeconds: Int!
4083
4149
  ratelimit: Int!
4084
4150
  isMaxed: Boolean!
4085
4151
  isPaused: Boolean!
4086
4152
  jobs: QueueJobsCounts
4087
4153
  }
4088
4154
  `;
4155
+ modelDefs += `
4156
+ type QueueConcurrency {
4157
+ worker: Int
4158
+ queue: Int
4159
+ }
4160
+ `;
4089
4161
  modelDefs += `
4090
4162
  type QueueJobsCounts {
4091
4163
  paused: Int!
@@ -4155,17 +4227,12 @@ type AgentEvalFunctionConfig {
4155
4227
  }
4156
4228
 
4157
4229
  type ItemChunks {
4158
- cosine_distance: Float
4159
- fts_rank: Float
4160
- hybrid_score: Float
4161
- content: String
4162
- source: ID
4163
- chunk_index: Int
4164
- chunk_id: ID
4165
- chunk_created_at: Date
4166
- chunk_updated_at: Date
4167
- embedding_size: Float
4168
- metadata: JSON
4230
+ chunk_id: String!
4231
+ chunk_index: Int!
4232
+ chunk_content: String!
4233
+ chunk_source: String!
4234
+ chunk_created_at: Date!
4235
+ chunk_updated_at: Date!
4169
4236
  }
4170
4237
 
4171
4238
  type Provider {
@@ -4200,7 +4267,8 @@ type Context {
4200
4267
  active: Boolean
4201
4268
  fields: JSON
4202
4269
  configuration: JSON
4203
- sources: [ContextSource!]
4270
+ sources: [ContextSource]
4271
+ processor: ContextProcessor
4204
4272
  }
4205
4273
  type Embedder {
4206
4274
  name: String!
@@ -4213,6 +4281,14 @@ type EmbedderConfig {
4213
4281
  description: String
4214
4282
  default: String
4215
4283
  }
4284
+ type ContextProcessor {
4285
+ name: String!
4286
+ description: String
4287
+ queue: String
4288
+ trigger: String
4289
+ timeoutInSeconds: Int
4290
+ generateEmbeddings: Boolean
4291
+ }
4216
4292
 
4217
4293
  type ContextSource {
4218
4294
  id: String!
@@ -4270,6 +4346,9 @@ type Job {
4270
4346
  name: String!
4271
4347
  returnvalue: JSON
4272
4348
  stacktrace: [String]
4349
+ finishedOn: Date
4350
+ processedOn: Date
4351
+ attemptsMade: Int
4273
4352
  failedReason: String
4274
4353
  state: String!
4275
4354
  data: JSON
@@ -4331,10 +4410,7 @@ async function getJobsByQueueName(queueName, statusses, page, limit) {
4331
4410
  const config = await queue.use();
4332
4411
  const startIndex = (page || 1) - 1;
4333
4412
  const endIndex = startIndex - 1 + (limit || 100);
4334
- console.log("[EXULU] Jobs pagination startIndex", startIndex);
4335
- console.log("[EXULU] Jobs pagination endIndex", endIndex);
4336
4413
  const jobs = await config.queue.getJobs(statusses || [], startIndex, endIndex, false);
4337
- console.log("[EXULU] Jobs pagination jobs", jobs?.length);
4338
4414
  const counts = await config.queue.getJobCounts(...statusses || []);
4339
4415
  let total = 0;
4340
4416
  if (counts) {
@@ -4394,21 +4470,12 @@ function getS3Client(config) {
4394
4470
  });
4395
4471
  return s3Client;
4396
4472
  }
4397
- var getPresignedUrl = async (key, config) => {
4473
+ var getPresignedUrl = async (bucket, key, config) => {
4398
4474
  if (!config.fileUploads) {
4399
4475
  throw new Error("File uploads are not configured");
4400
4476
  }
4401
- let bucket = config.fileUploads.s3Bucket;
4402
- if (key.includes("[bucket:")) {
4403
- console.log("[EXULU] key includes [bucket:name]", key);
4404
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4405
- if (!bucket?.length) {
4406
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4407
- }
4408
- key = key.split("]")[1] || "";
4409
- console.log("[EXULU] bucket", bucket);
4410
- console.log("[EXULU] key", key);
4411
- }
4477
+ console.log("[EXULU] getting presigned url for bucket", bucket);
4478
+ console.log("[EXULU] getting presigned url for key", key);
4412
4479
  const url = await getSignedUrl(
4413
4480
  getS3Client(config),
4414
4481
  new GetObjectCommand({
@@ -4419,7 +4486,7 @@ var getPresignedUrl = async (key, config) => {
4419
4486
  );
4420
4487
  return url;
4421
4488
  };
4422
- var addPrefixToKey = (keyPath, config) => {
4489
+ var addGeneralPrefixToKey = (keyPath, config) => {
4423
4490
  if (!config.fileUploads) {
4424
4491
  throw new Error("File uploads are not configured");
4425
4492
  }
@@ -4432,58 +4499,50 @@ var addPrefixToKey = (keyPath, config) => {
4432
4499
  }
4433
4500
  return `${prefix}/${keyPath}`;
4434
4501
  };
4435
- var uploadFile = async (file, key, config, options = {}, user) => {
4502
+ var addUserPrefixToKey = (key, user) => {
4503
+ if (!user) {
4504
+ return key;
4505
+ }
4506
+ if (key.includes(`/user_${user}/`)) {
4507
+ return key;
4508
+ }
4509
+ return `user_${user}/${key}`;
4510
+ };
4511
+ var addBucketPrefixToKey = (key, bucket) => {
4512
+ if (key.includes(`/${bucket}/`)) {
4513
+ return key;
4514
+ }
4515
+ return `${bucket}/${key}`;
4516
+ };
4517
+ var uploadFile = async (file, fileName, config, options = {}, user, customBucket) => {
4436
4518
  if (!config.fileUploads) {
4437
4519
  throw new Error("File uploads are not configured (in the exported uploadFile function)");
4438
4520
  }
4439
4521
  const client2 = getS3Client(config);
4440
4522
  let defaultBucket = config.fileUploads.s3Bucket;
4441
- let customBucket = false;
4442
- if (key.includes("[bucket:")) {
4443
- console.log("[EXULU] key includes [bucket:name]", key);
4444
- customBucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4445
- if (!customBucket?.length) {
4446
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4447
- }
4448
- key = key.split("]")[1] || "";
4449
- console.log("[EXULU] custom bucket", customBucket);
4450
- }
4451
- let folder = user ? `${user}/` : "";
4452
- const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
4453
- console.log("[EXULU] uploading file to s3 into bucket", customBucket || defaultBucket, "with key", fullKey);
4523
+ let key = fileName;
4524
+ key = addGeneralPrefixToKey(key, config);
4525
+ key = addUserPrefixToKey(key, user || "api");
4526
+ console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
4454
4527
  const command = new PutObjectCommand({
4455
4528
  Bucket: customBucket || defaultBucket,
4456
- Key: fullKey,
4529
+ Key: key,
4457
4530
  Body: file,
4458
4531
  ContentType: options.contentType,
4459
4532
  Metadata: options.metadata,
4460
4533
  ContentLength: file.byteLength
4461
4534
  });
4462
4535
  await client2.send(command);
4463
- console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", fullKey);
4464
- if (customBucket) {
4465
- return "[bucket:" + customBucket + "]" + fullKey;
4466
- }
4467
- return fullKey;
4536
+ console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
4537
+ return addBucketPrefixToKey(
4538
+ key,
4539
+ customBucket || defaultBucket
4540
+ );
4468
4541
  };
4469
- var createUppyRoutes = async (app, config) => {
4542
+ var createUppyRoutes = async (app, contexts, config) => {
4470
4543
  if (!config.fileUploads) {
4471
4544
  throw new Error("File uploads are not configured");
4472
4545
  }
4473
- const extractUserPrefix = (key) => {
4474
- if (!config.fileUploads) {
4475
- throw new Error("File uploads are not configured");
4476
- }
4477
- if (!config.fileUploads.s3prefix) {
4478
- return key.split("/")[0];
4479
- }
4480
- const prefix = config.fileUploads.s3prefix.replace(/\/$/, "");
4481
- if (key.startsWith(prefix + "/")) {
4482
- const keyWithoutPrefix = key.slice(prefix.length + 1);
4483
- return keyWithoutPrefix.split("/")[0];
4484
- }
4485
- return key.split("/")[0];
4486
- };
4487
4546
  const policy = {
4488
4547
  Version: "2012-10-17",
4489
4548
  Statement: [
@@ -4535,20 +4594,16 @@ var createUppyRoutes = async (app, config) => {
4535
4594
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4536
4595
  return;
4537
4596
  }
4597
+ const user = authenticationResult.user;
4538
4598
  let { key } = req.query;
4539
4599
  if (typeof key !== "string" || key.trim() === "") {
4540
4600
  res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4541
4601
  return;
4542
4602
  }
4543
- let bucket = config.fileUploads.s3Bucket;
4544
- if (key.includes("[bucket:")) {
4545
- console.log("[EXULU] key includes [bucket:name]", key);
4546
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4547
- if (!bucket?.length) {
4548
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4549
- }
4550
- key = key.split("]")[1] || "";
4551
- console.log("[EXULU] bucket", bucket);
4603
+ let bucket = key.split("/")[0];
4604
+ if (user.type !== "api" && !key.includes(`/user_${user.id}/`) && !user.super_admin) {
4605
+ res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
4606
+ return;
4552
4607
  }
4553
4608
  const client2 = getS3Client(config);
4554
4609
  const command = new DeleteObjectCommand({
@@ -4576,13 +4631,32 @@ var createUppyRoutes = async (app, config) => {
4576
4631
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4577
4632
  return;
4578
4633
  }
4579
- const { key } = req.query;
4634
+ const user = authenticationResult.user;
4635
+ let { key } = req.query;
4636
+ if (!key || typeof key !== "string" || key.trim() === "") {
4637
+ res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4638
+ return;
4639
+ }
4640
+ let bucket = key.split("/")[0];
4641
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
4642
+ res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
4643
+ return;
4644
+ }
4645
+ key = key.split("/").slice(1).join("/");
4580
4646
  if (typeof key !== "string" || key.trim() === "") {
4581
4647
  res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4582
4648
  return;
4583
4649
  }
4650
+ let allowed = false;
4651
+ if (user.type === "api" || user.super_admin || key.includes(`user_${user.id}/`)) {
4652
+ allowed = true;
4653
+ }
4654
+ if (!allowed) {
4655
+ res.status(405).json({ error: "Not allowed to access the file based on authenticated user." });
4656
+ return;
4657
+ }
4584
4658
  try {
4585
- const url = await getPresignedUrl(key, config);
4659
+ const url = await getPresignedUrl(bucket, key, config);
4586
4660
  res.setHeader("Access-Control-Allow-Origin", "*");
4587
4661
  res.json({ url, method: "GET", expiresIn });
4588
4662
  } catch (err) {
@@ -4611,16 +4685,15 @@ var createUppyRoutes = async (app, config) => {
4611
4685
  return;
4612
4686
  }
4613
4687
  let { key } = req.body;
4614
- let bucket = config.fileUploads.s3Bucket;
4615
- if (key.includes("[bucket:")) {
4616
- console.log("[EXULU] key includes [bucket:name]", key);
4617
- bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
4618
- console.log("[EXULU] bucket", bucket);
4619
- if (!bucket?.length) {
4620
- throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
4621
- }
4622
- key = key.split("]")[1] || "";
4623
- console.log("[EXULU] key", key);
4688
+ let bucket = key.split("/")[0];
4689
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
4690
+ res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
4691
+ return;
4692
+ }
4693
+ key = key.split("/").slice(1).join("/");
4694
+ if (!key || typeof key !== "string" || key.trim() === "") {
4695
+ res.status(400).json({ error: "Missing or invalid `key` query parameter." });
4696
+ return;
4624
4697
  }
4625
4698
  const client2 = getS3Client(config);
4626
4699
  const command = new HeadObjectCommand({
@@ -4724,11 +4797,12 @@ var createUppyRoutes = async (app, config) => {
4724
4797
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4725
4798
  return;
4726
4799
  }
4800
+ const user = authenticationResult.user;
4727
4801
  const { filename, contentType } = extractFileParameters(req);
4728
4802
  validateFileParameters(filename, contentType);
4729
4803
  const key = generateS3Key2(filename);
4730
- let folder = `${authenticationResult.user.id}/`;
4731
- const fullKey = addPrefixToKey(folder + key, config);
4804
+ let fullKey = addGeneralPrefixToKey(key, config);
4805
+ fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
4732
4806
  getSignedUrl(
4733
4807
  getS3Client(config),
4734
4808
  new PutObjectCommand({
@@ -4772,6 +4846,7 @@ var createUppyRoutes = async (app, config) => {
4772
4846
  res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
4773
4847
  return;
4774
4848
  }
4849
+ const user = authenticationResult.user;
4775
4850
  const client2 = getS3Client(config);
4776
4851
  const { type, metadata, filename } = req.body;
4777
4852
  if (typeof filename !== "string") {
@@ -4781,13 +4856,8 @@ var createUppyRoutes = async (app, config) => {
4781
4856
  return res.status(400).json({ error: "s3: content type must be a string" });
4782
4857
  }
4783
4858
  const key = `${randomUUID()}-_EXULU_${filename}`;
4784
- let folder = "";
4785
- if (authenticationResult.user.type === "api") {
4786
- folder = `api/`;
4787
- } else {
4788
- folder = `${authenticationResult.user.id}/`;
4789
- }
4790
- const fullKey = addPrefixToKey(folder + key, config);
4859
+ let fullKey = addGeneralPrefixToKey(key, config);
4860
+ fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
4791
4861
  const params = {
4792
4862
  Bucket: config.fileUploads.s3Bucket,
4793
4863
  Key: fullKey,
@@ -4990,7 +5060,7 @@ var createProjectRetrievalTool = async ({
4990
5060
  if (!context) {
4991
5061
  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.");
4992
5062
  }
4993
- const id = item.split("/")[1];
5063
+ const id = item.split("/").slice(1).join("/");
4994
5064
  if (set[context]) {
4995
5065
  set[context].push(id);
4996
5066
  } else {
@@ -5936,12 +6006,20 @@ var ExuluStorage = class {
5936
6006
  this.config = config;
5937
6007
  }
5938
6008
  getPresignedUrl = async (key) => {
5939
- return await getPresignedUrl(key, this.config);
6009
+ const bucket = key.split("/")[0];
6010
+ if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
6011
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
6012
+ }
6013
+ key = key.split("/").slice(1).join("/");
6014
+ if (!key || typeof key !== "string" || key.trim() === "") {
6015
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
6016
+ }
6017
+ return await getPresignedUrl(bucket, key, this.config);
5940
6018
  };
5941
- uploadFile = async (file, key, type, user, metadata) => {
6019
+ uploadFile = async (file, fileName, type, user, metadata, customBucket) => {
5942
6020
  return await uploadFile(
5943
6021
  file,
5944
- key,
6022
+ fileName,
5945
6023
  this.config,
5946
6024
  {
5947
6025
  contentType: type,
@@ -5950,7 +6028,8 @@ var ExuluStorage = class {
5950
6028
  type
5951
6029
  }
5952
6030
  },
5953
- user
6031
+ user,
6032
+ customBucket
5954
6033
  );
5955
6034
  };
5956
6035
  // todo add upload and delete methods
@@ -5963,12 +6042,12 @@ var ExuluContext = class {
5963
6042
  name;
5964
6043
  active;
5965
6044
  fields;
6045
+ processor;
5966
6046
  rateLimit;
5967
6047
  description;
5968
6048
  embedder;
5969
6049
  queryRewriter;
5970
6050
  resultReranker;
5971
- // todo typings
5972
6051
  configuration;
5973
6052
  sources = [];
5974
6053
  constructor({
@@ -5976,6 +6055,7 @@ var ExuluContext = class {
5976
6055
  name,
5977
6056
  description,
5978
6057
  embedder,
6058
+ processor,
5979
6059
  active,
5980
6060
  rateLimit,
5981
6061
  fields,
@@ -5988,10 +6068,12 @@ var ExuluContext = class {
5988
6068
  this.name = name;
5989
6069
  this.fields = fields || [];
5990
6070
  this.sources = sources || [];
6071
+ this.processor = processor;
5991
6072
  this.configuration = configuration || {
5992
6073
  calculateVectors: "manual",
5993
6074
  language: "english",
5994
- defaultRightsMode: "private"
6075
+ defaultRightsMode: "private",
6076
+ maxRetrievalResults: 10
5995
6077
  };
5996
6078
  this.description = description;
5997
6079
  this.embedder = embedder;
@@ -6001,23 +6083,18 @@ var ExuluContext = class {
6001
6083
  this.resultReranker = resultReranker;
6002
6084
  }
6003
6085
  processField = async (trigger, item, exuluConfig, user, role) => {
6004
- console.log("[EXULU] processing field", item.field, " in context", this.id);
6005
- console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
6006
- const field = this.fields.find((field2) => {
6007
- return field2.name.replace("_s3key", "") === item.field.replace("_s3key", "");
6008
- });
6009
- if (!field || !field.processor) {
6010
- console.error("[EXULU] field not found or processor not set for field", item.field, " in context", this.id);
6011
- throw new Error("Field not found or processor not set for field " + item.field + " in context " + this.id);
6012
- }
6086
+ console.log("[EXULU] processing item, ", item, " in context", this.id);
6013
6087
  const exuluStorage = new ExuluStorage({ config: exuluConfig });
6014
- const queue = await field.processor.config?.queue;
6088
+ if (!this.processor) {
6089
+ throw new Error(`Processor is not set for this context: ${this.id}.`);
6090
+ }
6091
+ const queue = await this.processor.config?.queue;
6015
6092
  if (queue?.queue.name) {
6016
6093
  console.log("[EXULU] processor is in queue mode, scheduling job.");
6017
6094
  const job = await bullmqDecorator({
6018
- timeoutInSeconds: field.processor?.config?.timeoutInSeconds || 600,
6019
- label: `${this.name} ${field.name} data processor`,
6020
- processor: `${this.id}-${field.name}`,
6095
+ timeoutInSeconds: this.processor.config?.timeoutInSeconds || 600,
6096
+ label: `${this.name} ${this.processor.name} data processor`,
6097
+ processor: `${this.id}-${this.processor.name}`,
6021
6098
  context: this.id,
6022
6099
  inputs: item,
6023
6100
  item: item.id,
@@ -6032,27 +6109,33 @@ var ExuluContext = class {
6032
6109
  trigger
6033
6110
  });
6034
6111
  return {
6035
- result: "",
6112
+ result: void 0,
6036
6113
  job: job.id
6037
6114
  };
6038
6115
  }
6039
6116
  console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
6040
- const result = await field.processor.execute({
6117
+ const processorResult = await this.processor.execute({
6041
6118
  item,
6042
6119
  user,
6043
6120
  role,
6044
6121
  utils: {
6045
- storage: exuluStorage,
6046
- items: {
6047
- update: this.updateItem,
6048
- create: this.createItem,
6049
- delete: this.deleteItem
6050
- }
6122
+ storage: exuluStorage
6051
6123
  },
6052
6124
  exuluConfig
6053
6125
  });
6126
+ if (!processorResult) {
6127
+ throw new Error("Processor result is required for updating the item in the db.");
6128
+ }
6129
+ const { db: db3 } = await postgresClient();
6130
+ delete processorResult.field;
6131
+ await db3.from(getTableName(this.id)).where({
6132
+ id: processorResult.id
6133
+ }).update({
6134
+ ...processorResult,
6135
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
6136
+ });
6054
6137
  return {
6055
- result,
6138
+ result: processorResult,
6056
6139
  job: void 0
6057
6140
  };
6058
6141
  };
@@ -6063,7 +6146,8 @@ var ExuluContext = class {
6063
6146
  user: options.user,
6064
6147
  role: options.role,
6065
6148
  context: this,
6066
- db: db3
6149
+ db: db3,
6150
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10
6067
6151
  });
6068
6152
  return result;
6069
6153
  };
@@ -6137,6 +6221,8 @@ var ExuluContext = class {
6137
6221
  };
6138
6222
  };
6139
6223
  createItem = async (item, config, user, role, upsert, generateEmbeddingsOverwrite) => {
6224
+ console.log("[EXULU] creating item", item);
6225
+ console.log("[EXULU] upsert", upsert);
6140
6226
  if (upsert && (!item.id && !item.external_id)) {
6141
6227
  throw new Error("Item id or external id is required for upsert.");
6142
6228
  }
@@ -6164,10 +6250,9 @@ var ExuluContext = class {
6164
6250
  }
6165
6251
  console.log("[EXULU] context configuration", this.configuration);
6166
6252
  let jobs = [];
6167
- let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
6168
- for (const [key, value] of Object.entries(item)) {
6169
- console.log("[EXULU] Checking for processors for field", key);
6170
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6253
+ let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
6254
+ if (this.processor) {
6255
+ const processor = this.processor;
6171
6256
  console.log("[EXULU] Processor found", processor);
6172
6257
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6173
6258
  const {
@@ -6177,8 +6262,7 @@ var ExuluContext = class {
6177
6262
  "api",
6178
6263
  {
6179
6264
  ...item,
6180
- id: results[0].id,
6181
- field: key
6265
+ id: results[0].id
6182
6266
  },
6183
6267
  config,
6184
6268
  user,
@@ -6187,8 +6271,13 @@ var ExuluContext = class {
6187
6271
  if (processorJob) {
6188
6272
  jobs.push(processorJob);
6189
6273
  }
6190
- if (!processorJob && processor.config?.generateEmbeddings) {
6191
- shouldGenerateEmbeddings = true;
6274
+ if (!processorJob) {
6275
+ await db3.from(getTableName(this.id)).where({ id: results[0].id }).update({
6276
+ ...processorResult
6277
+ });
6278
+ if (processor.config?.generateEmbeddings) {
6279
+ shouldGenerateEmbeddings = true;
6280
+ }
6192
6281
  }
6193
6282
  }
6194
6283
  }
@@ -6214,6 +6303,7 @@ var ExuluContext = class {
6214
6303
  };
6215
6304
  };
6216
6305
  updateItem = async (item, config, user, role, generateEmbeddingsOverwrite) => {
6306
+ console.log("[EXULU] updating item", item);
6217
6307
  const { db: db3 } = await postgresClient();
6218
6308
  if (item.field) {
6219
6309
  delete item.field;
@@ -6239,8 +6329,8 @@ var ExuluContext = class {
6239
6329
  await mutation;
6240
6330
  let jobs = [];
6241
6331
  let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
6242
- for (const [key, value] of Object.entries(item)) {
6243
- const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
6332
+ if (this.processor) {
6333
+ const processor = this.processor;
6244
6334
  if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
6245
6335
  const {
6246
6336
  job: processorJob,
@@ -6249,8 +6339,7 @@ var ExuluContext = class {
6249
6339
  "api",
6250
6340
  {
6251
6341
  ...item,
6252
- id: record.id,
6253
- field: key
6342
+ id: record.id
6254
6343
  },
6255
6344
  config,
6256
6345
  user,
@@ -6259,8 +6348,13 @@ var ExuluContext = class {
6259
6348
  if (processorJob) {
6260
6349
  jobs.push(processorJob);
6261
6350
  }
6262
- if (!processorJob && processor.config?.generateEmbeddings) {
6263
- shouldGenerateEmbeddings = true;
6351
+ if (!processorJob) {
6352
+ await db3.from(getTableName(this.id)).where({ id: record.id }).update({
6353
+ ...processorResult
6354
+ });
6355
+ if (processor.config?.generateEmbeddings) {
6356
+ shouldGenerateEmbeddings = true;
6357
+ }
6264
6358
  }
6265
6359
  }
6266
6360
  }
@@ -6284,23 +6378,23 @@ var ExuluContext = class {
6284
6378
  };
6285
6379
  deleteItem = async (item, user, role) => {
6286
6380
  const { db: db3 } = await postgresClient();
6381
+ if (!item.id && !item.external_id) {
6382
+ throw new Error("Item id or external id is required for deleting an item.");
6383
+ }
6287
6384
  if (!item.id?.length && item?.external_id) {
6288
6385
  item = await db3.from(getTableName(this.id)).where({ external_id: item.external_id }).first();
6289
6386
  if (!item || !item.id) {
6290
6387
  throw new Error(`Item not found for external id ${item?.external_id || "undefined"}.`);
6291
6388
  }
6292
6389
  }
6293
- await db3.from(getTableName(this.id)).where({ id: item.id }).delete();
6294
- if (!this.embedder) {
6295
- return {
6296
- id: item.id,
6297
- job: void 0
6298
- };
6299
- }
6300
- const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
6301
- if (chunks.length > 0) {
6302
- await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
6390
+ const chunkTableExists = await this.chunksTableExists();
6391
+ if (chunkTableExists) {
6392
+ const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
6393
+ if (chunks.length > 0) {
6394
+ await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
6395
+ }
6303
6396
  }
6397
+ await db3.from(getTableName(this.id)).where({ id: item.id }).delete();
6304
6398
  return {
6305
6399
  id: item.id,
6306
6400
  job: void 0
@@ -6483,22 +6577,26 @@ var ExuluContext = class {
6483
6577
  };
6484
6578
  // Exports the context as a tool that can be used by an agent
6485
6579
  tool = () => {
6580
+ if (this.configuration.enableAsTool === false) {
6581
+ return null;
6582
+ }
6486
6583
  return new ExuluTool2({
6487
6584
  id: this.id,
6488
- name: `${this.name}`,
6585
+ name: `${this.name}_context_search`,
6489
6586
  type: "context",
6490
6587
  category: "contexts",
6491
6588
  inputSchema: z.object({
6492
- query: z.string()
6589
+ originalQuestion: z.string().describe("The original question that the user asked"),
6590
+ relevantKeywords: z.array(z.string()).describe("The keywords that are relevant to the user's question, for example names of specific products, systems or parts, IDs, etc.")
6493
6591
  }),
6494
6592
  config: [],
6495
6593
  description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
6496
- execute: async ({ query, user, role }) => {
6594
+ execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
6497
6595
  const { db: db3 } = await postgresClient();
6498
6596
  const result = await vectorSearch({
6499
6597
  page: 1,
6500
- limit: 50,
6501
- query,
6598
+ limit: this.configuration.maxRetrievalResults ?? 10,
6599
+ query: originalQuestion,
6502
6600
  filters: [],
6503
6601
  user,
6504
6602
  role,
@@ -6518,7 +6616,13 @@ var ExuluContext = class {
6518
6616
  role: user?.role?.id
6519
6617
  });
6520
6618
  return {
6521
- items: result.items
6619
+ result: JSON.stringify(result.chunks.map((chunk) => ({
6620
+ ...chunk,
6621
+ context: {
6622
+ name: this.name,
6623
+ id: this.id
6624
+ }
6625
+ })))
6522
6626
  };
6523
6627
  }
6524
6628
  });
@@ -7111,7 +7215,7 @@ Mood: friendly and intelligent.
7111
7215
  });
7112
7216
  });
7113
7217
  if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
7114
- await createUppyRoutes(app, config);
7218
+ await createUppyRoutes(app, contexts ?? [], config);
7115
7219
  } else {
7116
7220
  console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
7117
7221
  }
@@ -7385,7 +7489,7 @@ import "ai";
7385
7489
  import CryptoJS4 from "crypto-js";
7386
7490
 
7387
7491
  // src/registry/log-metadata.ts
7388
- function logMetadata2(id, additionalMetadata) {
7492
+ function logMetadata(id, additionalMetadata) {
7389
7493
  return {
7390
7494
  __logMetadata: true,
7391
7495
  id,
@@ -7395,9 +7499,32 @@ function logMetadata2(id, additionalMetadata) {
7395
7499
 
7396
7500
  // src/registry/workers.ts
7397
7501
  var redisConnection;
7502
+ var unhandledRejectionHandlerInstalled = false;
7503
+ var installGlobalErrorHandlers = () => {
7504
+ if (unhandledRejectionHandlerInstalled) return;
7505
+ process.on("unhandledRejection", (reason, promise) => {
7506
+ console.error("[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.", {
7507
+ reason: reason instanceof Error ? reason.message : String(reason),
7508
+ stack: reason instanceof Error ? reason.stack : void 0
7509
+ });
7510
+ });
7511
+ process.on("uncaughtException", (error) => {
7512
+ console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
7513
+ error: error.message,
7514
+ stack: error.stack
7515
+ });
7516
+ if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
7517
+ console.error("[EXULU] Fatal error detected, exiting process.");
7518
+ process.exit(1);
7519
+ }
7520
+ });
7521
+ unhandledRejectionHandlerInstalled = true;
7522
+ console.log("[EXULU] Global error handlers installed to prevent worker crashes");
7523
+ };
7398
7524
  var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
7399
7525
  console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
7400
7526
  console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
7527
+ installGlobalErrorHandlers();
7401
7528
  if (!redisServer.host || !redisServer.port) {
7402
7529
  console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
7403
7530
  throw new Error("No redis server configured in the environment, so cannot start worker.");
@@ -7422,8 +7549,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7422
7549
  const worker = new Worker(
7423
7550
  `${queue.queue.name}`,
7424
7551
  async (bullmqJob) => {
7425
- console.log("[EXULU] starting execution for job", logMetadata2(bullmqJob.name, {
7552
+ console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
7426
7553
  name: bullmqJob.name,
7554
+ jobId: bullmqJob.id,
7427
7555
  status: await bullmqJob.getState(),
7428
7556
  type: bullmqJob.data.type
7429
7557
  }));
@@ -7431,16 +7559,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7431
7559
  const data = bullmqJob.data;
7432
7560
  const timeoutInSeconds = data.timeoutInSeconds || 600;
7433
7561
  const timeoutMs = timeoutInSeconds * 1e3;
7562
+ let timeoutHandle;
7434
7563
  const timeoutPromise = new Promise((_, reject) => {
7435
- setTimeout(() => {
7436
- reject(new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`));
7564
+ timeoutHandle = setTimeout(() => {
7565
+ const timeoutError = new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`);
7566
+ console.error(`[EXULU] ${timeoutError.message}`);
7567
+ reject(timeoutError);
7437
7568
  }, timeoutMs);
7438
7569
  });
7439
7570
  const workPromise = (async () => {
7440
7571
  try {
7572
+ console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
7441
7573
  bullmq.validate(bullmqJob.id, data);
7442
7574
  if (data.type === "embedder") {
7443
- console.log("[EXULU] running an embedder job.", logMetadata2(bullmqJob.name));
7575
+ console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
7444
7576
  const label = `embedder-${bullmqJob.name}`;
7445
7577
  await db3.from("job_results").insert({
7446
7578
  job_id: bullmqJob.id,
@@ -7470,7 +7602,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7470
7602
  };
7471
7603
  }
7472
7604
  if (data.type === "processor") {
7473
- console.log("[EXULU] running a processor job.", logMetadata2(bullmqJob.name));
7605
+ console.log("[EXULU] running a processor job, job name: ", bullmqJob.name, " job id: ", bullmqJob.id, " job data: ", data, " job queue: ", bullmqJob.queueName);
7474
7606
  const label = `processor-${bullmqJob.name}`;
7475
7607
  await db3.from("job_results").insert({
7476
7608
  job_id: bullmqJob.id,
@@ -7483,44 +7615,46 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7483
7615
  if (!context) {
7484
7616
  throw new Error(`Context ${data.context} not found in the registry.`);
7485
7617
  }
7486
- const field = context.fields.find((field2) => {
7487
- return field2.name.replace("_s3key", "") === data.inputs.field.replace("_s3key", "");
7488
- });
7489
- if (!field) {
7490
- throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
7618
+ if (!data.inputs.id) {
7619
+ throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
7491
7620
  }
7492
- if (!field.processor) {
7493
- throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
7621
+ if (!context.processor) {
7622
+ throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
7494
7623
  }
7495
7624
  const exuluStorage = new ExuluStorage({ config });
7496
- if (!data.user) {
7497
- throw new Error(`User not set for processor job.`);
7498
- }
7499
- if (!data.role) {
7500
- throw new Error(`Role not set for processor job.`);
7501
- }
7502
7625
  console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
7503
- const result = await field.processor.execute({
7626
+ const processorResult = await context.processor.execute({
7504
7627
  item: data.inputs,
7505
7628
  user: data.user,
7506
7629
  role: data.role,
7507
7630
  utils: {
7508
- storage: exuluStorage,
7509
- items: {
7510
- update: context.updateItem,
7511
- create: context.createItem,
7512
- delete: context.deleteItem
7513
- }
7631
+ storage: exuluStorage
7514
7632
  },
7515
- config
7633
+ exuluConfig: config
7634
+ });
7635
+ if (!processorResult) {
7636
+ throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
7637
+ }
7638
+ delete processorResult.field;
7639
+ await db3.from(getTableName(context.id)).where({
7640
+ id: processorResult.id
7641
+ }).update({
7642
+ ...processorResult,
7643
+ last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
7516
7644
  });
7517
7645
  let jobs = [];
7518
- if (field.processor.config?.generateEmbeddings) {
7646
+ if (context.processor?.config?.generateEmbeddings) {
7647
+ const fullItem = await db3.from(getTableName(context.id)).where({
7648
+ id: processorResult.id
7649
+ }).first();
7650
+ if (!fullItem) {
7651
+ throw new Error(`[EXULU] Item ${processorResult.id} not found after processor update in context ${context.id}`);
7652
+ }
7519
7653
  const { job: embeddingsJob } = await context.embeddings.generate.one({
7520
- item: data.inputs,
7654
+ item: fullItem,
7521
7655
  user: data.user,
7522
7656
  role: data.role,
7523
- trigger: "api",
7657
+ trigger: "processor",
7524
7658
  config
7525
7659
  });
7526
7660
  if (embeddingsJob) {
@@ -7528,14 +7662,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7528
7662
  }
7529
7663
  }
7530
7664
  return {
7531
- result,
7665
+ result: processorResult,
7532
7666
  metadata: {
7533
7667
  jobs: jobs.length > 0 ? jobs.join(",") : void 0
7534
7668
  }
7535
7669
  };
7536
7670
  }
7537
7671
  if (data.type === "eval_run") {
7538
- console.log("[EXULU] running an eval run job.", logMetadata2(bullmqJob.name));
7672
+ console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
7539
7673
  const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
7540
7674
  const existingResult = await db3.from("job_results").where({ label }).first();
7541
7675
  if (existingResult) {
@@ -7584,7 +7718,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7584
7718
  resolve(messages2);
7585
7719
  break;
7586
7720
  } catch (error) {
7587
- console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata2(bullmqJob.name, {
7721
+ console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
7588
7722
  error: error instanceof Error ? error.message : String(error)
7589
7723
  }));
7590
7724
  attempts++;
@@ -7645,7 +7779,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7645
7779
  eval_function_config: evalFunction.config || {},
7646
7780
  result: result2 || 0
7647
7781
  };
7648
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7782
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7649
7783
  result: result2 || 0
7650
7784
  }));
7651
7785
  evalFunctionResults.push(evalFunctionResult);
@@ -7664,7 +7798,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7664
7798
  result: result2 || 0
7665
7799
  };
7666
7800
  evalFunctionResults.push(evalFunctionResult);
7667
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata2(bullmqJob.name, {
7801
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
7668
7802
  result: result2 || 0
7669
7803
  }));
7670
7804
  }
@@ -7701,7 +7835,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7701
7835
  };
7702
7836
  }
7703
7837
  if (data.type === "eval_function") {
7704
- console.log("[EXULU] running an eval function job.", logMetadata2(bullmqJob.name));
7838
+ console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
7705
7839
  if (data.eval_functions?.length !== 1) {
7706
7840
  throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
7707
7841
  }
@@ -7747,7 +7881,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7747
7881
  inputMessages,
7748
7882
  evalFunction.config || {}
7749
7883
  );
7750
- console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata2(bullmqJob.name, {
7884
+ console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
7751
7885
  result: result || 0
7752
7886
  }));
7753
7887
  }
@@ -7757,7 +7891,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7757
7891
  };
7758
7892
  }
7759
7893
  if (data.type === "source") {
7760
- console.log("[EXULU] running a source job.", logMetadata2(bullmqJob.name));
7894
+ console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
7761
7895
  if (!data.source) {
7762
7896
  throw new Error(`No source id set for source job.`);
7763
7897
  }
@@ -7785,14 +7919,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7785
7919
  );
7786
7920
  if (job) {
7787
7921
  jobs.push(job);
7788
- console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata2(bullmqJob.name, {
7922
+ console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
7789
7923
  item: createdItem,
7790
7924
  job
7791
7925
  }));
7792
7926
  }
7793
7927
  if (createdItem.id) {
7794
7928
  items.push(createdItem.id);
7795
- console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata2(bullmqJob.name, {
7929
+ console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
7796
7930
  item: createdItem
7797
7931
  }));
7798
7932
  }
@@ -7820,11 +7954,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7820
7954
  throw error;
7821
7955
  }
7822
7956
  })();
7823
- return Promise.race([workPromise, timeoutPromise]);
7957
+ try {
7958
+ const result = await Promise.race([workPromise, timeoutPromise]);
7959
+ clearTimeout(timeoutHandle);
7960
+ return result;
7961
+ } catch (error) {
7962
+ clearTimeout(timeoutHandle);
7963
+ console.error(`[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`, error instanceof Error ? error.message : String(error));
7964
+ throw error;
7965
+ }
7824
7966
  },
7825
7967
  {
7826
7968
  autorun: true,
7827
7969
  connection: redisConnection,
7970
+ concurrency: queue.concurrency?.worker || 1,
7828
7971
  removeOnComplete: { count: 1e3 },
7829
7972
  removeOnFail: { count: 5e3 },
7830
7973
  ...queue.ratelimit && {
@@ -7854,7 +7997,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7854
7997
  });
7855
7998
  return;
7856
7999
  }
7857
- console.error(`[EXULU] job failed.`, job?.name ? logMetadata2(job.name, {
8000
+ console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
7858
8001
  error: error instanceof Error ? error.message : String(error)
7859
8002
  }) : error);
7860
8003
  });
@@ -7862,7 +8005,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
7862
8005
  console.error(`[EXULU] worker error.`, error);
7863
8006
  });
7864
8007
  worker.on("progress", (job, progress) => {
7865
- console.log(`[EXULU] job progress ${job.id}.`, logMetadata2(job.name, {
8008
+ console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
7866
8009
  progress
7867
8010
  }));
7868
8011
  });
@@ -9028,18 +9171,24 @@ var ExuluQueues = class {
9028
9171
  // method of ExuluQueues we need to store the desired rate limit on the queue
9029
9172
  // here so we can use the value when creating workers for the queue instance
9030
9173
  // as there is no way to store a rate limit value natively on a bullm queue.
9031
- register = (name, concurrency = 1, ratelimit = 1) => {
9174
+ register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
9175
+ const queueConcurrency = concurrency.queue || 1;
9176
+ const workerConcurrency = concurrency.worker || 1;
9032
9177
  const use = async () => {
9033
9178
  const existing = this.queues.find((x) => x.queue?.name === name);
9034
9179
  if (existing) {
9035
9180
  const globalConcurrency = await existing.queue.getGlobalConcurrency();
9036
- if (globalConcurrency !== concurrency) {
9037
- await existing.queue.setGlobalConcurrency(concurrency);
9181
+ if (globalConcurrency !== queueConcurrency) {
9182
+ await existing.queue.setGlobalConcurrency(queueConcurrency);
9038
9183
  }
9039
9184
  return {
9040
9185
  queue: existing.queue,
9041
9186
  ratelimit,
9042
- concurrency
9187
+ concurrency: {
9188
+ worker: workerConcurrency,
9189
+ queue: queueConcurrency
9190
+ },
9191
+ timeoutInSeconds
9043
9192
  };
9044
9193
  }
9045
9194
  if (!redisServer.host?.length || !redisServer.port?.length) {
@@ -9058,22 +9207,34 @@ var ExuluQueues = class {
9058
9207
  telemetry: new BullMQOtel("simple-guide")
9059
9208
  }
9060
9209
  );
9061
- await newQueue.setGlobalConcurrency(concurrency);
9210
+ await newQueue.setGlobalConcurrency(queueConcurrency);
9062
9211
  this.queues.push({
9063
9212
  queue: newQueue,
9064
9213
  ratelimit,
9065
- concurrency
9214
+ concurrency: {
9215
+ worker: workerConcurrency,
9216
+ queue: queueConcurrency
9217
+ },
9218
+ timeoutInSeconds
9066
9219
  });
9067
9220
  return {
9068
9221
  queue: newQueue,
9069
9222
  ratelimit,
9070
- concurrency
9223
+ concurrency: {
9224
+ worker: workerConcurrency,
9225
+ queue: queueConcurrency
9226
+ },
9227
+ timeoutInSeconds
9071
9228
  };
9072
9229
  };
9073
9230
  this.list.set(name, {
9074
9231
  name,
9075
- concurrency,
9232
+ concurrency: {
9233
+ worker: workerConcurrency,
9234
+ queue: queueConcurrency
9235
+ },
9076
9236
  ratelimit,
9237
+ timeoutInSeconds,
9077
9238
  use
9078
9239
  });
9079
9240
  return {
@@ -9128,7 +9289,10 @@ var llmAsJudgeEval = () => {
9128
9289
  name: "prompt",
9129
9290
  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."
9130
9291
  }],
9131
- queue: queues.register("llm_as_judge", 1, 1).use(),
9292
+ queue: queues.register("llm_as_judge", {
9293
+ worker: 1,
9294
+ queue: 1
9295
+ }, 1).use(),
9132
9296
  llm: true
9133
9297
  });
9134
9298
  }
@@ -9763,11 +9927,16 @@ var previewPdfTool = new ExuluTool2({
9763
9927
  type: "function",
9764
9928
  config: [],
9765
9929
  inputSchema: z5.object({
9766
- s3key: z5.string().describe("The S3 key of the PDF file to preview, can also optionally include a [bucket:name] to specify the bucket."),
9930
+ s3key: z5.string().describe("The S3 key of the PDF file to preview."),
9767
9931
  page: z5.number().describe("The page number to preview, defaults to 1.").optional()
9768
9932
  }),
9769
9933
  execute: async ({ s3key, page, exuluConfig }) => {
9770
- const url = await getPresignedUrl(s3key, exuluConfig);
9934
+ const bucket = s3key.split("/")[0];
9935
+ const key = s3key.split("/").slice(1).join("/");
9936
+ if (!bucket || !key) {
9937
+ throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
9938
+ }
9939
+ const url = await getPresignedUrl(bucket, key, exuluConfig);
9771
9940
  if (!url) {
9772
9941
  throw new Error("No URL provided for PDF preview");
9773
9942
  }
@@ -10131,7 +10300,7 @@ var ExuluApp = class {
10131
10300
  ...[previewPdfTool],
10132
10301
  ...todoTools,
10133
10302
  // Add contexts as tools
10134
- ...Object.values(contexts || {}).map((context) => context.tool())
10303
+ ...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
10135
10304
  // Because agents are stored in the database, we add those as tools
10136
10305
  // at request time, not during ExuluApp initialization. We add them
10137
10306
  // in the grahql tools resolver.
@@ -10161,7 +10330,10 @@ var ExuluApp = class {
10161
10330
  }
10162
10331
  const queueSet = /* @__PURE__ */ new Set();
10163
10332
  if (redisServer.host?.length && redisServer.port?.length) {
10164
- queues.register(global_queues.eval_runs, 1, 1);
10333
+ queues.register(global_queues.eval_runs, {
10334
+ worker: 1,
10335
+ queue: 1
10336
+ }, 1);
10165
10337
  for (const queue of queues.list.values()) {
10166
10338
  const config2 = await queue.use();
10167
10339
  queueSet.add(config2);
@@ -10290,10 +10462,7 @@ var ExuluApp = class {
10290
10462
  console.warn("[EXULU] No queue configured for source", source.name);
10291
10463
  continue;
10292
10464
  }
10293
- if (queue) {
10294
- if (!source.config?.schedule) {
10295
- throw new Error("Schedule is required for source when configuring a queue: " + source.name);
10296
- }
10465
+ if (queue && source.config?.schedule) {
10297
10466
  console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
10298
10467
  await queue.queue?.upsertJobScheduler(source.id, {
10299
10468
  pattern: source.config?.schedule
@@ -12012,5 +12181,5 @@ export {
12012
12181
  ExuluUtils,
12013
12182
  ExuluVariables,
12014
12183
  db2 as db,
12015
- logMetadata2 as logMetadata
12184
+ logMetadata
12016
12185
  };