@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/CHANGELOG.md +2 -8
- package/dist/index.cjs +370 -57
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +370 -57
- package/package.json +1 -1
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
|
-
|
|
2631
|
-
|
|
2632
|
-
this.
|
|
2633
|
-
this.
|
|
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
|
|
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
|
|
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 >
|
|
3194
|
-
throw new Error("Limit cannot be greater than
|
|
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,
|
|
3251
|
-
[language,
|
|
3278
|
+
`ts_rank(chunks.fts, to_tsquery(?, ?)) as fts_rank`,
|
|
3279
|
+
[language, orQuery]
|
|
3252
3280
|
)).whereRaw(
|
|
3253
|
-
`chunks.fts @@
|
|
3254
|
-
[language,
|
|
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
|
|
3272
|
-
const fullTextWeight =
|
|
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,
|
|
3311
|
+
ORDER BY ts_rank(chunks.fts, plainto_tsquery(?, ?)) DESC
|
|
3282
3312
|
) AS rank_ix
|
|
3283
3313
|
FROM ${chunksTable} as chunks
|
|
3284
|
-
|
|
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(?,
|
|
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(?,
|
|
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,
|
|
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(?,
|
|
3375
|
+
LIMIT LEAST(?, 250)
|
|
3333
3376
|
OFFSET 0
|
|
3334
3377
|
`;
|
|
3335
3378
|
const bindings = [
|
|
3336
|
-
// full_text:
|
|
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
|
-
|
|
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 (
|
|
3442
|
+
if (results.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
|
|
3381
3443
|
const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
|
|
3382
|
-
const topScore =
|
|
3383
|
-
const bottomScore =
|
|
3384
|
-
const medianScore =
|
|
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:
|
|
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.
|
|
3393
|
-
const beforeFilterCount =
|
|
3394
|
-
|
|
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 -
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
4748
|
+
Metadata: sanitizedMetadata,
|
|
4566
4749
|
ContentLength: file.byteLength
|
|
4567
4750
|
});
|
|
4568
|
-
|
|
4569
|
-
|
|
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(
|
|
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 =
|
|
4838
|
-
fullKey =
|
|
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 =
|
|
4893
|
-
fullKey =
|
|
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)
|
|
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
|
};
|