@exulu/backend 1.41.0 → 1.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2614,7 +2614,7 @@ var applyAccessControl = (table, query, user, field_prefix) => {
2614
2614
  }
2615
2615
  console.log("[EXULU] user.role", user?.role);
2616
2616
  console.log("[EXULU] table.name.plural", table.name.plural);
2617
- if (!user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2617
+ if (user && !user?.super_admin && (!user?.role || !(table.name.plural === "agents" && (user.role.agents === "read" || user.role.agents === "write")) && !(table.name.plural === "workflow_templates" && (user.role.workflows === "read" || user.role.workflows === "write")) && !(table.name.plural === "variables" && (user.role.variables === "read" || user.role.variables === "write")) && !(table.name.plural === "users" && (user.role.users === "read" || user.role.users === "write")) && !((table.name.plural === "test_cases" || table.name.plural === "eval_sets" || table.name.plural === "eval_runs") && (user.role.evals === "read" || user.role.evals === "write")))) {
2618
2618
  console.error("==== Access control error: no role found or no access to entity type. ====");
2619
2619
  throw new Error("Access control error: no role found or no access to entity type.");
2620
2620
  }
@@ -2623,17 +2623,23 @@ var applyAccessControl = (table, query, user, field_prefix) => {
2623
2623
  if (!hasRBAC) {
2624
2624
  return query;
2625
2625
  }
2626
+ if (user?.super_admin) {
2627
+ return query;
2628
+ }
2626
2629
  const prefix = field_prefix ? field_prefix + "." : "";
2630
+ console.log("[EXULU] applying access control with this prefix", prefix);
2627
2631
  try {
2628
2632
  query = query.where(function() {
2629
2633
  this.where(`${prefix}rights_mode`, "public");
2630
- this.orWhere(`${prefix}created_by`, user.id);
2631
- this.orWhere(function() {
2632
- this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2633
- this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "User").where("rbac.user_id", user.id);
2634
+ if (user) {
2635
+ this.orWhere(`${prefix}created_by`, user.id);
2636
+ this.orWhere(function() {
2637
+ this.where(`${prefix}rights_mode`, "users").whereExists(function() {
2638
+ this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "User").where("rbac.user_id", user.id);
2639
+ });
2634
2640
  });
2635
- });
2636
- if (user.role) {
2641
+ }
2642
+ if (user?.role) {
2637
2643
  this.orWhere(function() {
2638
2644
  this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
2639
2645
  this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "Role").where("rbac.role_id", user.role.id);
@@ -3030,7 +3036,7 @@ var paginationRequest = async ({
3030
3036
  const pageCount = Math.ceil(itemCount / limit);
3031
3037
  const currentPage = page;
3032
3038
  const hasPreviousPage = currentPage > 1;
3033
- const hasNextPage = currentPage < pageCount - 1;
3039
+ const hasNextPage = currentPage <= pageCount - 1;
3034
3040
  let dataQuery = db3(tableName);
3035
3041
  dataQuery = applyFilters(dataQuery, filters, table);
3036
3042
  dataQuery = applyAccessControl(table, dataQuery, user);
@@ -3159,7 +3165,9 @@ function createQueries(table, agents, tools, contexts) {
3159
3165
  method: args.method,
3160
3166
  user: context.user,
3161
3167
  role: context.user?.role?.id,
3162
- trigger: "api"
3168
+ trigger: "api",
3169
+ cutoffs: args.cutoffs,
3170
+ expand: args.expand
3163
3171
  });
3164
3172
  };
3165
3173
  }
@@ -3176,7 +3184,9 @@ var vectorSearch = async ({
3176
3184
  method,
3177
3185
  user,
3178
3186
  role,
3179
- trigger
3187
+ trigger,
3188
+ cutoffs,
3189
+ expand
3180
3190
  }) => {
3181
3191
  const table = contextToTableDefinition(context);
3182
3192
  console.log("[EXULU] Called vector search.", {
@@ -3188,10 +3198,12 @@ var vectorSearch = async ({
3188
3198
  query,
3189
3199
  method,
3190
3200
  user,
3191
- role
3201
+ role,
3202
+ cutoffs,
3203
+ expand
3192
3204
  });
3193
- if (limit > 50) {
3194
- throw new Error("Limit cannot be greater than 50.");
3205
+ if (limit > 250) {
3206
+ throw new Error("Limit cannot be greater than 1000.");
3195
3207
  }
3196
3208
  if (!query) {
3197
3209
  throw new Error("Query is required.");
@@ -3208,6 +3220,15 @@ var vectorSearch = async ({
3208
3220
  }
3209
3221
  const mainTable = getTableName(id);
3210
3222
  const chunksTable = getChunksTableName(id);
3223
+ cutoffs = {
3224
+ cosineDistance: cutoffs?.cosineDistance || context.configuration?.cutoffs?.cosineDistance || 0,
3225
+ tsvector: cutoffs?.tsvector || context.configuration?.cutoffs?.tsvector || 0,
3226
+ hybrid: cutoffs?.hybrid ? (cutoffs?.hybrid ?? 0) / 100 : context.configuration?.cutoffs ? (context.configuration?.cutoffs?.hybrid ?? 0) / 100 : 0
3227
+ };
3228
+ expand = {
3229
+ before: expand?.before || context.configuration?.expand?.before || 0,
3230
+ after: expand?.after || context.configuration?.expand?.after || 0
3231
+ };
3211
3232
  let chunksQuery = db3(chunksTable + " as chunks").select([
3212
3233
  "chunks.id as chunk_id",
3213
3234
  "chunks.source",
@@ -3242,22 +3263,30 @@ var vectorSearch = async ({
3242
3263
  const vectorStr = `ARRAY[${vector.join(",")}]`;
3243
3264
  const vectorExpr = `${vectorStr}::vector`;
3244
3265
  const language = configuration.language || "english";
3266
+ console.log("[EXULU] Vector search params:", { method, query, cutoffs });
3245
3267
  let resultChunks = [];
3246
3268
  switch (method) {
3247
3269
  case "tsvector":
3248
3270
  chunksQuery.limit(limit * 2);
3271
+ const tokens = query.trim().split(/\s+/).filter((t) => t.length > 0);
3272
+ const sanitizedTokens = tokens.flatMap((t) => {
3273
+ return t.split(/[^\w]+/).filter((part) => part.length > 0);
3274
+ });
3275
+ const orQuery = sanitizedTokens.join(" | ");
3276
+ console.log("[EXULU] FTS query transformation:", { original: query, tokens, sanitizedTokens, orQuery, cutoff: cutoffs?.tsvector });
3249
3277
  chunksQuery.select(db3.raw(
3250
- `ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
3251
- [language, query]
3278
+ `ts_rank(chunks.fts, to_tsquery(?, ?)) as fts_rank`,
3279
+ [language, orQuery]
3252
3280
  )).whereRaw(
3253
- `chunks.fts @@ websearch_to_tsquery(?, ?)`,
3254
- [language, query]
3281
+ `(chunks.fts @@ to_tsquery(?, ?)) AND (items.archived IS FALSE OR items.archived IS NULL)`,
3282
+ [language, orQuery]
3255
3283
  ).orderByRaw(`fts_rank DESC`);
3284
+ console.log("[EXULU] FTS query SQL:", chunksQuery.toQuery());
3256
3285
  resultChunks = await chunksQuery;
3257
3286
  break;
3258
3287
  case "cosineDistance":
3259
3288
  chunksQuery.limit(limit * 2);
3260
- chunksQuery.whereNotNull(`chunks.embedding`);
3289
+ chunksQuery.whereNotNull(`chunks.embedding`).whereRaw(`(items.archived IS FALSE OR items.archived IS NULL)`);
3261
3290
  console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
3262
3291
  chunksQuery.select(
3263
3292
  db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
@@ -3265,11 +3294,12 @@ var vectorSearch = async ({
3265
3294
  chunksQuery.orderByRaw(
3266
3295
  `chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
3267
3296
  );
3297
+ chunksQuery.whereRaw(`(1 - (chunks.embedding <=> ${vectorExpr}) >= ?)`, [cutoffs?.cosineDistance || 0]);
3268
3298
  resultChunks = await chunksQuery;
3269
3299
  break;
3270
3300
  case "hybridSearch":
3271
- const matchCount = Math.min(limit * 2, 100);
3272
- const fullTextWeight = 1;
3301
+ const matchCount = Math.min(limit * 2);
3302
+ const fullTextWeight = 2;
3273
3303
  const semanticWeight = 1;
3274
3304
  const rrfK = 50;
3275
3305
  const hybridSQL = `
@@ -3278,12 +3308,15 @@ var vectorSearch = async ({
3278
3308
  chunks.id,
3279
3309
  chunks.source,
3280
3310
  row_number() OVER (
3281
- ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
3311
+ ORDER BY ts_rank(chunks.fts, plainto_tsquery(?, ?)) DESC
3282
3312
  ) AS rank_ix
3283
3313
  FROM ${chunksTable} as chunks
3284
- WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
3314
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3315
+ WHERE chunks.fts @@ plainto_tsquery(?, ?)
3316
+ AND ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?
3317
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3285
3318
  ORDER BY rank_ix
3286
- LIMIT LEAST(?, 15) * 2
3319
+ LIMIT LEAST(?, 250) * 2
3287
3320
  ),
3288
3321
  semantic AS (
3289
3322
  SELECT
@@ -3293,9 +3326,12 @@ var vectorSearch = async ({
3293
3326
  ORDER BY chunks.embedding <=> ${vectorExpr} ASC
3294
3327
  ) AS rank_ix
3295
3328
  FROM ${chunksTable} as chunks
3329
+ LEFT JOIN ${mainTable} as items ON items.id = chunks.source
3296
3330
  WHERE chunks.embedding IS NOT NULL
3331
+ AND (1 - (chunks.embedding <=> ${vectorExpr})) >= ?
3332
+ AND (items.archived IS FALSE OR items.archived IS NULL)
3297
3333
  ORDER BY rank_ix
3298
- LIMIT LEAST(?, 50) * 2
3334
+ LIMIT LEAST(?, 250) * 2
3299
3335
  )
3300
3336
  SELECT
3301
3337
  items.id as item_id,
@@ -3311,7 +3347,7 @@ var vectorSearch = async ({
3311
3347
  items."updatedAt" as item_updated_at,
3312
3348
  items."createdAt" as item_created_at,
3313
3349
  /* Per-signal scores for introspection */
3314
- ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
3350
+ ts_rank(chunks.fts, plainto_tsquery(?, ?)) AS fts_rank,
3315
3351
  (1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
3316
3352
 
3317
3353
  /* Hybrid RRF score */
@@ -3328,18 +3364,31 @@ var vectorSearch = async ({
3328
3364
  ON COALESCE(ft.id, se.id) = chunks.id
3329
3365
  JOIN ${mainTable} as items
3330
3366
  ON items.id = chunks.source
3367
+ WHERE (
3368
+ COALESCE(1.0 / (? + ft.rank_ix), 0.0) * ?
3369
+ +
3370
+ COALESCE(1.0 / (? + se.rank_ix), 0.0) * ?
3371
+ ) >= ?
3372
+ AND (chunks.fts IS NULL OR ts_rank(chunks.fts, plainto_tsquery(?, ?)) > ?)
3373
+ AND (chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)
3331
3374
  ORDER BY hybrid_score DESC
3332
- LIMIT LEAST(?, 50)
3375
+ LIMIT LEAST(?, 250)
3333
3376
  OFFSET 0
3334
3377
  `;
3335
3378
  const bindings = [
3336
- // full_text: websearch_to_tsquery(lang, query) in rank and where
3379
+ // full_text: plainto_tsquery(lang, query) in rank and where
3337
3380
  language,
3338
3381
  query,
3339
3382
  language,
3340
3383
  query,
3384
+ language,
3385
+ query,
3386
+ cutoffs?.tsvector || 0,
3387
+ // full_text tsvector cutoff
3341
3388
  matchCount,
3342
3389
  // full_text limit
3390
+ cutoffs?.cosineDistance || 0,
3391
+ // semantic cosine distance cutoff
3343
3392
  matchCount,
3344
3393
  // semantic limit
3345
3394
  // fts_rank (ts_rank) call
@@ -3350,13 +3399,26 @@ var vectorSearch = async ({
3350
3399
  fullTextWeight,
3351
3400
  rrfK,
3352
3401
  semanticWeight,
3402
+ // WHERE clause hybrid_score filter
3403
+ rrfK,
3404
+ fullTextWeight,
3405
+ rrfK,
3406
+ semanticWeight,
3407
+ cutoffs?.hybrid || 0,
3408
+ // Additional cutoff filters in main WHERE clause
3409
+ language,
3410
+ query,
3411
+ cutoffs?.tsvector || 0,
3412
+ // tsvector cutoff for results from semantic CTE
3413
+ cutoffs?.cosineDistance || 0,
3414
+ // cosine distance cutoff for results from full_text CTE
3353
3415
  matchCount
3354
3416
  // final limit
3355
3417
  ];
3356
3418
  resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
3357
3419
  }
3358
3420
  console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
3359
- resultChunks = resultChunks.map((chunk) => ({
3421
+ let results = resultChunks.map((chunk) => ({
3360
3422
  chunk_content: chunk.content,
3361
3423
  chunk_index: chunk.chunk_index,
3362
3424
  chunk_id: chunk.chunk_id,
@@ -3373,37 +3435,135 @@ var vectorSearch = async ({
3373
3435
  name: table.name.singular,
3374
3436
  id: table.id || ""
3375
3437
  },
3376
- ...method === "cosineDistance" && { chunk_cosine_distance: chunk.cosine_distance },
3438
+ ...(method === "cosineDistance" || method === "hybridSearch") && { chunk_cosine_distance: chunk.cosine_distance },
3377
3439
  ...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
3378
- ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score }
3440
+ ...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score * 1e4 / 100 }
3379
3441
  }));
3380
- if (resultChunks.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3442
+ if (results.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
3381
3443
  const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
3382
- const topScore = resultChunks[0][scoreKey];
3383
- const bottomScore = resultChunks[resultChunks.length - 1][scoreKey];
3384
- const medianScore = resultChunks[Math.floor(resultChunks.length / 2)][scoreKey];
3444
+ const topScore = results[0]?.[scoreKey];
3445
+ const bottomScore = results[results.length - 1]?.[scoreKey];
3446
+ const medianScore = results[Math.floor(results.length / 2)]?.[scoreKey];
3385
3447
  console.log("[EXULU] Score distribution:", {
3386
3448
  method,
3387
- count: resultChunks.length,
3449
+ count: results.length,
3388
3450
  topScore: topScore?.toFixed(4),
3389
3451
  bottomScore: bottomScore?.toFixed(4),
3390
3452
  medianScore: medianScore?.toFixed(4)
3391
3453
  });
3392
- const adaptiveThreshold = topScore * 0.7;
3393
- const beforeFilterCount = resultChunks.length;
3394
- resultChunks = resultChunks.filter((chunk) => {
3454
+ const adaptiveThreshold = topScore ? topScore * 0.6 : 0;
3455
+ const beforeFilterCount = results.length;
3456
+ results = results.filter((chunk) => {
3395
3457
  const score = chunk[scoreKey];
3396
3458
  return score !== void 0 && score >= adaptiveThreshold;
3397
3459
  });
3398
- const filteredCount = beforeFilterCount - resultChunks.length;
3460
+ const filteredCount = beforeFilterCount - results.length;
3399
3461
  if (filteredCount > 0) {
3400
3462
  console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
3401
3463
  }
3402
3464
  }
3403
3465
  if (resultReranker && query) {
3404
- resultChunks = await resultReranker(resultChunks);
3405
3466
  }
3406
- resultChunks = resultChunks.slice(0, limit);
3467
+ results = results.slice(0, limit);
3468
+ if (expand?.before || expand?.after) {
3469
+ const expandedMap = /* @__PURE__ */ new Map();
3470
+ for (const chunk of results) {
3471
+ expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
3472
+ }
3473
+ if (expand?.before) {
3474
+ for (const chunk of results) {
3475
+ const indicesToFetch = Array.from(
3476
+ { length: expand.before },
3477
+ (_, i) => chunk.chunk_index - expand.before + i
3478
+ ).filter((index) => index >= 0);
3479
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3480
+ await Promise.all(indicesToFetch.map(async (index) => {
3481
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3482
+ return;
3483
+ }
3484
+ const expandedChunk = await db3(chunksTable).where({
3485
+ source: chunk.item_id,
3486
+ chunk_index: index
3487
+ }).first();
3488
+ if (expandedChunk) {
3489
+ if (expandedChunk) {
3490
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3491
+ chunk_content: expandedChunk.content,
3492
+ chunk_index: expandedChunk.chunk_index,
3493
+ chunk_id: expandedChunk.id,
3494
+ chunk_source: expandedChunk.source,
3495
+ chunk_metadata: expandedChunk.metadata,
3496
+ chunk_created_at: expandedChunk.createdAt,
3497
+ chunk_updated_at: expandedChunk.updatedAt,
3498
+ item_updated_at: chunk.item_updated_at,
3499
+ item_created_at: chunk.item_created_at,
3500
+ item_id: chunk.item_id,
3501
+ item_external_id: chunk.item_external_id,
3502
+ item_name: chunk.item_name,
3503
+ chunk_cosine_distance: 0,
3504
+ chunk_fts_rank: 0,
3505
+ chunk_hybrid_score: 0,
3506
+ context: {
3507
+ name: table.name.singular,
3508
+ id: table.id || ""
3509
+ }
3510
+ });
3511
+ }
3512
+ }
3513
+ }));
3514
+ }
3515
+ }
3516
+ if (expand?.after) {
3517
+ for (const chunk of results) {
3518
+ const indicesToFetch = Array.from(
3519
+ { length: expand.after },
3520
+ (_, i) => chunk.chunk_index + i + 1
3521
+ );
3522
+ console.log("[EXULU] Indices to fetch:", indicesToFetch);
3523
+ await Promise.all(indicesToFetch.map(async (index) => {
3524
+ if (expandedMap.has(`${chunk.item_id}-${index}`)) {
3525
+ return;
3526
+ }
3527
+ const expandedChunk = await db3(chunksTable).where({
3528
+ source: chunk.item_id,
3529
+ chunk_index: index
3530
+ }).first();
3531
+ if (expandedChunk) {
3532
+ expandedMap.set(`${chunk.item_id}-${index}`, {
3533
+ chunk_content: expandedChunk.content,
3534
+ chunk_index: expandedChunk.chunk_index,
3535
+ chunk_id: expandedChunk.id,
3536
+ chunk_source: expandedChunk.source,
3537
+ chunk_metadata: expandedChunk.metadata,
3538
+ chunk_created_at: expandedChunk.createdAt,
3539
+ chunk_updated_at: expandedChunk.updatedAt,
3540
+ item_updated_at: chunk.item_updated_at,
3541
+ item_created_at: chunk.item_created_at,
3542
+ item_id: chunk.item_id,
3543
+ item_external_id: chunk.item_external_id,
3544
+ item_name: chunk.item_name,
3545
+ chunk_cosine_distance: 0,
3546
+ chunk_fts_rank: 0,
3547
+ chunk_hybrid_score: 0,
3548
+ context: {
3549
+ name: table.name.singular,
3550
+ id: table.id || ""
3551
+ }
3552
+ });
3553
+ }
3554
+ }));
3555
+ }
3556
+ }
3557
+ results = Array.from(expandedMap.values());
3558
+ results = results.sort((a, b) => {
3559
+ if (a.item_id !== b.item_id) {
3560
+ return a.item_id.localeCompare(b.item_id);
3561
+ }
3562
+ const aIndex = Number(a.chunk_index);
3563
+ const bIndex = Number(b.chunk_index);
3564
+ return aIndex - bIndex;
3565
+ });
3566
+ }
3407
3567
  await updateStatistic({
3408
3568
  name: "count",
3409
3569
  label: table.name.singular,
@@ -3421,7 +3581,7 @@ var vectorSearch = async ({
3421
3581
  id: table.id || "",
3422
3582
  embedder: embedder.name
3423
3583
  },
3424
- chunks: resultChunks
3584
+ chunks: results
3425
3585
  };
3426
3586
  };
3427
3587
  var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
@@ -3594,7 +3754,7 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3594
3754
  `;
3595
3755
  if (table.type === "items") {
3596
3756
  typeDefs += `
3597
- ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}VectorSearchResult
3757
+ ${tableNamePlural}VectorSearch(query: String!, method: VectorMethodEnum!, filters: [Filter${tableNameSingularUpperCaseFirst}], cutoffs: SearchCutoffs, expand: SearchExpand): ${tableNameSingular}VectorSearchResult
3598
3758
  `;
3599
3759
  }
3600
3760
  mutationDefs += `
@@ -3647,6 +3807,17 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
3647
3807
  tsvector
3648
3808
  }
3649
3809
 
3810
+ input SearchCutoffs {
3811
+ cosineDistance: Float
3812
+ hybrid: Float
3813
+ tsvector: Float
3814
+ }
3815
+
3816
+ input SearchExpand {
3817
+ before: Int
3818
+ after: Int
3819
+ }
3820
+
3650
3821
  type ${tableNameSingular}VectorSearchResult {
3651
3822
  chunks: [${tableNameSingular}VectorSearchChunk!]!
3652
3823
  context: VectoSearchResultContext!
@@ -4519,6 +4690,18 @@ var getPresignedUrl = async (bucket, key, config) => {
4519
4690
  );
4520
4691
  return url;
4521
4692
  };
4693
+ function sanitizeMetadata(metadata) {
4694
+ if (!metadata) return void 0;
4695
+ const sanitized = {};
4696
+ for (const [key, value] of Object.entries(metadata)) {
4697
+ if (typeof value === "string") {
4698
+ sanitized[key] = encodeURIComponent(value);
4699
+ } else {
4700
+ sanitized[key] = String(value);
4701
+ }
4702
+ }
4703
+ return sanitized;
4704
+ }
4522
4705
  var addGeneralPrefixToKey = (keyPath, config) => {
4523
4706
  if (!config.fileUploads) {
4524
4707
  throw new Error("File uploads are not configured");
@@ -4554,19 +4737,41 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
4554
4737
  const client2 = getS3Client(config);
4555
4738
  let defaultBucket = config.fileUploads.s3Bucket;
4556
4739
  let key = fileName;
4557
- key = addGeneralPrefixToKey(key, config);
4558
4740
  key = addUserPrefixToKey(key, user || "api");
4559
- console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
4741
+ key = addGeneralPrefixToKey(key, config);
4742
+ const sanitizedMetadata = sanitizeMetadata(options.metadata);
4560
4743
  const command = new import_client_s3.PutObjectCommand({
4561
4744
  Bucket: customBucket || defaultBucket,
4562
4745
  Key: key,
4563
4746
  Body: file,
4564
4747
  ContentType: options.contentType,
4565
- Metadata: options.metadata,
4748
+ Metadata: sanitizedMetadata,
4566
4749
  ContentLength: file.byteLength
4567
4750
  });
4568
- await client2.send(command);
4569
- console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
4751
+ const maxRetries = 3;
4752
+ let lastError = null;
4753
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
4754
+ try {
4755
+ await client2.send(command);
4756
+ break;
4757
+ } catch (error) {
4758
+ lastError = error;
4759
+ if (error.name === "SignatureDoesNotMatch" || error.name === "InvalidAccessKeyId" || error.name === "AccessDenied") {
4760
+ if (attempt < maxRetries) {
4761
+ const backoffMs = Math.pow(2, attempt) * 1e3;
4762
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
4763
+ s3Client = void 0;
4764
+ getS3Client(config);
4765
+ continue;
4766
+ }
4767
+ } else {
4768
+ throw error;
4769
+ }
4770
+ }
4771
+ }
4772
+ if (lastError) {
4773
+ throw lastError;
4774
+ }
4570
4775
  return addBucketPrefixToKey(
4571
4776
  key,
4572
4777
  customBucket || defaultBucket
@@ -4638,6 +4843,8 @@ var createUppyRoutes = async (app, contexts, config) => {
4638
4843
  res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
4639
4844
  return;
4640
4845
  }
4846
+ key = key.replace(`${bucket}/`, "");
4847
+ console.log("[EXULU] deleting file from s3 into bucket", bucket, "with key", key);
4641
4848
  const client2 = getS3Client(config);
4642
4849
  const command = new import_client_s3.DeleteObjectCommand({
4643
4850
  Bucket: bucket,
@@ -4761,7 +4968,7 @@ var createUppyRoutes = async (app, contexts, config) => {
4761
4968
  const client2 = getS3Client(config);
4762
4969
  const command = new import_client_s3.ListObjectsV2Command({
4763
4970
  Bucket: config.fileUploads.s3Bucket,
4764
- Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}${authenticationResult.user.id}`,
4971
+ Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}user_${authenticationResult.user.id}`,
4765
4972
  MaxKeys: 9,
4766
4973
  ...req.query.continuationToken && { ContinuationToken: req.query.continuationToken }
4767
4974
  });
@@ -4773,7 +4980,17 @@ var createUppyRoutes = async (app, contexts, config) => {
4773
4980
  search.toLowerCase()
4774
4981
  ));
4775
4982
  }
4776
- res.json(response);
4983
+ res.json({
4984
+ ...response,
4985
+ Contents: response.Contents?.map((content) => {
4986
+ return {
4987
+ ...content,
4988
+ // For consistency and to support multi-bucket environments
4989
+ // we prepend the bucket name to the key here.
4990
+ Key: `${config.fileUploads?.s3Bucket}/${content.Key}`
4991
+ };
4992
+ })
4993
+ });
4777
4994
  res.end();
4778
4995
  });
4779
4996
  app.get("/s3/sts", (req, res, next) => {
@@ -4834,8 +5051,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4834
5051
  const { filename, contentType } = extractFileParameters(req);
4835
5052
  validateFileParameters(filename, contentType);
4836
5053
  const key = generateS3Key2(filename);
4837
- let fullKey = addGeneralPrefixToKey(key, config);
4838
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5054
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5055
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5056
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4839
5057
  (0, import_s3_request_presigner.getSignedUrl)(
4840
5058
  getS3Client(config),
4841
5059
  new import_client_s3.PutObjectCommand({
@@ -4889,8 +5107,9 @@ var createUppyRoutes = async (app, contexts, config) => {
4889
5107
  return res.status(400).json({ error: "s3: content type must be a string" });
4890
5108
  }
4891
5109
  const key = `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
4892
- let fullKey = addGeneralPrefixToKey(key, config);
4893
- fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
5110
+ let fullKey = addUserPrefixToKey(key, user.type === "api" ? "api" : user.id);
5111
+ fullKey = addGeneralPrefixToKey(fullKey, config);
5112
+ console.log("[EXULU] signing on server for user", user.id, "with key", fullKey);
4894
5113
  const params = {
4895
5114
  Bucket: config.fileUploads.s3Bucket,
4896
5115
  Key: fullKey,
@@ -5141,7 +5360,10 @@ var createProjectRetrievalTool = async ({
5141
5360
  };
5142
5361
  var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project) => {
5143
5362
  if (!currentTools) return {};
5144
- if (!allExuluTools) return {};
5363
+ if (!allExuluTools) {
5364
+ allExuluTools = [];
5365
+ }
5366
+ ;
5145
5367
  if (!contexts) {
5146
5368
  contexts = [];
5147
5369
  }
@@ -5175,6 +5397,7 @@ var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, pro
5175
5397
  ...cur.tool,
5176
5398
  description,
5177
5399
  async *execute(inputs, options) {
5400
+ console.log("[EXULU] Executing tool", cur.name, "with inputs", inputs, "and options", options);
5178
5401
  if (!cur.tool?.execute) {
5179
5402
  console.error("[EXULU] Tool execute function is undefined.", cur.tool);
5180
5403
  throw new Error("Tool execute function is undefined.");
@@ -6026,6 +6249,68 @@ var ExuluTool2 = class {
6026
6249
  execute: execute2
6027
6250
  });
6028
6251
  }
6252
+ execute = async ({
6253
+ agent,
6254
+ config,
6255
+ user,
6256
+ inputs,
6257
+ project
6258
+ }) => {
6259
+ const agentInstance = await loadAgent(agent);
6260
+ if (!agentInstance) {
6261
+ throw new Error("Agent not found.");
6262
+ }
6263
+ const { db: db3 } = await postgresClient();
6264
+ let providerapikey;
6265
+ const variableName = agentInstance.providerapikey;
6266
+ if (variableName) {
6267
+ console.log("[EXULU] provider api key variable name", variableName);
6268
+ const variable = await db3.from("variables").where({ name: variableName }).first();
6269
+ if (!variable) {
6270
+ throw new Error("Provider API key variable not found for " + agentInstance.name + " (" + agentInstance.id + ").");
6271
+ }
6272
+ providerapikey = variable.value;
6273
+ if (!variable.encrypted) {
6274
+ throw new Error("Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys.");
6275
+ }
6276
+ if (variable.encrypted) {
6277
+ const bytes = import_crypto_js2.default.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
6278
+ providerapikey = bytes.toString(import_crypto_js2.default.enc.Utf8);
6279
+ }
6280
+ }
6281
+ const tools = await convertToolsArrayToObject(
6282
+ [this],
6283
+ [],
6284
+ agentInstance.tools,
6285
+ providerapikey,
6286
+ void 0,
6287
+ user,
6288
+ config,
6289
+ void 0,
6290
+ void 0,
6291
+ project
6292
+ );
6293
+ const tool2 = tools[sanitizeName(this.name)] || tools[this.name] || tools[this.id];
6294
+ if (!tool2?.execute) {
6295
+ throw new Error("Tool " + sanitizeName(this.name) + " not found in " + JSON.stringify(tools));
6296
+ }
6297
+ console.log("[EXULU] Tool found", this.name);
6298
+ const generator = tool2.execute(inputs, {
6299
+ toolCallId: this.id + "_" + (0, import_node_crypto2.randomUUID)(),
6300
+ messages: []
6301
+ });
6302
+ let lastValue;
6303
+ for await (const chunk of generator) {
6304
+ lastValue = chunk;
6305
+ }
6306
+ if (typeof lastValue === "string") {
6307
+ lastValue = JSON.parse(lastValue);
6308
+ }
6309
+ if (lastValue?.result && typeof lastValue.result === "string") {
6310
+ lastValue.result = JSON.parse(lastValue.result);
6311
+ }
6312
+ return lastValue;
6313
+ };
6029
6314
  };
6030
6315
  var getTableName = (id) => {
6031
6316
  return sanitizeName(id) + "_items";
@@ -6106,7 +6391,16 @@ var ExuluContext = class {
6106
6391
  calculateVectors: "manual",
6107
6392
  language: "english",
6108
6393
  defaultRightsMode: "private",
6109
- maxRetrievalResults: 10
6394
+ maxRetrievalResults: 10,
6395
+ expand: {
6396
+ before: 0,
6397
+ after: 0
6398
+ },
6399
+ cutoffs: {
6400
+ cosineDistance: 0.5,
6401
+ tsvector: 0.5,
6402
+ hybrid: 0.5
6403
+ }
6110
6404
  };
6111
6405
  this.description = description;
6112
6406
  this.embedder = embedder;
@@ -6121,6 +6415,23 @@ var ExuluContext = class {
6121
6415
  if (!this.processor) {
6122
6416
  throw new Error(`Processor is not set for this context: ${this.id}.`);
6123
6417
  }
6418
+ if (this.processor.filter) {
6419
+ const result = await this.processor.filter({
6420
+ item,
6421
+ user,
6422
+ role,
6423
+ utils: {
6424
+ storage: exuluStorage
6425
+ },
6426
+ exuluConfig
6427
+ });
6428
+ if (!result) {
6429
+ return {
6430
+ result: void 0,
6431
+ job: void 0
6432
+ };
6433
+ }
6434
+ }
6124
6435
  const queue = await this.processor.config?.queue;
6125
6436
  if (queue?.queue.name) {
6126
6437
  console.log("[EXULU] processor is in queue mode, scheduling job.");
@@ -6180,7 +6491,9 @@ var ExuluContext = class {
6180
6491
  role: options.role,
6181
6492
  context: this,
6182
6493
  db: db3,
6183
- limit: options?.limit || this.configuration.maxRetrievalResults || 10
6494
+ limit: options?.limit || this.configuration.maxRetrievalResults || 10,
6495
+ cutoffs: options.cutoffs,
6496
+ expand: options.expand
6184
6497
  });
6185
6498
  return result;
6186
6499
  };