@exulu/backend 1.41.0 → 1.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2562,7 +2562,7 @@ var applyAccessControl = (table, query, user, field_prefix) => {
2562
2562
  }
2563
2563
  console.log("[EXULU] user.role", user?.role);
2564
2564
  console.log("[EXULU] table.name.plural", table.name.plural);
2565
- if (!user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2565
+ if (user && !user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2566
2566
  console.error("==== Access control error: no role found or no access to entity type. ====");
2567
2567
  throw new Error("Access control error: no role found or no access to entity type.");
2568
2568
  }
@@ -2571,17 +2571,23 @@ var applyAccessControl = (table, query, user, field_prefix) => {
2571
2571
  if (!hasRBAC) {
2572
2572
  return query;
2573
2573
  }
2574
+ if (user?.super_admin) {
2575
+ return query;
2576
+ }
2574
2577
  const prefix = field_prefix ? field_prefix + "." : "";
2578
+ console.log("[EXULU] applying access control with this prefix", prefix);
2575
2579
  try {
2576
2580
  query = query.where(function() {
2577
2581
  this.where(`${prefix}rights_mode`, "public");
2578
- this.orWhere(`${prefix}created_by`, user.id);
2579
- this.orWhere(function() {
2580
- this.where(`${prefix}rights_mode`, "users").whereExists(function() {
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);
2582
+ if (user) {
2583
+ this.orWhere(`${prefix}created_by`, user.id);
2584
+ this.orWhere(function() {
2585
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2586
+ 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);
2587
+ });
2582
2588
  });
2583
- });
2584
- if (user.role) {
2589
+ }
2590
+ if (user?.role) {
2585
2591
  this.orWhere(function() {
2586
2592
  this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2587
2593
  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);
@@ -3107,7 +3113,9 @@ function createQueries(table, agents, tools, contexts) {
3107
3113
  method: args.method,
3108
3114
  user: context.user,
3109
3115
  role: context.user?.role?.id,
3110
- trigger: "api"
3116
+ trigger: "api",
3117
+ cutoffs: args.cutoffs,
3118
+ expand: args.expand
3111
3119
  });
3112
3120
  };
3113
3121
  }
@@ -3124,7 +3132,9 @@ var vectorSearch = async ({
3124
3132
  method,
3125
3133
  user,
3126
3134
  role,
3127
- trigger
3135
+ trigger,
3136
+ cutoffs,
3137
+ expand
3128
3138
  }) => {
3129
3139
  const table = contextToTableDefinition(context);
3130
3140
  console.log("[EXULU] Called vector search.", {
@@ -3136,10 +3146,12 @@ var vectorSearch = async ({
3136
3146
  query,
3137
3147
  method,
3138
3148
  user,
3139
- role
3149
+ role,
3150
+ cutoffs,
3151
+ expand
3140
3152
  });
3141
- if (limit > 50) {
3142
- throw new Error("Limit cannot be greater than 50.");
3153
+ if (limit > 250) {
3154
+ throw new Error("Limit cannot be greater than 1000.");
3143
3155
  }
3144
3156
  if (!query) {
3145
3157
  throw new Error("Query is required.");
@@ -3156,6 +3168,15 @@ var vectorSearch = async ({
3156
3168
  }
3157
3169
  const mainTable = getTableName(id);
3158
3170
  const chunksTable = getChunksTableName(id);
3171
+ cutoffs = {
3172
+ cosineDistance: cutoffs?.cosineDistance || context.configuration?.cutoffs?.cosineDistance || 0,
3173
+ tsvector: cutoffs?.tsvector || context.configuration?.cutoffs?.tsvector || 0,
3174
+ hybrid: cutoffs?.hybrid ? (cutoffs?.hybrid ?? 0) / 100 : context.configuration?.cutoffs ? (context.configuration?.cutoffs?.hybrid ?? 0) / 100 : 0
3175
+ };
3176
+ expand = {
3177
+ before: expand?.before || context.configuration?.expand?.before || 0,
3178
+ after: expand?.after || context.configuration?.expand?.after || 0
3179
+ };
3159
3180
  let chunksQuery = db3(chunksTable + " as chunks").select([
3160
3181
  "chunks.id as chunk_id",
3161
3182
  "chunks.source",
@@ -3190,22 +3211,30 @@ var vectorSearch = async ({
3190
3211
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3191
3212
  const vectorExpr = `${vectorStr}::vector`;
3192
3213
  const language = configuration.language || "english";
3214
+ console.log("[EXULU] Vector search params:", { method, query, cutoffs });
3193
3215
  let resultChunks = [];
3194
3216
  switch (method) {
3195
3217
  case "tsvector":
3196
3218
  chunksQuery.limit(limit * 2);
3219
+ const tokens = query.trim().split(/\s+/).filter((t) => t.length > 0);
3220
+ const sanitizedTokens = tokens.flatMap((t) => {
3221
+ return t.split(/[^\w]+/).filter((part) => part.length > 0);
3222
+ });
3223
+ const orQuery = sanitizedTokens.join(" | ");
3224
+ console.log("[EXULU] FTS query transformation:", { original: query, tokens, sanitizedTokens, orQuery, cutoff: cutoffs?.tsvector });
3197
3225
  chunksQuery.select(db3.raw(
3198
- `ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3199
- [language, query]
3226
+ `ts_rank(chunks.fts, to_tsquery(?, ?)) as fts_rank`,
3227
+ [language, orQuery]
3200
3228
  )).whereRaw(
3201
- `chunks.fts @@ websearch_to_tsquery(?, ?)`,
3202
- [language, query]
3229
+ `(chunks.fts @@ to_tsquery(?, ?)) AND (items.archived IS FALSE OR items.archived IS NULL)`,
3230
+ [language, orQuery]
3203
3231
  ).orderByRaw(`fts_rank DESC`);
3232
+ console.log("[EXULU] FTS query SQL:", chunksQuery.toQuery());
3204
3233
  resultChunks = await chunksQuery;
3205
3234
  break;
3206
3235
  case "cosineDistance":
3207
3236
  chunksQuery.limit(limit * 2);
3208
- chunksQuery.whereNotNull(`chunks.embedding`);
3237
+ chunksQuery.whereNotNull(`chunks.embedding`).whereRaw(`(items.archived IS FALSE OR items.archived IS NULL)`);
3209
3238
  console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
3210
3239
  chunksQuery.select(
3211
3240
  db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
@@ -3213,11 +3242,12 @@ var vectorSearch = async ({
3213
3242
  chunksQuery.orderByRaw(
3214
3243
  `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3215
3244
  );
3245
+ chunksQuery.whereRaw(`(1 - (chunks.embedding <=> ${vectorExpr}) >= ?)`, [cutoffs?.cosineDistance || 0]);
3216
3246
  resultChunks = await chunksQuery;
3217
3247
  break;
3218
3248
  case "hybridSearch":
3219
- const matchCount = Math.min(limit * 2, 100);
3220
- const fullTextWeight = 1;
3249
+ const matchCount = Math.min(limit * 2);
3250
+ const fullTextWeight = 2;
3221
3251
  const semanticWeight = 1;
3222
3252
  const rrfK = 50;
3223
3253
  const hybridSQL = `
@@ -3226,12 +3256,15 @@ var vectorSearch = async ({
3226
3256
  chunks.id,
3227
3257
  chunks.source,
3228
3258
  row_number() OVER (
3229
- ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
3259
+ ORDER BY ts_rank(chunks.fts, plainto_tsquery(?, ?)) DESC
3230
3260
  ) AS rank_ix
3231
3261
  FROM ${chunksTable} as chunks
3232
- WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
3262
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3263
+ WHERE chunks.fts @@ plainto_tsquery(?, ?)
3264
+ AND ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?
3265
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3233
3266
  ORDER BY rank_ix
3234
- LIMIT LEAST(?, 15) * 2
3267
+ LIMIT LEAST(?, 250) * 2
3235
3268
  ),
3236
3269
  semantic AS (
3237
3270
  SELECT
@@ -3241,9 +3274,12 @@ var vectorSearch = async ({
3241
3274
  ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3242
3275
  ) AS rank_ix
3243
3276
  FROM ${chunksTable} as chunks
3277
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3244
3278
  WHERE chunks.embedding IS NOT NULL
3279
+ AND (1 - (chunks.embedding <=> ${vectorExpr})) >= ?
3280
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3245
3281
  ORDER BY rank_ix
3246
- LIMIT LEAST(?, 50) * 2
3282
+ LIMIT LEAST(?, 250) * 2
3247
3283
  )
3248
3284
  SELECT
3249
3285
  items.id as item_id,
@@ -3259,7 +3295,7 @@ var vectorSearch = async ({
3259
3295
  items."updatedAt" as item_updated_at,
3260
3296
  items."createdAt" as item_created_at,
3261
3297
  /* Per-signal scores for introspection */
3262
- ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3298
+ ts_rank(chunks.fts, plainto_tsquery(?, ?)) AS fts_rank,
3263
3299
  (1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
3264
3300
 
3265
3301
  /* Hybrid RRF score */
@@ -3276,18 +3312,31 @@ var vectorSearch = async ({
3276
3312
  ON COALESCE(ft.id, se.id) = chunks.id
3277
3313
  JOIN ${mainTable} as items
3278
3314
  ON items.id = chunks.source
3315
+ WHERE (
3316
+ COALESCE(1.0 / (? + ft.rank_ix), 0.0) * ?
3317
+ +
3318
+ COALESCE(1.0 / (? + se.rank_ix), 0.0) * ?
3319
+ ) >= ?
3320
+ AND (chunks.fts IS NULL OR ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?)
3321
+ AND (chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)
3279
3322
  ORDER BY hybrid_score DESC
3280
- LIMIT LEAST(?, 50)
3323
+ LIMIT LEAST(?, 250)
3281
3324
  OFFSET 0
3282
3325
  `;
3283
3326
  const bindings = [
3284
- // full_text: websearch_to_tsquery(lang, query) in rank and where
3327
+ // full_text: plainto_tsquery(lang, query) in rank and where
3285
3328
  language,
3286
3329
  query,
3287
3330
  language,
3288
3331
  query,
3332
+ language,
3333
+ query,
3334
+ cutoffs?.tsvector || 0,
3335
+ // full_text tsvector cutoff
3289
3336
  matchCount,
3290
3337
  // full_text limit
3338
+ cutoffs?.cosineDistance || 0,
3339
+ // semantic cosine distance cutoff
3291
3340
  matchCount,
3292
3341
  // semantic limit
3293
3342
  // fts_rank (ts_rank) call
@@ -3298,13 +3347,26 @@ var vectorSearch = async ({
3298
3347
  fullTextWeight,
3299
3348
  rrfK,
3300
3349
  semanticWeight,
3350
+ // WHERE clause hybrid_score filter
3351
+ rrfK,
3352
+ fullTextWeight,
3353
+ rrfK,
3354
+ semanticWeight,
3355
+ cutoffs?.hybrid || 0,
3356
+ // Additional cutoff filters in main WHERE clause
3357
+ language,
3358
+ query,
3359
+ cutoffs?.tsvector || 0,
3360
+ // tsvector cutoff for results from semantic CTE
3361
+ cutoffs?.cosineDistance || 0,
3362
+ // cosine distance cutoff for results from full_text CTE
3301
3363
  matchCount
3302
3364
  // final limit
3303
3365
  ];
3304
3366
  resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3305
3367
  }
3306
3368
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
3307
- resultChunks = resultChunks.map((chunk) => ({
3369
+ let results = resultChunks.map((chunk) => ({
3308
3370
  chunk_content: chunk.content,
3309
3371
  chunk_index: chunk.chunk_index,
3310
3372
  chunk_id: chunk.chunk_id,
@@ -3321,37 +3383,135 @@ var vectorSearch = async ({
3321
3383
  name: table.name.singular,
3322
3384
  id: table.id || ""
3323
3385
  },
3324
- ...method === "cosineDistance" && { chunk_cosine_distance: chunk.cosine_distance },
3386
+ ...(method === "cosineDistance" || method === "hybridSearch") && { chunk_cosine_distance: chunk.cosine_distance },
3325
3387
  ...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
3326
- ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score }
3388
+ ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score * 1e4 / 100 }
3327
3389
  }));
3328
- if (resultChunks.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3390
+ if (results.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3329
3391
  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];
3392
+ const topScore = results[0]?.[scoreKey];
3393
+ const bottomScore = results[results.length - 1]?.[scoreKey];
3394
+ const medianScore = results[Math.floor(results.length / 2)]?.[scoreKey];
3333
3395
  console.log("[EXULU] Score distribution:", {
3334
3396
  method,
3335
- count: resultChunks.length,
3397
+ count: results.length,
3336
3398
  topScore: topScore?.toFixed(4),
3337
3399
  bottomScore: bottomScore?.toFixed(4),
3338
3400
  medianScore: medianScore?.toFixed(4)
3339
3401
  });
3340
- const adaptiveThreshold = topScore * 0.7;
3341
- const beforeFilterCount = resultChunks.length;
3342
- resultChunks = resultChunks.filter((chunk) => {
3402
+ const adaptiveThreshold = topScore ? topScore * 0.6 : 0;
3403
+ const beforeFilterCount = results.length;
3404
+ results = results.filter((chunk) => {
3343
3405
  const score = chunk[scoreKey];
3344
3406
  return score !== void 0 && score >= adaptiveThreshold;
3345
3407
  });
3346
- const filteredCount = beforeFilterCount - resultChunks.length;
3408
+ const filteredCount = beforeFilterCount - results.length;
3347
3409
  if (filteredCount > 0) {
3348
3410
  console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
3349
3411
  }
3350
3412
  }
3351
3413
  if (resultReranker && query) {
3352
- resultChunks = await resultReranker(resultChunks);
3353
3414
  }
3354
- resultChunks = resultChunks.slice(0, limit);
3415
+ results = results.slice(0, limit);
3416
+ if (expand?.before || expand?.after) {
3417
+ const expandedMap = /* @__PURE__ */ new Map();
3418
+ for (const chunk of results) {
3419
+ expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
3420
+ }
3421
+ if (expand?.before) {
3422
+ for (const chunk of results) {
3423
+ const indicesToFetch = Array.from(
3424
+ { length: expand.before },
3425
+ (_, i) => chunk.chunk_index - expand.before + i
3426
+ ).filter((index) => index >= 0);
3427
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3428
+ await Promise.all(indicesToFetch.map(async (index) => {
3429
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3430
+ return;
3431
+ }
3432
+ const expandedChunk = await db3(chunksTable).where({
3433
+ source: chunk.item_id,
3434
+ chunk_index: index
3435
+ }).first();
3436
+ if (expandedChunk) {
3437
+ if (expandedChunk) {
3438
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3439
+ chunk_content: expandedChunk.content,
3440
+ chunk_index: expandedChunk.chunk_index,
3441
+ chunk_id: expandedChunk.id,
3442
+ chunk_source: expandedChunk.source,
3443
+ chunk_metadata: expandedChunk.metadata,
3444
+ chunk_created_at: expandedChunk.createdAt,
3445
+ chunk_updated_at: expandedChunk.updatedAt,
3446
+ item_updated_at: chunk.item_updated_at,
3447
+ item_created_at: chunk.item_created_at,
3448
+ item_id: chunk.item_id,
3449
+ item_external_id: chunk.item_external_id,
3450
+ item_name: chunk.item_name,
3451
+ chunk_cosine_distance: 0,
3452
+ chunk_fts_rank: 0,
3453
+ chunk_hybrid_score: 0,
3454
+ context: {
3455
+ name: table.name.singular,
3456
+ id: table.id || ""
3457
+ }
3458
+ });
3459
+ }
3460
+ }
3461
+ }));
3462
+ }
3463
+ }
3464
+ if (expand?.after) {
3465
+ for (const chunk of results) {
3466
+ const indicesToFetch = Array.from(
3467
+ { length: expand.after },
3468
+ (_, i) => chunk.chunk_index + i + 1
3469
+ );
3470
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3471
+ await Promise.all(indicesToFetch.map(async (index) => {
3472
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3473
+ return;
3474
+ }
3475
+ const expandedChunk = await db3(chunksTable).where({
3476
+ source: chunk.item_id,
3477
+ chunk_index: index
3478
+ }).first();
3479
+ if (expandedChunk) {
3480
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3481
+ chunk_content: expandedChunk.content,
3482
+ chunk_index: expandedChunk.chunk_index,
3483
+ chunk_id: expandedChunk.id,
3484
+ chunk_source: expandedChunk.source,
3485
+ chunk_metadata: expandedChunk.metadata,
3486
+ chunk_created_at: expandedChunk.createdAt,
3487
+ chunk_updated_at: expandedChunk.updatedAt,
3488
+ item_updated_at: chunk.item_updated_at,
3489
+ item_created_at: chunk.item_created_at,
3490
+ item_id: chunk.item_id,
3491
+ item_external_id: chunk.item_external_id,
3492
+ item_name: chunk.item_name,
3493
+ chunk_cosine_distance: 0,
3494
+ chunk_fts_rank: 0,
3495
+ chunk_hybrid_score: 0,
3496
+ context: {
3497
+ name: table.name.singular,
3498
+ id: table.id || ""
3499
+ }
3500
+ });
3501
+ }
3502
+ }));
3503
+ }
3504
+ }
3505
+ results = Array.from(expandedMap.values());
3506
+ results = results.sort((a, b) => {
3507
+ if (a.item_id !== b.item_id) {
3508
+ return a.item_id.localeCompare(b.item_id);
3509
+ }
3510
+ const aIndex = Number(a.chunk_index);
3511
+ const bIndex = Number(b.chunk_index);
3512
+ return aIndex - bIndex;
3513
+ });
3514
+ }
3355
3515
  await updateStatistic({
3356
3516
  name: "count",
3357
3517
  label: table.name.singular,
@@ -3369,7 +3529,7 @@ var vectorSearch = async ({
3369
3529
  id: table.id || "",
3370
3530
  embedder: embedder.name
3371
3531
  },
3372
- chunks: resultChunks
3532
+ chunks: results
3373
3533
  };
3374
3534
  };
3375
3535
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3542,7 +3702,7 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3542
3702
  `;
3543
3703
  if (table.type === "items") {
3544
3704
  typeDefs += `
3545
- ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}VectorSearchResult
3705
+ ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}], cutoffs: SearchCutoffs, expand: SearchExpand): ${tableNameSingular}VectorSearchResult
3546
3706
  `;
3547
3707
  }
3548
3708
  mutationDefs += `
@@ -3595,6 +3755,17 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3595
3755
  tsvector
3596
3756
  }
3597
3757
 
3758
+ input SearchCutoffs {
3759
+ cosineDistance: Float
3760
+ hybrid: Float
3761
+ tsvector: Float
3762
+ }
3763
+
3764
+ input SearchExpand {
3765
+ before: Int
3766
+ after: Int
3767
+ }
3768
+
3598
3769
  type ${tableNameSingular}VectorSearchResult {
3599
3770
  chunks: [${tableNameSingular}VectorSearchChunk!]!
3600
3771
  context: VectoSearchResultContext!
@@ -4486,6 +4657,18 @@ var getPresignedUrl = async (bucket, key, config) => {
4486
4657
  );
4487
4658
  return url;
4488
4659
  };
4660
+ function sanitizeMetadata(metadata) {
4661
+ if (!metadata) return void 0;
4662
+ const sanitized = {};
4663
+ for (const [key, value] of Object.entries(metadata)) {
4664
+ if (typeof value === "string") {
4665
+ sanitized[key] = encodeURIComponent(value);
4666
+ } else {
4667
+ sanitized[key] = String(value);
4668
+ }
4669
+ }
4670
+ return sanitized;
4671
+ }
4489
4672
  var addGeneralPrefixToKey = (keyPath, config) => {
4490
4673
  if (!config.fileUploads) {
4491
4674
  throw new Error("File uploads are not configured");
@@ -4521,19 +4704,41 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
4521
4704
  const client2 = getS3Client(config);
4522
4705
  let defaultBucket = config.fileUploads.s3Bucket;
4523
4706
  let key = fileName;
4524
- key = addGeneralPrefixToKey(key, config);
4525
4707
  key = addUserPrefixToKey(key, user || "api");
4526
- console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
4708
+ key = addGeneralPrefixToKey(key, config);
4709
+ const sanitizedMetadata = sanitizeMetadata(options.metadata);
4527
4710
  const command = new PutObjectCommand({
4528
4711
  Bucket: customBucket || defaultBucket,
4529
4712
  Key: key,
4530
4713
  Body: file,
4531
4714
  ContentType: options.contentType,
4532
- Metadata: options.metadata,
4715
+ Metadata: sanitizedMetadata,
4533
4716
  ContentLength: file.byteLength
4534
4717
  });
4535
- await client2.send(command);
4536
- console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
4718
+ const maxRetries = 3;
4719
+ let lastError = null;
4720
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
4721
+ try {
4722
+ await client2.send(command);
4723
+ break;
4724
+ } catch (error) {
4725
+ lastError = error;
4726
+ if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
4727
+ if (attempt < maxRetries) {
4728
+ const backoffMs = Math.pow(2, attempt) * 1e3;
4729
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
4730
+ s3Client = void 0;
4731
+ getS3Client(config);
4732
+ continue;
4733
+ }
4734
+ } else {
4735
+ throw error;
4736
+ }
4737
+ }
4738
+ }
4739
+ if (lastError) {
4740
+ throw lastError;
4741
+ }
4537
4742
  return addBucketPrefixToKey(
4538
4743
  key,
4539
4744
  customBucket || defaultBucket
@@ -4605,6 +4810,8 @@ var createUppyRoutes = async (app, contexts, config) => {
4605
4810
  res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
4606
4811
  return;
4607
4812
  }
4813
+ key = key.replace(`${bucket}/`, "");
4814
+ console.log("[EXULU] deleting file from s3 into bucket", bucket, "with key", key);
4608
4815
  const client2 = getS3Client(config);
4609
4816
  const command = new DeleteObjectCommand({
4610
4817
  Bucket: bucket,
@@ -4728,7 +4935,7 @@ var createUppyRoutes = async (app, contexts, config) => {
4728
4935
  const client2 = getS3Client(config);
4729
4936
  const command = new ListObjectsV2Command({
4730
4937
  Bucket: config.fileUploads.s3Bucket,
4731
- Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}${authenticationResult.user.id}`,
4938
+ Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}user_${authenticationResult.user.id}`,
4732
4939
  MaxKeys: 9,
4733
4940
  ...req.query.continuationToken && { ContinuationToken: req.query.continuationToken }
4734
4941
  });
@@ -4740,7 +4947,17 @@ var createUppyRoutes = async (app, contexts, config) => {
4740
4947
  search.toLowerCase()
4741
4948
  ));
4742
4949
  }
4743
- res.json(response);
4950
+ res.json({
4951
+ ...response,
4952
+ Contents: response.Contents?.map((content) => {
4953
+ return {
4954
+ ...content,
4955
+ // For consistency and to support multi-bucket environments
4956
+ // we prepend the bucket name to the key here.
4957
+ Key: `${config.fileUploads?.s3Bucket}/${content.Key}`
4958
+ };
4959
+ })
4960
+ });
4744
4961
  res.end();
4745
4962
  });
4746
4963
  app.get("/s3/sts", (req, res, next) => {
@@ -4801,8 +5018,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4801
5018
  const { filename, contentType } = extractFileParameters(req);
4802
5019
  validateFileParameters(filename, contentType);
4803
5020
  const key = generateS3Key2(filename);
4804
- let fullKey = addGeneralPrefixToKey(key, config);
4805
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5021
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5022
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5023
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4806
5024
  getSignedUrl(
4807
5025
  getS3Client(config),
4808
5026
  new PutObjectCommand({
@@ -4856,8 +5074,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4856
5074
  return res.status(400).json({ error: "s3: content type must be a string" });
4857
5075
  }
4858
5076
  const key = `${randomUUID()}-_EXULU_${filename}`;
4859
- let fullKey = addGeneralPrefixToKey(key, config);
4860
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5077
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5078
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5079
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4861
5080
  const params = {
4862
5081
  Bucket: config.fileUploads.s3Bucket,
4863
5082
  Key: fullKey,
@@ -5108,7 +5327,10 @@ var createProjectRetrievalTool = async ({
5108
5327
  };
5109
5328
  var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project) => {
5110
5329
  if (!currentTools) return {};
5111
- if (!allExuluTools) return {};
5330
+ if (!allExuluTools) {
5331
+ allExuluTools = [];
5332
+ }
5333
+ ;
5112
5334
  if (!contexts) {
5113
5335
  contexts = [];
5114
5336
  }
@@ -5142,6 +5364,7 @@ var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, pro
5142
5364
  ...cur.tool,
5143
5365
  description,
5144
5366
  async *execute(inputs, options) {
5367
+ console.log("[EXULU] Executing tool", cur.name, "with inputs", inputs, "and options", options);
5145
5368
  if (!cur.tool?.execute) {
5146
5369
  console.error("[EXULU] Tool execute function is undefined.", cur.tool);
5147
5370
  throw new Error("Tool execute function is undefined.");
@@ -5993,6 +6216,68 @@ var ExuluTool2 = class {
5993
6216
  execute: execute2
5994
6217
  });
5995
6218
  }
6219
+ execute = async ({
6220
+ agent,
6221
+ config,
6222
+ user,
6223
+ inputs,
6224
+ project
6225
+ }) => {
6226
+ const agentInstance = await loadAgent(agent);
6227
+ if (!agentInstance) {
6228
+ throw new Error("Agent not found.");
6229
+ }
6230
+ const { db: db3 } = await postgresClient();
6231
+ let providerapikey;
6232
+ const variableName = agentInstance.providerapikey;
6233
+ if (variableName) {
6234
+ console.log("[EXULU] provider api key variable name", variableName);
6235
+ const variable = await db3.from("variables").where({ name: variableName }).first();
6236
+ if (!variable) {
6237
+ throw new Error("Provider API key variable not found for " + agentInstance.name + " (" + agentInstance.id + ").");
6238
+ }
6239
+ providerapikey = variable.value;
6240
+ if (!variable.encrypted) {
6241
+ throw new Error("Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys.");
6242
+ }
6243
+ if (variable.encrypted) {
6244
+ const bytes = CryptoJS2.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6245
+ providerapikey = bytes.toString(CryptoJS2.enc.Utf8);
6246
+ }
6247
+ }
6248
+ const tools = await convertToolsArrayToObject(
6249
+ [this],
6250
+ [],
6251
+ agentInstance.tools,
6252
+ providerapikey,
6253
+ void 0,
6254
+ user,
6255
+ config,
6256
+ void 0,
6257
+ void 0,
6258
+ project
6259
+ );
6260
+ const tool2 = tools[sanitizeName(this.name)] || tools[this.name] || tools[this.id];
6261
+ if (!tool2?.execute) {
6262
+ throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
6263
+ }
6264
+ console.log("[EXULU] Tool found", this.name);
6265
+ const generator = tool2.execute(inputs, {
6266
+ toolCallId: this.id + "_" + randomUUID2(),
6267
+ messages: []
6268
+ });
6269
+ let lastValue;
6270
+ for await (const chunk of generator) {
6271
+ lastValue = chunk;
6272
+ }
6273
+ if (typeof lastValue === "string") {
6274
+ lastValue = JSON.parse(lastValue);
6275
+ }
6276
+ if (lastValue?.result && typeof lastValue.result === "string") {
6277
+ lastValue.result = JSON.parse(lastValue.result);
6278
+ }
6279
+ return lastValue;
6280
+ };
5996
6281
  };
5997
6282
  var getTableName = (id) => {
5998
6283
  return sanitizeName(id) + "_items";
@@ -6073,7 +6358,16 @@ var ExuluContext = class {
6073
6358
  calculateVectors: "manual",
6074
6359
  language: "english",
6075
6360
  defaultRightsMode: "private",
6076
- maxRetrievalResults: 10
6361
+ maxRetrievalResults: 10,
6362
+ expand: {
6363
+ before: 0,
6364
+ after: 0
6365
+ },
6366
+ cutoffs: {
6367
+ cosineDistance: 0.5,
6368
+ tsvector: 0.5,
6369
+ hybrid: 0.5
6370
+ }
6077
6371
  };
6078
6372
  this.description = description;
6079
6373
  this.embedder = embedder;
@@ -6088,6 +6382,23 @@ var ExuluContext = class {
6088
6382
  if (!this.processor) {
6089
6383
  throw new Error(`Processor is not set for this context: ${this.id}.`);
6090
6384
  }
6385
+ if (this.processor.filter) {
6386
+ const result = await this.processor.filter({
6387
+ item,
6388
+ user,
6389
+ role,
6390
+ utils: {
6391
+ storage: exuluStorage
6392
+ },
6393
+ exuluConfig
6394
+ });
6395
+ if (!result) {
6396
+ return {
6397
+ result: void 0,
6398
+ job: void 0
6399
+ };
6400
+ }
6401
+ }
6091
6402
  const queue = await this.processor.config?.queue;
6092
6403
  if (queue?.queue.name) {
6093
6404
  console.log("[EXULU] processor is in queue mode, scheduling job.");
@@ -6147,7 +6458,9 @@ var ExuluContext = class {
6147
6458
  role: options.role,
6148
6459
  context: this,
6149
6460
  db: db3,
6150
- limit: options?.limit || this.configuration.maxRetrievalResults || 10
6461
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10,
6462
+ cutoffs: options.cutoffs,
6463
+ expand: options.expand
6151
6464
  });
6152
6465
  return result;
6153
6466
  };