@exulu/backend 1.40.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/CHANGELOG.md +2 -7
- package/dist/index.cjs +819 -442
- package/dist/index.d.cts +114 -23
- package/dist/index.d.ts +114 -23
- package/dist/index.js +819 -442
- package/package.json +1 -1
- package/types/models/context.ts +6 -6
- package/types/models/item.ts +1 -1
package/dist/index.cjs
CHANGED
|
@@ -48,7 +48,7 @@ __export(index_exports, {
|
|
|
48
48
|
ExuluUtils: () => ExuluUtils,
|
|
49
49
|
ExuluVariables: () => ExuluVariables,
|
|
50
50
|
db: () => db2,
|
|
51
|
-
logMetadata: () =>
|
|
51
|
+
logMetadata: () => logMetadata
|
|
52
52
|
});
|
|
53
53
|
module.exports = __toCommonJS(index_exports);
|
|
54
54
|
var import_config = require("dotenv/config");
|
|
@@ -203,21 +203,37 @@ async function postgresClient() {
|
|
|
203
203
|
database: dbName,
|
|
204
204
|
password: process.env.POSTGRES_DB_PASSWORD,
|
|
205
205
|
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
206
|
-
connectionTimeoutMillis:
|
|
206
|
+
connectionTimeoutMillis: 3e4,
|
|
207
|
+
// Increased from 10s to 30s to handle connection spikes
|
|
208
|
+
// PostgreSQL statement timeout (in milliseconds) - kills queries that run too long
|
|
209
|
+
// This prevents runaway queries from blocking connections
|
|
210
|
+
statement_timeout: 18e5,
|
|
211
|
+
// 30 minutes - should be longer than max job timeout (1200s = 20m)
|
|
212
|
+
// Connection idle timeout - how long pg client waits before timing out
|
|
213
|
+
query_timeout: 18e5
|
|
214
|
+
// 30 minutes
|
|
207
215
|
},
|
|
208
216
|
pool: {
|
|
209
|
-
min:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
min: 5,
|
|
218
|
+
// Increased from 2 to ensure enough connections available
|
|
219
|
+
max: 50,
|
|
220
|
+
// Increased from 20 to handle more concurrent operations with processor jobs
|
|
221
|
+
acquireTimeoutMillis: 6e4,
|
|
222
|
+
// Increased from 30s to 60s to handle pool contention
|
|
213
223
|
createTimeoutMillis: 3e4,
|
|
214
|
-
idleTimeoutMillis:
|
|
224
|
+
idleTimeoutMillis: 6e4,
|
|
225
|
+
// Increased to keep connections alive longer
|
|
215
226
|
reapIntervalMillis: 1e3,
|
|
216
227
|
createRetryIntervalMillis: 200,
|
|
217
228
|
// Log pool events to help debug connection issues
|
|
218
229
|
afterCreate: (conn, done) => {
|
|
219
230
|
console.log("[EXULU] New database connection created");
|
|
220
|
-
|
|
231
|
+
conn.query("SET statement_timeout = 1800000", (err) => {
|
|
232
|
+
if (err) {
|
|
233
|
+
console.error("[EXULU] Error setting statement_timeout:", err);
|
|
234
|
+
}
|
|
235
|
+
done(err, conn);
|
|
236
|
+
});
|
|
221
237
|
}
|
|
222
238
|
}
|
|
223
239
|
});
|
|
@@ -1467,6 +1483,10 @@ var addCoreFields = (schema) => {
|
|
|
1467
1483
|
field.name = field.name + "_s3key";
|
|
1468
1484
|
}
|
|
1469
1485
|
});
|
|
1486
|
+
schema.fields.push({
|
|
1487
|
+
name: "last_processed_at",
|
|
1488
|
+
type: "date"
|
|
1489
|
+
});
|
|
1470
1490
|
if (schema.RBAC) {
|
|
1471
1491
|
if (!schema.fields.some((field) => field.name === "rights_mode")) {
|
|
1472
1492
|
schema.fields.push({
|
|
@@ -2349,33 +2369,38 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2349
2369
|
}
|
|
2350
2370
|
};
|
|
2351
2371
|
if (table.type === "items") {
|
|
2352
|
-
if (table.
|
|
2353
|
-
|
|
2372
|
+
if (table.processor) {
|
|
2373
|
+
const contextItemProcessorMutation = async (context, items, user, role) => {
|
|
2374
|
+
let jobs = [];
|
|
2375
|
+
let results = [];
|
|
2376
|
+
await Promise.all(items.map(async (item) => {
|
|
2377
|
+
const result = await context.processField(
|
|
2378
|
+
"api",
|
|
2379
|
+
item,
|
|
2380
|
+
config,
|
|
2381
|
+
user,
|
|
2382
|
+
role
|
|
2383
|
+
);
|
|
2384
|
+
if (result.job) {
|
|
2385
|
+
jobs.push(result.job);
|
|
2386
|
+
}
|
|
2387
|
+
if (result.result) {
|
|
2388
|
+
results.push(result.result);
|
|
2389
|
+
}
|
|
2390
|
+
}));
|
|
2391
|
+
return {
|
|
2392
|
+
message: jobs.length > 0 ? "Processing job scheduled." : "Items processed successfully.",
|
|
2393
|
+
results: results.map((result) => JSON.stringify(result)),
|
|
2394
|
+
jobs
|
|
2395
|
+
};
|
|
2396
|
+
};
|
|
2397
|
+
mutations[`${tableNameSingular}ProcessItem`] = async (_, args, context, info) => {
|
|
2354
2398
|
if (!context.user?.super_admin) {
|
|
2355
2399
|
throw new Error("You are not authorized to process fields via API, user must be super admin.");
|
|
2356
2400
|
}
|
|
2357
|
-
const exists = contexts.find((context2) => context2.id === table.id);
|
|
2358
|
-
if (!exists) {
|
|
2359
|
-
throw new Error(`Context ${table.id} not found.`);
|
|
2360
|
-
}
|
|
2361
|
-
if (!args.field) {
|
|
2362
|
-
throw new Error("Field argument missing, the field argument is required.");
|
|
2363
|
-
}
|
|
2364
2401
|
if (!args.item) {
|
|
2365
2402
|
throw new Error("Item argument missing, the item argument is required.");
|
|
2366
2403
|
}
|
|
2367
|
-
const name = args.field?.replace("_s3key", "");
|
|
2368
|
-
console.log("[EXULU] name", name);
|
|
2369
|
-
console.log("[EXULU] fields", exists.fields.map((field2) => field2.name));
|
|
2370
|
-
const field = exists.fields.find((field2) => {
|
|
2371
|
-
return field2.name.replace("_s3key", "") === name;
|
|
2372
|
-
});
|
|
2373
|
-
if (!field) {
|
|
2374
|
-
throw new Error(`Field ${name} not found in context ${exists.id}].`);
|
|
2375
|
-
}
|
|
2376
|
-
if (!field.processor) {
|
|
2377
|
-
throw new Error(`Processor not set for field ${args.field} in context ${exists.id}.`);
|
|
2378
|
-
}
|
|
2379
2404
|
const { db: db3 } = context;
|
|
2380
2405
|
let query = db3.from(tableNamePlural).select("*").where({ id: args.item });
|
|
2381
2406
|
query = applyAccessControl(table, query, context.user);
|
|
@@ -2383,21 +2408,38 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2383
2408
|
if (!item) {
|
|
2384
2409
|
throw new Error("Item not found, or your user does not have access to it.");
|
|
2385
2410
|
}
|
|
2386
|
-
const
|
|
2387
|
-
|
|
2388
|
-
{
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2411
|
+
const exists = contexts.find((context2) => context2.id === table.id);
|
|
2412
|
+
if (!exists) {
|
|
2413
|
+
throw new Error(`Context ${table.id} not found.`);
|
|
2414
|
+
}
|
|
2415
|
+
return contextItemProcessorMutation(exists, [item], context.user.id, context.user.role?.id);
|
|
2416
|
+
};
|
|
2417
|
+
mutations[`${tableNameSingular}ProcessItems`] = async (_, args, context, info) => {
|
|
2418
|
+
if (!context.user?.super_admin) {
|
|
2419
|
+
throw new Error("You are not authorized to process fields via API, user must be super admin.");
|
|
2420
|
+
}
|
|
2421
|
+
const { limit = 10, filters = [], sort } = args;
|
|
2422
|
+
const { db: db3 } = context;
|
|
2423
|
+
const { items } = await paginationRequest({
|
|
2424
|
+
db: db3,
|
|
2425
|
+
limit,
|
|
2426
|
+
page: 0,
|
|
2427
|
+
filters,
|
|
2428
|
+
sort,
|
|
2429
|
+
table,
|
|
2430
|
+
user: context.user,
|
|
2431
|
+
fields: "*"
|
|
2432
|
+
});
|
|
2433
|
+
const exists = contexts.find((context2) => context2.id === table.id);
|
|
2434
|
+
if (!exists) {
|
|
2435
|
+
throw new Error(`Context ${table.id} not found.`);
|
|
2436
|
+
}
|
|
2437
|
+
return contextItemProcessorMutation(
|
|
2438
|
+
exists,
|
|
2439
|
+
items,
|
|
2393
2440
|
context.user.id,
|
|
2394
2441
|
context.user.role?.id
|
|
2395
2442
|
);
|
|
2396
|
-
return {
|
|
2397
|
-
message: job ? "Processing job scheduled." : "Item processed successfully.",
|
|
2398
|
-
result,
|
|
2399
|
-
job
|
|
2400
|
-
};
|
|
2401
2443
|
};
|
|
2402
2444
|
}
|
|
2403
2445
|
mutations[`${tableNameSingular}ExecuteSource`] = async (_, args, context, info) => {
|
|
@@ -2565,14 +2607,14 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2565
2607
|
}
|
|
2566
2608
|
return mutations;
|
|
2567
2609
|
}
|
|
2568
|
-
var applyAccessControl = (table, query, user) => {
|
|
2610
|
+
var applyAccessControl = (table, query, user, field_prefix) => {
|
|
2569
2611
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
2570
2612
|
if (table.name.plural !== "agent_sessions" && user?.super_admin === true) {
|
|
2571
2613
|
return query;
|
|
2572
2614
|
}
|
|
2573
2615
|
console.log("[EXULU] user.role", user?.role);
|
|
2574
2616
|
console.log("[EXULU] table.name.plural", table.name.plural);
|
|
2575
|
-
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")))) {
|
|
2576
2618
|
console.error("==== Access control error: no role found or no access to entity type. ====");
|
|
2577
2619
|
throw new Error("Access control error: no role found or no access to entity type.");
|
|
2578
2620
|
}
|
|
@@ -2581,18 +2623,25 @@ var applyAccessControl = (table, query, user) => {
|
|
|
2581
2623
|
if (!hasRBAC) {
|
|
2582
2624
|
return query;
|
|
2583
2625
|
}
|
|
2626
|
+
if (user?.super_admin) {
|
|
2627
|
+
return query;
|
|
2628
|
+
}
|
|
2629
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2630
|
+
console.log("[EXULU] applying access control with this prefix", prefix);
|
|
2584
2631
|
try {
|
|
2585
2632
|
query = query.where(function() {
|
|
2586
|
-
this.where(
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
this.
|
|
2590
|
-
this.
|
|
2633
|
+
this.where(`${prefix}rights_mode`, "public");
|
|
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
|
+
});
|
|
2591
2640
|
});
|
|
2592
|
-
}
|
|
2593
|
-
if (user
|
|
2641
|
+
}
|
|
2642
|
+
if (user?.role) {
|
|
2594
2643
|
this.orWhere(function() {
|
|
2595
|
-
this.where(
|
|
2644
|
+
this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
|
|
2596
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);
|
|
2597
2646
|
});
|
|
2598
2647
|
});
|
|
@@ -2604,9 +2653,11 @@ var applyAccessControl = (table, query, user) => {
|
|
|
2604
2653
|
}
|
|
2605
2654
|
return query;
|
|
2606
2655
|
};
|
|
2607
|
-
var converOperatorToQuery = (query, fieldName, operators, table) => {
|
|
2656
|
+
var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
|
|
2608
2657
|
const field = table?.fields.find((f) => f.name === fieldName);
|
|
2609
2658
|
const isJsonField = field?.type === "json";
|
|
2659
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2660
|
+
fieldName = prefix + fieldName;
|
|
2610
2661
|
if (operators.eq !== void 0) {
|
|
2611
2662
|
if (isJsonField) {
|
|
2612
2663
|
query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
|
|
@@ -2916,53 +2967,95 @@ var finalizeRequestedFields = async ({
|
|
|
2916
2967
|
}
|
|
2917
2968
|
const { db: db3 } = await postgresClient();
|
|
2918
2969
|
const query = db3.from(getChunksTableName(context.id)).where({ source: result.id }).select("id", "content", "source", "chunk_index", "createdAt", "updatedAt");
|
|
2919
|
-
query.select(
|
|
2920
|
-
db3.raw("vector_dims(??) as embedding_size", [`embedding`])
|
|
2921
|
-
);
|
|
2922
2970
|
const chunks = await query;
|
|
2923
2971
|
result.chunks = chunks.map((chunk) => ({
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
hybrid_score: 0,
|
|
2927
|
-
content: chunk.content,
|
|
2928
|
-
source: chunk.source,
|
|
2972
|
+
chunk_content: chunk.content,
|
|
2973
|
+
chunk_source: chunk.source,
|
|
2929
2974
|
chunk_index: chunk.chunk_index,
|
|
2930
2975
|
chunk_id: chunk.id,
|
|
2931
2976
|
chunk_created_at: chunk.createdAt,
|
|
2932
2977
|
chunk_updated_at: chunk.updatedAt,
|
|
2933
|
-
|
|
2978
|
+
item_updated_at: chunk.item_updated_at,
|
|
2979
|
+
item_created_at: chunk.item_created_at,
|
|
2980
|
+
item_id: chunk.item_id,
|
|
2981
|
+
item_external_id: chunk.item_external_id,
|
|
2982
|
+
item_name: chunk.item_name
|
|
2934
2983
|
}));
|
|
2935
2984
|
}
|
|
2936
2985
|
}
|
|
2937
2986
|
}
|
|
2938
2987
|
return result;
|
|
2939
2988
|
};
|
|
2940
|
-
var applyFilters = (query, filters, table) => {
|
|
2989
|
+
var applyFilters = (query, filters, table, field_prefix) => {
|
|
2941
2990
|
filters.forEach((filter) => {
|
|
2942
2991
|
Object.entries(filter).forEach(([fieldName, operators]) => {
|
|
2943
2992
|
if (operators) {
|
|
2944
2993
|
if (operators.and !== void 0) {
|
|
2945
2994
|
operators.and.forEach((operator) => {
|
|
2946
|
-
query = converOperatorToQuery(query, fieldName, operator, table);
|
|
2995
|
+
query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
|
|
2947
2996
|
});
|
|
2948
2997
|
}
|
|
2949
2998
|
if (operators.or !== void 0) {
|
|
2950
2999
|
operators.or.forEach((operator) => {
|
|
2951
|
-
query = converOperatorToQuery(query, fieldName, operator, table);
|
|
3000
|
+
query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
|
|
2952
3001
|
});
|
|
2953
3002
|
}
|
|
2954
|
-
query = converOperatorToQuery(query, fieldName, operators, table);
|
|
3003
|
+
query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
|
|
2955
3004
|
}
|
|
2956
3005
|
});
|
|
2957
3006
|
});
|
|
2958
3007
|
return query;
|
|
2959
3008
|
};
|
|
2960
|
-
var applySorting = (query, sort) => {
|
|
3009
|
+
var applySorting = (query, sort, field_prefix) => {
|
|
3010
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2961
3011
|
if (sort) {
|
|
3012
|
+
sort.field = prefix + sort.field;
|
|
2962
3013
|
query = query.orderBy(sort.field, sort.direction.toLowerCase());
|
|
2963
3014
|
}
|
|
2964
3015
|
return query;
|
|
2965
3016
|
};
|
|
3017
|
+
var paginationRequest = async ({
|
|
3018
|
+
db: db3,
|
|
3019
|
+
limit,
|
|
3020
|
+
page,
|
|
3021
|
+
filters,
|
|
3022
|
+
sort,
|
|
3023
|
+
table,
|
|
3024
|
+
user,
|
|
3025
|
+
fields
|
|
3026
|
+
}) => {
|
|
3027
|
+
if (limit > 1e4) {
|
|
3028
|
+
throw new Error("Limit cannot be greater than 10.000.");
|
|
3029
|
+
}
|
|
3030
|
+
const tableName = table.name.plural.toLowerCase();
|
|
3031
|
+
let countQuery = db3(tableName);
|
|
3032
|
+
countQuery = applyFilters(countQuery, filters, table);
|
|
3033
|
+
countQuery = applyAccessControl(table, countQuery, user);
|
|
3034
|
+
const countResult = await countQuery.count("* as count");
|
|
3035
|
+
const itemCount = Number(countResult[0]?.count || 0);
|
|
3036
|
+
const pageCount = Math.ceil(itemCount / limit);
|
|
3037
|
+
const currentPage = page;
|
|
3038
|
+
const hasPreviousPage = currentPage > 1;
|
|
3039
|
+
const hasNextPage = currentPage < pageCount - 1;
|
|
3040
|
+
let dataQuery = db3(tableName);
|
|
3041
|
+
dataQuery = applyFilters(dataQuery, filters, table);
|
|
3042
|
+
dataQuery = applyAccessControl(table, dataQuery, user);
|
|
3043
|
+
dataQuery = applySorting(dataQuery, sort);
|
|
3044
|
+
if (page > 1) {
|
|
3045
|
+
dataQuery = dataQuery.offset((page - 1) * limit);
|
|
3046
|
+
}
|
|
3047
|
+
let items = await dataQuery.select(fields ? fields : "*").limit(limit);
|
|
3048
|
+
return {
|
|
3049
|
+
items,
|
|
3050
|
+
pageInfo: {
|
|
3051
|
+
pageCount,
|
|
3052
|
+
itemCount,
|
|
3053
|
+
currentPage,
|
|
3054
|
+
hasPreviousPage,
|
|
3055
|
+
hasNextPage
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
};
|
|
2966
3059
|
function createQueries(table, agents, tools, contexts) {
|
|
2967
3060
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
2968
3061
|
const tableNameSingular = table.name.singular.toLowerCase();
|
|
@@ -2998,38 +3091,22 @@ function createQueries(table, agents, tools, contexts) {
|
|
|
2998
3091
|
return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
|
|
2999
3092
|
},
|
|
3000
3093
|
[`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
|
|
3001
|
-
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3002
3094
|
const { db: db3 } = context;
|
|
3003
|
-
|
|
3004
|
-
throw new Error("Limit cannot be greater than 500.");
|
|
3005
|
-
}
|
|
3006
|
-
let countQuery = db3(tableNamePlural);
|
|
3007
|
-
countQuery = applyFilters(countQuery, filters, table);
|
|
3008
|
-
countQuery = applyAccessControl(table, countQuery, context.user);
|
|
3009
|
-
const countResult = await countQuery.count("* as count");
|
|
3010
|
-
const itemCount = Number(countResult[0]?.count || 0);
|
|
3011
|
-
const pageCount = Math.ceil(itemCount / limit);
|
|
3012
|
-
const currentPage = page;
|
|
3013
|
-
const hasPreviousPage = currentPage > 1;
|
|
3014
|
-
const hasNextPage = currentPage < pageCount - 1;
|
|
3015
|
-
let dataQuery = db3(tableNamePlural);
|
|
3016
|
-
dataQuery = applyFilters(dataQuery, filters, table);
|
|
3017
|
-
dataQuery = applyAccessControl(table, dataQuery, context.user);
|
|
3095
|
+
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3018
3096
|
const requestedFields = getRequestedFields(info);
|
|
3019
|
-
dataQuery = applySorting(dataQuery, sort);
|
|
3020
|
-
if (page > 1) {
|
|
3021
|
-
dataQuery = dataQuery.offset((page - 1) * limit);
|
|
3022
|
-
}
|
|
3023
3097
|
const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
|
|
3024
|
-
|
|
3098
|
+
const { items, pageInfo } = await paginationRequest({
|
|
3099
|
+
db: db3,
|
|
3100
|
+
limit,
|
|
3101
|
+
page,
|
|
3102
|
+
filters,
|
|
3103
|
+
sort,
|
|
3104
|
+
table,
|
|
3105
|
+
user: context.user,
|
|
3106
|
+
fields: sanitizedFields
|
|
3107
|
+
});
|
|
3025
3108
|
return {
|
|
3026
|
-
pageInfo
|
|
3027
|
-
pageCount,
|
|
3028
|
-
itemCount,
|
|
3029
|
-
currentPage,
|
|
3030
|
-
hasPreviousPage,
|
|
3031
|
-
hasNextPage
|
|
3032
|
-
},
|
|
3109
|
+
pageInfo,
|
|
3033
3110
|
items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
|
|
3034
3111
|
};
|
|
3035
3112
|
},
|
|
@@ -3078,7 +3155,7 @@ function createQueries(table, agents, tools, contexts) {
|
|
|
3078
3155
|
}
|
|
3079
3156
|
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3080
3157
|
return await vectorSearch({
|
|
3081
|
-
limit,
|
|
3158
|
+
limit: limit || exists.configuration.maxRetrievalResults || 10,
|
|
3082
3159
|
page,
|
|
3083
3160
|
filters,
|
|
3084
3161
|
sort,
|
|
@@ -3088,7 +3165,9 @@ function createQueries(table, agents, tools, contexts) {
|
|
|
3088
3165
|
method: args.method,
|
|
3089
3166
|
user: context.user,
|
|
3090
3167
|
role: context.user?.role?.id,
|
|
3091
|
-
trigger: "api"
|
|
3168
|
+
trigger: "api",
|
|
3169
|
+
cutoffs: args.cutoffs,
|
|
3170
|
+
expand: args.expand
|
|
3092
3171
|
});
|
|
3093
3172
|
};
|
|
3094
3173
|
}
|
|
@@ -3105,7 +3184,9 @@ var vectorSearch = async ({
|
|
|
3105
3184
|
method,
|
|
3106
3185
|
user,
|
|
3107
3186
|
role,
|
|
3108
|
-
trigger
|
|
3187
|
+
trigger,
|
|
3188
|
+
cutoffs,
|
|
3189
|
+
expand
|
|
3109
3190
|
}) => {
|
|
3110
3191
|
const table = contextToTableDefinition(context);
|
|
3111
3192
|
console.log("[EXULU] Called vector search.", {
|
|
@@ -3117,10 +3198,12 @@ var vectorSearch = async ({
|
|
|
3117
3198
|
query,
|
|
3118
3199
|
method,
|
|
3119
3200
|
user,
|
|
3120
|
-
role
|
|
3201
|
+
role,
|
|
3202
|
+
cutoffs,
|
|
3203
|
+
expand
|
|
3121
3204
|
});
|
|
3122
|
-
if (limit >
|
|
3123
|
-
throw new Error("Limit cannot be greater than
|
|
3205
|
+
if (limit > 250) {
|
|
3206
|
+
throw new Error("Limit cannot be greater than 1000.");
|
|
3124
3207
|
}
|
|
3125
3208
|
if (!query) {
|
|
3126
3209
|
throw new Error("Query is required.");
|
|
@@ -3137,106 +3220,135 @@ var vectorSearch = async ({
|
|
|
3137
3220
|
}
|
|
3138
3221
|
const mainTable = getTableName(id);
|
|
3139
3222
|
const chunksTable = getChunksTableName(id);
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
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
|
+
};
|
|
3232
|
+
let chunksQuery = db3(chunksTable + " as chunks").select([
|
|
3233
|
+
"chunks.id as chunk_id",
|
|
3234
|
+
"chunks.source",
|
|
3235
|
+
"chunks.content",
|
|
3236
|
+
"chunks.chunk_index",
|
|
3237
|
+
db3.raw('chunks."createdAt" as chunk_created_at'),
|
|
3238
|
+
db3.raw('chunks."updatedAt" as chunk_updated_at'),
|
|
3239
|
+
"chunks.metadata",
|
|
3240
|
+
"items.id as item_id",
|
|
3241
|
+
"items.name as item_name",
|
|
3242
|
+
"items.external_id as item_external_id",
|
|
3243
|
+
db3.raw('items."updatedAt" as item_updated_at'),
|
|
3244
|
+
db3.raw('items."createdAt" as item_created_at')
|
|
3245
|
+
]);
|
|
3246
|
+
chunksQuery.leftJoin(mainTable + " as items", function() {
|
|
3247
|
+
this.on("chunks.source", "=", "items.id");
|
|
3248
|
+
});
|
|
3249
|
+
chunksQuery = applyFilters(chunksQuery, filters, table, "items");
|
|
3250
|
+
chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
|
|
3251
|
+
chunksQuery = applySorting(chunksQuery, sort, "items");
|
|
3148
3252
|
if (queryRewriter) {
|
|
3149
3253
|
query = await queryRewriter(query);
|
|
3150
3254
|
}
|
|
3151
|
-
|
|
3152
|
-
itemsQuery.leftJoin(chunksTable, function() {
|
|
3153
|
-
this.on(chunksTable + ".source", "=", mainTable + ".id");
|
|
3154
|
-
});
|
|
3155
|
-
itemsQuery.select(chunksTable + ".id as chunk_id");
|
|
3156
|
-
itemsQuery.select(chunksTable + ".source");
|
|
3157
|
-
itemsQuery.select(chunksTable + ".content");
|
|
3158
|
-
itemsQuery.select(chunksTable + ".chunk_index");
|
|
3159
|
-
itemsQuery.select(chunksTable + ".createdAt as chunk_created_at");
|
|
3160
|
-
itemsQuery.select(chunksTable + ".updatedAt as chunk_updated_at");
|
|
3161
|
-
itemsQuery.select(db3.raw("vector_dims(??) as embedding_size", [`${chunksTable}.embedding`]));
|
|
3162
|
-
const { chunks } = await embedder.generateFromQuery(context.id, query, {
|
|
3255
|
+
const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
|
|
3163
3256
|
label: table.name.singular,
|
|
3164
3257
|
trigger
|
|
3165
3258
|
}, user?.id, role);
|
|
3166
|
-
if (!
|
|
3259
|
+
if (!queryChunks?.[0]?.vector) {
|
|
3167
3260
|
throw new Error("No vector generated for query.");
|
|
3168
3261
|
}
|
|
3169
|
-
const vector =
|
|
3262
|
+
const vector = queryChunks[0].vector;
|
|
3170
3263
|
const vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
3171
3264
|
const vectorExpr = `${vectorStr}::vector`;
|
|
3172
3265
|
const language = configuration.language || "english";
|
|
3173
|
-
|
|
3266
|
+
console.log("[EXULU] Vector search params:", { method, query, cutoffs });
|
|
3267
|
+
let resultChunks = [];
|
|
3174
3268
|
switch (method) {
|
|
3175
3269
|
case "tsvector":
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
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 });
|
|
3277
|
+
chunksQuery.select(db3.raw(
|
|
3278
|
+
`ts_rank(chunks.fts, to_tsquery(?, ?)) as fts_rank`,
|
|
3279
|
+
[language, orQuery]
|
|
3179
3280
|
)).whereRaw(
|
|
3180
|
-
|
|
3181
|
-
[language,
|
|
3281
|
+
`(chunks.fts @@ to_tsquery(?, ?)) AND (items.archived IS FALSE OR items.archived IS NULL)`,
|
|
3282
|
+
[language, orQuery]
|
|
3182
3283
|
).orderByRaw(`fts_rank DESC`);
|
|
3183
|
-
|
|
3284
|
+
console.log("[EXULU] FTS query SQL:", chunksQuery.toQuery());
|
|
3285
|
+
resultChunks = await chunksQuery;
|
|
3184
3286
|
break;
|
|
3185
3287
|
case "cosineDistance":
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3288
|
+
chunksQuery.limit(limit * 2);
|
|
3289
|
+
chunksQuery.whereNotNull(`chunks.embedding`).whereRaw(`(items.archived IS FALSE OR items.archived IS NULL)`);
|
|
3290
|
+
console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
|
|
3291
|
+
chunksQuery.select(
|
|
3292
|
+
db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
|
|
3190
3293
|
);
|
|
3191
|
-
|
|
3192
|
-
|
|
3294
|
+
chunksQuery.orderByRaw(
|
|
3295
|
+
`chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
|
|
3193
3296
|
);
|
|
3194
|
-
|
|
3297
|
+
chunksQuery.whereRaw(`(1 - (chunks.embedding <=> ${vectorExpr}) >= ?)`, [cutoffs?.cosineDistance || 0]);
|
|
3298
|
+
resultChunks = await chunksQuery;
|
|
3195
3299
|
break;
|
|
3196
3300
|
case "hybridSearch":
|
|
3197
|
-
const matchCount = Math.min(limit *
|
|
3198
|
-
const fullTextWeight =
|
|
3301
|
+
const matchCount = Math.min(limit * 2);
|
|
3302
|
+
const fullTextWeight = 2;
|
|
3199
3303
|
const semanticWeight = 1;
|
|
3200
3304
|
const rrfK = 50;
|
|
3201
3305
|
const hybridSQL = `
|
|
3202
3306
|
WITH full_text AS (
|
|
3203
3307
|
SELECT
|
|
3204
|
-
|
|
3205
|
-
|
|
3308
|
+
chunks.id,
|
|
3309
|
+
chunks.source,
|
|
3206
3310
|
row_number() OVER (
|
|
3207
|
-
ORDER BY
|
|
3311
|
+
ORDER BY ts_rank(chunks.fts, plainto_tsquery(?, ?)) DESC
|
|
3208
3312
|
) AS rank_ix
|
|
3209
|
-
FROM ${chunksTable}
|
|
3210
|
-
|
|
3313
|
+
FROM ${chunksTable} as chunks
|
|
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)
|
|
3211
3318
|
ORDER BY rank_ix
|
|
3212
|
-
LIMIT LEAST(?,
|
|
3319
|
+
LIMIT LEAST(?, 250) * 2
|
|
3213
3320
|
),
|
|
3214
3321
|
semantic AS (
|
|
3215
3322
|
SELECT
|
|
3216
|
-
|
|
3217
|
-
|
|
3323
|
+
chunks.id,
|
|
3324
|
+
chunks.source,
|
|
3218
3325
|
row_number() OVER (
|
|
3219
|
-
ORDER BY
|
|
3326
|
+
ORDER BY chunks.embedding <=> ${vectorExpr} ASC
|
|
3220
3327
|
) AS rank_ix
|
|
3221
|
-
FROM ${chunksTable}
|
|
3222
|
-
|
|
3328
|
+
FROM ${chunksTable} as chunks
|
|
3329
|
+
LEFT JOIN ${mainTable} as items ON items.id = chunks.source
|
|
3330
|
+
WHERE chunks.embedding IS NOT NULL
|
|
3331
|
+
AND (1 - (chunks.embedding <=> ${vectorExpr})) >= ?
|
|
3332
|
+
AND (items.archived IS FALSE OR items.archived IS NULL)
|
|
3223
3333
|
ORDER BY rank_ix
|
|
3224
|
-
LIMIT LEAST(?,
|
|
3334
|
+
LIMIT LEAST(?, 250) * 2
|
|
3225
3335
|
)
|
|
3226
3336
|
SELECT
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3337
|
+
items.id as item_id,
|
|
3338
|
+
items.name as item_name,
|
|
3339
|
+
items.external_id as item_external_id,
|
|
3340
|
+
chunks.id AS chunk_id,
|
|
3341
|
+
chunks.source,
|
|
3342
|
+
chunks.content,
|
|
3343
|
+
chunks.chunk_index,
|
|
3344
|
+
chunks.metadata,
|
|
3345
|
+
chunks."createdAt" as chunk_created_at,
|
|
3346
|
+
chunks."updatedAt" as chunk_updated_at,
|
|
3347
|
+
items."updatedAt" as item_updated_at,
|
|
3348
|
+
items."createdAt" as item_created_at,
|
|
3237
3349
|
/* Per-signal scores for introspection */
|
|
3238
|
-
ts_rank(
|
|
3239
|
-
(1 - (
|
|
3350
|
+
ts_rank(chunks.fts, plainto_tsquery(?, ?)) AS fts_rank,
|
|
3351
|
+
(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
|
|
3240
3352
|
|
|
3241
3353
|
/* Hybrid RRF score */
|
|
3242
3354
|
(
|
|
@@ -3248,22 +3360,35 @@ var vectorSearch = async ({
|
|
|
3248
3360
|
FROM full_text ft
|
|
3249
3361
|
FULL OUTER JOIN semantic se
|
|
3250
3362
|
ON ft.id = se.id
|
|
3251
|
-
JOIN ${chunksTable}
|
|
3252
|
-
ON COALESCE(ft.id, se.id) =
|
|
3253
|
-
JOIN ${mainTable}
|
|
3254
|
-
ON
|
|
3363
|
+
JOIN ${chunksTable} as chunks
|
|
3364
|
+
ON COALESCE(ft.id, se.id) = chunks.id
|
|
3365
|
+
JOIN ${mainTable} as items
|
|
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})) >= ?)
|
|
3255
3374
|
ORDER BY hybrid_score DESC
|
|
3256
|
-
LIMIT LEAST(?,
|
|
3375
|
+
LIMIT LEAST(?, 250)
|
|
3257
3376
|
OFFSET 0
|
|
3258
3377
|
`;
|
|
3259
3378
|
const bindings = [
|
|
3260
|
-
// full_text:
|
|
3379
|
+
// full_text: plainto_tsquery(lang, query) in rank and where
|
|
3380
|
+
language,
|
|
3381
|
+
query,
|
|
3261
3382
|
language,
|
|
3262
3383
|
query,
|
|
3263
3384
|
language,
|
|
3264
3385
|
query,
|
|
3386
|
+
cutoffs?.tsvector || 0,
|
|
3387
|
+
// full_text tsvector cutoff
|
|
3265
3388
|
matchCount,
|
|
3266
3389
|
// full_text limit
|
|
3390
|
+
cutoffs?.cosineDistance || 0,
|
|
3391
|
+
// semantic cosine distance cutoff
|
|
3267
3392
|
matchCount,
|
|
3268
3393
|
// semantic limit
|
|
3269
3394
|
// fts_rank (ts_rank) call
|
|
@@ -3274,89 +3399,171 @@ var vectorSearch = async ({
|
|
|
3274
3399
|
fullTextWeight,
|
|
3275
3400
|
rrfK,
|
|
3276
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
|
|
3277
3415
|
matchCount
|
|
3278
3416
|
// final limit
|
|
3279
3417
|
];
|
|
3280
|
-
|
|
3281
|
-
}
|
|
3282
|
-
console.log("[EXULU] Vector search results:",
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3418
|
+
resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
|
|
3419
|
+
}
|
|
3420
|
+
console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
|
|
3421
|
+
let results = resultChunks.map((chunk) => ({
|
|
3422
|
+
chunk_content: chunk.content,
|
|
3423
|
+
chunk_index: chunk.chunk_index,
|
|
3424
|
+
chunk_id: chunk.chunk_id,
|
|
3425
|
+
chunk_source: chunk.source,
|
|
3426
|
+
chunk_metadata: chunk.metadata,
|
|
3427
|
+
chunk_created_at: chunk.chunk_created_at,
|
|
3428
|
+
chunk_updated_at: chunk.chunk_updated_at,
|
|
3429
|
+
item_updated_at: chunk.item_updated_at,
|
|
3430
|
+
item_created_at: chunk.item_created_at,
|
|
3431
|
+
item_id: chunk.item_id,
|
|
3432
|
+
item_external_id: chunk.item_external_id,
|
|
3433
|
+
item_name: chunk.item_name,
|
|
3434
|
+
context: {
|
|
3435
|
+
name: table.name.singular,
|
|
3436
|
+
id: table.id || ""
|
|
3437
|
+
},
|
|
3438
|
+
...(method === "cosineDistance" || method === "hybridSearch") && { chunk_cosine_distance: chunk.cosine_distance },
|
|
3439
|
+
...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
|
|
3440
|
+
...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score * 1e4 / 100 }
|
|
3441
|
+
}));
|
|
3442
|
+
if (results.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
|
|
3443
|
+
const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
|
|
3444
|
+
const topScore = results[0]?.[scoreKey];
|
|
3445
|
+
const bottomScore = results[results.length - 1]?.[scoreKey];
|
|
3446
|
+
const medianScore = results[Math.floor(results.length / 2)]?.[scoreKey];
|
|
3447
|
+
console.log("[EXULU] Score distribution:", {
|
|
3448
|
+
method,
|
|
3449
|
+
count: results.length,
|
|
3450
|
+
topScore: topScore?.toFixed(4),
|
|
3451
|
+
bottomScore: bottomScore?.toFixed(4),
|
|
3452
|
+
medianScore: medianScore?.toFixed(4)
|
|
3453
|
+
});
|
|
3454
|
+
const adaptiveThreshold = topScore ? topScore * 0.6 : 0;
|
|
3455
|
+
const beforeFilterCount = results.length;
|
|
3456
|
+
results = results.filter((chunk) => {
|
|
3457
|
+
const score = chunk[scoreKey];
|
|
3458
|
+
return score !== void 0 && score >= adaptiveThreshold;
|
|
3459
|
+
});
|
|
3460
|
+
const filteredCount = beforeFilterCount - results.length;
|
|
3461
|
+
if (filteredCount > 0) {
|
|
3462
|
+
console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
|
|
3324
3463
|
}
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3464
|
+
}
|
|
3465
|
+
if (resultReranker && query) {
|
|
3466
|
+
}
|
|
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
|
+
}
|
|
3331
3515
|
}
|
|
3332
|
-
if (
|
|
3333
|
-
const
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
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
|
+
}
|
|
3354
3556
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
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
|
+
});
|
|
3358
3566
|
}
|
|
3359
|
-
console.log("[EXULU] Vector search results after slicing:", items?.length);
|
|
3360
3567
|
await updateStatistic({
|
|
3361
3568
|
name: "count",
|
|
3362
3569
|
label: table.name.singular,
|
|
@@ -3374,7 +3581,7 @@ var vectorSearch = async ({
|
|
|
3374
3581
|
id: table.id || "",
|
|
3375
3582
|
embedder: embedder.name
|
|
3376
3583
|
},
|
|
3377
|
-
|
|
3584
|
+
chunks: results
|
|
3378
3585
|
};
|
|
3379
3586
|
};
|
|
3380
3587
|
var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
|
|
@@ -3404,10 +3611,10 @@ var contextToTableDefinition = (context) => {
|
|
|
3404
3611
|
plural: tableName?.endsWith("s") ? tableName : tableName + "s"
|
|
3405
3612
|
},
|
|
3406
3613
|
RBAC: true,
|
|
3614
|
+
processor: context.processor,
|
|
3407
3615
|
fields: context.fields.map((field) => ({
|
|
3408
3616
|
name: sanitizeName(field.name),
|
|
3409
3617
|
type: field.type,
|
|
3410
|
-
processor: field.processor,
|
|
3411
3618
|
required: field.required,
|
|
3412
3619
|
default: field.default,
|
|
3413
3620
|
index: field.index,
|
|
@@ -3537,7 +3744,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3537
3744
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
3538
3745
|
const tableNameSingular = table.name.singular.toLowerCase();
|
|
3539
3746
|
const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
|
|
3540
|
-
const processorFields = table.fields.filter((field) => field.processor?.execute);
|
|
3541
3747
|
typeDefs += `
|
|
3542
3748
|
${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
|
|
3543
3749
|
|
|
@@ -3548,7 +3754,7 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3548
3754
|
`;
|
|
3549
3755
|
if (table.type === "items") {
|
|
3550
3756
|
typeDefs += `
|
|
3551
|
-
${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
|
|
3552
3758
|
`;
|
|
3553
3759
|
}
|
|
3554
3760
|
mutationDefs += `
|
|
@@ -3564,9 +3770,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3564
3770
|
${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
|
|
3565
3771
|
${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
|
|
3566
3772
|
`;
|
|
3567
|
-
if (
|
|
3773
|
+
if (table.processor) {
|
|
3568
3774
|
mutationDefs += `
|
|
3569
|
-
${tableNameSingular}
|
|
3775
|
+
${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
|
|
3776
|
+
${tableNameSingular}ProcessItems(limit: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}ProcessItemFieldReturnPayload
|
|
3570
3777
|
`;
|
|
3571
3778
|
}
|
|
3572
3779
|
modelDefs += `
|
|
@@ -3584,8 +3791,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3584
3791
|
|
|
3585
3792
|
type ${tableNameSingular}ProcessItemFieldReturnPayload {
|
|
3586
3793
|
message: String!
|
|
3587
|
-
|
|
3588
|
-
|
|
3794
|
+
results: [String]
|
|
3795
|
+
jobs: [String]
|
|
3589
3796
|
}
|
|
3590
3797
|
|
|
3591
3798
|
type ${tableNameSingular}DeleteChunksReturnPayload {
|
|
@@ -3600,20 +3807,42 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3600
3807
|
tsvector
|
|
3601
3808
|
}
|
|
3602
3809
|
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3810
|
+
input SearchCutoffs {
|
|
3811
|
+
cosineDistance: Float
|
|
3812
|
+
hybrid: Float
|
|
3813
|
+
tsvector: Float
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
input SearchExpand {
|
|
3817
|
+
before: Int
|
|
3818
|
+
after: Int
|
|
3606
3819
|
}
|
|
3607
|
-
` : ""}
|
|
3608
|
-
|
|
3609
3820
|
|
|
3610
|
-
|
|
3611
|
-
|
|
3821
|
+
type ${tableNameSingular}VectorSearchResult {
|
|
3822
|
+
chunks: [${tableNameSingular}VectorSearchChunk!]!
|
|
3612
3823
|
context: VectoSearchResultContext!
|
|
3613
3824
|
filters: JSON!
|
|
3614
3825
|
query: String!
|
|
3615
3826
|
method: VectorMethodEnum!
|
|
3616
3827
|
}
|
|
3828
|
+
|
|
3829
|
+
type ${tableNameSingular}VectorSearchChunk {
|
|
3830
|
+
chunk_content: String
|
|
3831
|
+
chunk_index: Int
|
|
3832
|
+
chunk_id: String
|
|
3833
|
+
chunk_source: String
|
|
3834
|
+
chunk_metadata: JSON
|
|
3835
|
+
chunk_created_at: Date
|
|
3836
|
+
chunk_updated_at: Date
|
|
3837
|
+
item_updated_at: Date
|
|
3838
|
+
item_created_at: Date
|
|
3839
|
+
item_id: String!
|
|
3840
|
+
item_external_id: String
|
|
3841
|
+
item_name: String!
|
|
3842
|
+
chunk_cosine_distance: Float
|
|
3843
|
+
chunk_fts_rank: Float
|
|
3844
|
+
chunk_hybrid_score: Float
|
|
3845
|
+
}
|
|
3617
3846
|
|
|
3618
3847
|
type VectoSearchResultContext {
|
|
3619
3848
|
name: String!
|
|
@@ -3722,6 +3951,7 @@ type PageInfo {
|
|
|
3722
3951
|
worker: config2.concurrency?.worker || void 0,
|
|
3723
3952
|
queue: config2.concurrency?.queue || void 0
|
|
3724
3953
|
},
|
|
3954
|
+
timeoutInSeconds: config2.timeoutInSeconds,
|
|
3725
3955
|
ratelimit: config2.ratelimit,
|
|
3726
3956
|
isMaxed: await config2.queue.isMaxed(),
|
|
3727
3957
|
isPaused: await config2.queue.isPaused(),
|
|
@@ -3935,25 +4165,21 @@ type PageInfo {
|
|
|
3935
4165
|
};
|
|
3936
4166
|
resolvers.Query["contexts"] = async (_, args, context, info) => {
|
|
3937
4167
|
const data = await Promise.all(contexts.map(async (context2) => {
|
|
3938
|
-
let
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
}
|
|
3954
|
-
return null;
|
|
3955
|
-
}));
|
|
3956
|
-
processors = processors.filter((processor) => processor !== null);
|
|
4168
|
+
let processor = null;
|
|
4169
|
+
if (context2.processor) {
|
|
4170
|
+
processor = await new Promise(async (resolve, reject) => {
|
|
4171
|
+
const config2 = await context2.processor?.config;
|
|
4172
|
+
const queue = await config2?.queue;
|
|
4173
|
+
resolve({
|
|
4174
|
+
name: context2.processor.name,
|
|
4175
|
+
description: context2.processor.description,
|
|
4176
|
+
queue: queue?.queue?.name || void 0,
|
|
4177
|
+
trigger: context2.processor?.config?.trigger || "manual",
|
|
4178
|
+
timeoutInSeconds: queue?.timeoutInSeconds || 600,
|
|
4179
|
+
generateEmbeddings: context2.processor?.config?.generateEmbeddings || false
|
|
4180
|
+
});
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
3957
4183
|
const sources = await Promise.all(context2.sources.map(async (source) => {
|
|
3958
4184
|
let queueName = void 0;
|
|
3959
4185
|
if (source.config) {
|
|
@@ -3985,7 +4211,7 @@ type PageInfo {
|
|
|
3985
4211
|
slug: "/contexts/" + context2.id,
|
|
3986
4212
|
active: context2.active,
|
|
3987
4213
|
sources,
|
|
3988
|
-
|
|
4214
|
+
processor,
|
|
3989
4215
|
fields: context2.fields.map((field) => {
|
|
3990
4216
|
return {
|
|
3991
4217
|
...field,
|
|
@@ -4011,25 +4237,21 @@ type PageInfo {
|
|
|
4011
4237
|
if (!data) {
|
|
4012
4238
|
return null;
|
|
4013
4239
|
}
|
|
4014
|
-
let
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
}
|
|
4030
|
-
return null;
|
|
4031
|
-
}));
|
|
4032
|
-
processors = processors.filter((processor) => processor !== null);
|
|
4240
|
+
let processor = null;
|
|
4241
|
+
if (data.processor) {
|
|
4242
|
+
processor = await new Promise(async (resolve, reject) => {
|
|
4243
|
+
const config2 = await data.processor?.config;
|
|
4244
|
+
const queue = await config2?.queue;
|
|
4245
|
+
resolve({
|
|
4246
|
+
name: data.processor.name,
|
|
4247
|
+
description: data.processor.description,
|
|
4248
|
+
queue: queue?.queue?.name || void 0,
|
|
4249
|
+
trigger: data.processor?.config?.trigger || "manual",
|
|
4250
|
+
timeoutInSeconds: queue?.timeoutInSeconds || 600,
|
|
4251
|
+
generateEmbeddings: data.processor?.config?.generateEmbeddings || false
|
|
4252
|
+
});
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4033
4255
|
const sources = await Promise.all(data.sources.map(async (source) => {
|
|
4034
4256
|
let queueName = void 0;
|
|
4035
4257
|
if (source.config) {
|
|
@@ -4066,39 +4288,18 @@ type PageInfo {
|
|
|
4066
4288
|
slug: "/contexts/" + data.id,
|
|
4067
4289
|
active: data.active,
|
|
4068
4290
|
sources,
|
|
4069
|
-
|
|
4291
|
+
processor,
|
|
4070
4292
|
fields: await Promise.all(data.fields.map(async (field) => {
|
|
4071
4293
|
const label = field.name?.replace("_s3key", "");
|
|
4072
4294
|
if (field.type === "file" && !field.name.endsWith("_s3key")) {
|
|
4073
4295
|
field.name = field.name + "_s3key";
|
|
4074
4296
|
}
|
|
4075
|
-
let queue = null;
|
|
4076
|
-
if (field.processor?.config?.queue) {
|
|
4077
|
-
queue = await field.processor.config.queue;
|
|
4078
|
-
}
|
|
4079
4297
|
return {
|
|
4080
4298
|
...field,
|
|
4081
4299
|
name: sanitizeName(field.name),
|
|
4082
4300
|
...field.type === "file" ? {
|
|
4083
4301
|
allowedFileTypes: field.allowedFileTypes
|
|
4084
4302
|
} : {},
|
|
4085
|
-
...field.processor ? {
|
|
4086
|
-
processor: {
|
|
4087
|
-
description: field.processor?.description,
|
|
4088
|
-
config: {
|
|
4089
|
-
trigger: field.processor?.config?.trigger,
|
|
4090
|
-
queue: {
|
|
4091
|
-
name: queue?.queue.name || void 0,
|
|
4092
|
-
ratelimit: queue?.ratelimit || void 0,
|
|
4093
|
-
concurrency: {
|
|
4094
|
-
worker: queue?.concurrency?.worker || void 0,
|
|
4095
|
-
queue: queue?.concurrency?.queue || void 0
|
|
4096
|
-
}
|
|
4097
|
-
}
|
|
4098
|
-
},
|
|
4099
|
-
execute: "function"
|
|
4100
|
-
}
|
|
4101
|
-
} : {},
|
|
4102
4303
|
label
|
|
4103
4304
|
};
|
|
4104
4305
|
})),
|
|
@@ -4167,6 +4368,7 @@ type PageInfo {
|
|
|
4167
4368
|
type QueueResult {
|
|
4168
4369
|
name: String!
|
|
4169
4370
|
concurrency: QueueConcurrency!
|
|
4371
|
+
timeoutInSeconds: Int!
|
|
4170
4372
|
ratelimit: Int!
|
|
4171
4373
|
isMaxed: Boolean!
|
|
4172
4374
|
isPaused: Boolean!
|
|
@@ -4248,17 +4450,12 @@ type AgentEvalFunctionConfig {
|
|
|
4248
4450
|
}
|
|
4249
4451
|
|
|
4250
4452
|
type ItemChunks {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
chunk_id: ID
|
|
4258
|
-
chunk_created_at: Date
|
|
4259
|
-
chunk_updated_at: Date
|
|
4260
|
-
embedding_size: Float
|
|
4261
|
-
metadata: JSON
|
|
4453
|
+
chunk_id: String!
|
|
4454
|
+
chunk_index: Int!
|
|
4455
|
+
chunk_content: String!
|
|
4456
|
+
chunk_source: String!
|
|
4457
|
+
chunk_created_at: Date!
|
|
4458
|
+
chunk_updated_at: Date!
|
|
4262
4459
|
}
|
|
4263
4460
|
|
|
4264
4461
|
type Provider {
|
|
@@ -4294,7 +4491,7 @@ type Context {
|
|
|
4294
4491
|
fields: JSON
|
|
4295
4492
|
configuration: JSON
|
|
4296
4493
|
sources: [ContextSource]
|
|
4297
|
-
|
|
4494
|
+
processor: ContextProcessor
|
|
4298
4495
|
}
|
|
4299
4496
|
type Embedder {
|
|
4300
4497
|
name: String!
|
|
@@ -4308,7 +4505,7 @@ type EmbedderConfig {
|
|
|
4308
4505
|
default: String
|
|
4309
4506
|
}
|
|
4310
4507
|
type ContextProcessor {
|
|
4311
|
-
|
|
4508
|
+
name: String!
|
|
4312
4509
|
description: String
|
|
4313
4510
|
queue: String
|
|
4314
4511
|
trigger: String
|
|
@@ -4372,6 +4569,9 @@ type Job {
|
|
|
4372
4569
|
name: String!
|
|
4373
4570
|
returnvalue: JSON
|
|
4374
4571
|
stacktrace: [String]
|
|
4572
|
+
finishedOn: Date
|
|
4573
|
+
processedOn: Date
|
|
4574
|
+
attemptsMade: Int
|
|
4375
4575
|
failedReason: String
|
|
4376
4576
|
state: String!
|
|
4377
4577
|
data: JSON
|
|
@@ -4478,6 +4678,8 @@ var getPresignedUrl = async (bucket, key, config) => {
|
|
|
4478
4678
|
if (!config.fileUploads) {
|
|
4479
4679
|
throw new Error("File uploads are not configured");
|
|
4480
4680
|
}
|
|
4681
|
+
console.log("[EXULU] getting presigned url for bucket", bucket);
|
|
4682
|
+
console.log("[EXULU] getting presigned url for key", key);
|
|
4481
4683
|
const url = await (0, import_s3_request_presigner.getSignedUrl)(
|
|
4482
4684
|
getS3Client(config),
|
|
4483
4685
|
new import_client_s3.GetObjectCommand({
|
|
@@ -4488,6 +4690,18 @@ var getPresignedUrl = async (bucket, key, config) => {
|
|
|
4488
4690
|
);
|
|
4489
4691
|
return url;
|
|
4490
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
|
+
}
|
|
4491
4705
|
var addGeneralPrefixToKey = (keyPath, config) => {
|
|
4492
4706
|
if (!config.fileUploads) {
|
|
4493
4707
|
throw new Error("File uploads are not configured");
|
|
@@ -4523,19 +4737,41 @@ var uploadFile = async (file, fileName, config, options = {}, user, customBucket
|
|
|
4523
4737
|
const client2 = getS3Client(config);
|
|
4524
4738
|
let defaultBucket = config.fileUploads.s3Bucket;
|
|
4525
4739
|
let key = fileName;
|
|
4526
|
-
key = addGeneralPrefixToKey(key, config);
|
|
4527
4740
|
key = addUserPrefixToKey(key, user || "api");
|
|
4528
|
-
|
|
4741
|
+
key = addGeneralPrefixToKey(key, config);
|
|
4742
|
+
const sanitizedMetadata = sanitizeMetadata(options.metadata);
|
|
4529
4743
|
const command = new import_client_s3.PutObjectCommand({
|
|
4530
4744
|
Bucket: customBucket || defaultBucket,
|
|
4531
4745
|
Key: key,
|
|
4532
4746
|
Body: file,
|
|
4533
4747
|
ContentType: options.contentType,
|
|
4534
|
-
Metadata:
|
|
4748
|
+
Metadata: sanitizedMetadata,
|
|
4535
4749
|
ContentLength: file.byteLength
|
|
4536
4750
|
});
|
|
4537
|
-
|
|
4538
|
-
|
|
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
|
+
}
|
|
4539
4775
|
return addBucketPrefixToKey(
|
|
4540
4776
|
key,
|
|
4541
4777
|
customBucket || defaultBucket
|
|
@@ -4607,6 +4843,8 @@ var createUppyRoutes = async (app, contexts, config) => {
|
|
|
4607
4843
|
res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
|
|
4608
4844
|
return;
|
|
4609
4845
|
}
|
|
4846
|
+
key = key.replace(`${bucket}/`, "");
|
|
4847
|
+
console.log("[EXULU] deleting file from s3 into bucket", bucket, "with key", key);
|
|
4610
4848
|
const client2 = getS3Client(config);
|
|
4611
4849
|
const command = new import_client_s3.DeleteObjectCommand({
|
|
4612
4850
|
Bucket: bucket,
|
|
@@ -4730,7 +4968,7 @@ var createUppyRoutes = async (app, contexts, config) => {
|
|
|
4730
4968
|
const client2 = getS3Client(config);
|
|
4731
4969
|
const command = new import_client_s3.ListObjectsV2Command({
|
|
4732
4970
|
Bucket: config.fileUploads.s3Bucket,
|
|
4733
|
-
Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}${authenticationResult.user.id}`,
|
|
4971
|
+
Prefix: `${config.fileUploads.s3prefix ? config.fileUploads.s3prefix.replace(/\/$/, "") + "/" : ""}user_${authenticationResult.user.id}`,
|
|
4734
4972
|
MaxKeys: 9,
|
|
4735
4973
|
...req.query.continuationToken && { ContinuationToken: req.query.continuationToken }
|
|
4736
4974
|
});
|
|
@@ -4742,7 +4980,17 @@ var createUppyRoutes = async (app, contexts, config) => {
|
|
|
4742
4980
|
search.toLowerCase()
|
|
4743
4981
|
));
|
|
4744
4982
|
}
|
|
4745
|
-
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
|
+
});
|
|
4746
4994
|
res.end();
|
|
4747
4995
|
});
|
|
4748
4996
|
app.get("/s3/sts", (req, res, next) => {
|
|
@@ -4803,8 +5051,9 @@ var createUppyRoutes = async (app, contexts, config) => {
|
|
|
4803
5051
|
const { filename, contentType } = extractFileParameters(req);
|
|
4804
5052
|
validateFileParameters(filename, contentType);
|
|
4805
5053
|
const key = generateS3Key2(filename);
|
|
4806
|
-
let fullKey =
|
|
4807
|
-
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);
|
|
4808
5057
|
(0, import_s3_request_presigner.getSignedUrl)(
|
|
4809
5058
|
getS3Client(config),
|
|
4810
5059
|
new import_client_s3.PutObjectCommand({
|
|
@@ -4858,8 +5107,9 @@ var createUppyRoutes = async (app, contexts, config) => {
|
|
|
4858
5107
|
return res.status(400).json({ error: "s3: content type must be a string" });
|
|
4859
5108
|
}
|
|
4860
5109
|
const key = `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
|
|
4861
|
-
let fullKey =
|
|
4862
|
-
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);
|
|
4863
5113
|
const params = {
|
|
4864
5114
|
Bucket: config.fileUploads.s3Bucket,
|
|
4865
5115
|
Key: fullKey,
|
|
@@ -5110,7 +5360,10 @@ var createProjectRetrievalTool = async ({
|
|
|
5110
5360
|
};
|
|
5111
5361
|
var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, providerapikey, contexts, user, exuluConfig, sessionID, req, project) => {
|
|
5112
5362
|
if (!currentTools) return {};
|
|
5113
|
-
if (!allExuluTools)
|
|
5363
|
+
if (!allExuluTools) {
|
|
5364
|
+
allExuluTools = [];
|
|
5365
|
+
}
|
|
5366
|
+
;
|
|
5114
5367
|
if (!contexts) {
|
|
5115
5368
|
contexts = [];
|
|
5116
5369
|
}
|
|
@@ -5144,6 +5397,7 @@ var convertToolsArrayToObject = async (currentTools, allExuluTools, configs, pro
|
|
|
5144
5397
|
...cur.tool,
|
|
5145
5398
|
description,
|
|
5146
5399
|
async *execute(inputs, options) {
|
|
5400
|
+
console.log("[EXULU] Executing tool", cur.name, "with inputs", inputs, "and options", options);
|
|
5147
5401
|
if (!cur.tool?.execute) {
|
|
5148
5402
|
console.error("[EXULU] Tool execute function is undefined.", cur.tool);
|
|
5149
5403
|
throw new Error("Tool execute function is undefined.");
|
|
@@ -5995,6 +6249,68 @@ var ExuluTool2 = class {
|
|
|
5995
6249
|
execute: execute2
|
|
5996
6250
|
});
|
|
5997
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
|
+
};
|
|
5998
6314
|
};
|
|
5999
6315
|
var getTableName = (id) => {
|
|
6000
6316
|
return sanitizeName(id) + "_items";
|
|
@@ -6044,12 +6360,12 @@ var ExuluContext = class {
|
|
|
6044
6360
|
name;
|
|
6045
6361
|
active;
|
|
6046
6362
|
fields;
|
|
6363
|
+
processor;
|
|
6047
6364
|
rateLimit;
|
|
6048
6365
|
description;
|
|
6049
6366
|
embedder;
|
|
6050
6367
|
queryRewriter;
|
|
6051
6368
|
resultReranker;
|
|
6052
|
-
// todo typings
|
|
6053
6369
|
configuration;
|
|
6054
6370
|
sources = [];
|
|
6055
6371
|
constructor({
|
|
@@ -6057,6 +6373,7 @@ var ExuluContext = class {
|
|
|
6057
6373
|
name,
|
|
6058
6374
|
description,
|
|
6059
6375
|
embedder,
|
|
6376
|
+
processor,
|
|
6060
6377
|
active,
|
|
6061
6378
|
rateLimit,
|
|
6062
6379
|
fields,
|
|
@@ -6069,10 +6386,21 @@ var ExuluContext = class {
|
|
|
6069
6386
|
this.name = name;
|
|
6070
6387
|
this.fields = fields || [];
|
|
6071
6388
|
this.sources = sources || [];
|
|
6389
|
+
this.processor = processor;
|
|
6072
6390
|
this.configuration = configuration || {
|
|
6073
6391
|
calculateVectors: "manual",
|
|
6074
6392
|
language: "english",
|
|
6075
|
-
defaultRightsMode: "private"
|
|
6393
|
+
defaultRightsMode: "private",
|
|
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
|
+
}
|
|
6076
6404
|
};
|
|
6077
6405
|
this.description = description;
|
|
6078
6406
|
this.embedder = embedder;
|
|
@@ -6082,26 +6410,35 @@ var ExuluContext = class {
|
|
|
6082
6410
|
this.resultReranker = resultReranker;
|
|
6083
6411
|
}
|
|
6084
6412
|
processField = async (trigger, item, exuluConfig, user, role) => {
|
|
6085
|
-
|
|
6086
|
-
|
|
6413
|
+
console.log("[EXULU] processing item, ", item, " in context", this.id);
|
|
6414
|
+
const exuluStorage = new ExuluStorage({ config: exuluConfig });
|
|
6415
|
+
if (!this.processor) {
|
|
6416
|
+
throw new Error(`Processor is not set for this context: ${this.id}.`);
|
|
6087
6417
|
}
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
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
|
+
}
|
|
6096
6434
|
}
|
|
6097
|
-
const
|
|
6098
|
-
const queue = await field.processor.config?.queue;
|
|
6435
|
+
const queue = await this.processor.config?.queue;
|
|
6099
6436
|
if (queue?.queue.name) {
|
|
6100
6437
|
console.log("[EXULU] processor is in queue mode, scheduling job.");
|
|
6101
6438
|
const job = await bullmqDecorator({
|
|
6102
|
-
timeoutInSeconds:
|
|
6103
|
-
label: `${this.name} ${
|
|
6104
|
-
processor: `${this.id}-${
|
|
6439
|
+
timeoutInSeconds: this.processor.config?.timeoutInSeconds || 600,
|
|
6440
|
+
label: `${this.name} ${this.processor.name} data processor`,
|
|
6441
|
+
processor: `${this.id}-${this.processor.name}`,
|
|
6105
6442
|
context: this.id,
|
|
6106
6443
|
inputs: item,
|
|
6107
6444
|
item: item.id,
|
|
@@ -6116,12 +6453,12 @@ var ExuluContext = class {
|
|
|
6116
6453
|
trigger
|
|
6117
6454
|
});
|
|
6118
6455
|
return {
|
|
6119
|
-
result:
|
|
6456
|
+
result: void 0,
|
|
6120
6457
|
job: job.id
|
|
6121
6458
|
};
|
|
6122
6459
|
}
|
|
6123
6460
|
console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
|
|
6124
|
-
const processorResult = await
|
|
6461
|
+
const processorResult = await this.processor.execute({
|
|
6125
6462
|
item,
|
|
6126
6463
|
user,
|
|
6127
6464
|
role,
|
|
@@ -6138,7 +6475,8 @@ var ExuluContext = class {
|
|
|
6138
6475
|
await db3.from(getTableName(this.id)).where({
|
|
6139
6476
|
id: processorResult.id
|
|
6140
6477
|
}).update({
|
|
6141
|
-
...processorResult
|
|
6478
|
+
...processorResult,
|
|
6479
|
+
last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6142
6480
|
});
|
|
6143
6481
|
return {
|
|
6144
6482
|
result: processorResult,
|
|
@@ -6152,7 +6490,10 @@ var ExuluContext = class {
|
|
|
6152
6490
|
user: options.user,
|
|
6153
6491
|
role: options.role,
|
|
6154
6492
|
context: this,
|
|
6155
|
-
db: db3
|
|
6493
|
+
db: db3,
|
|
6494
|
+
limit: options?.limit || this.configuration.maxRetrievalResults || 10,
|
|
6495
|
+
cutoffs: options.cutoffs,
|
|
6496
|
+
expand: options.expand
|
|
6156
6497
|
});
|
|
6157
6498
|
return result;
|
|
6158
6499
|
};
|
|
@@ -6256,9 +6597,8 @@ var ExuluContext = class {
|
|
|
6256
6597
|
console.log("[EXULU] context configuration", this.configuration);
|
|
6257
6598
|
let jobs = [];
|
|
6258
6599
|
let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
|
|
6600
|
+
if (this.processor) {
|
|
6601
|
+
const processor = this.processor;
|
|
6262
6602
|
console.log("[EXULU] Processor found", processor);
|
|
6263
6603
|
if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
|
|
6264
6604
|
const {
|
|
@@ -6268,8 +6608,7 @@ var ExuluContext = class {
|
|
|
6268
6608
|
"api",
|
|
6269
6609
|
{
|
|
6270
6610
|
...item,
|
|
6271
|
-
id: results[0].id
|
|
6272
|
-
field: key
|
|
6611
|
+
id: results[0].id
|
|
6273
6612
|
},
|
|
6274
6613
|
config,
|
|
6275
6614
|
user,
|
|
@@ -6336,8 +6675,8 @@ var ExuluContext = class {
|
|
|
6336
6675
|
await mutation;
|
|
6337
6676
|
let jobs = [];
|
|
6338
6677
|
let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
|
|
6339
|
-
|
|
6340
|
-
const processor = this.
|
|
6678
|
+
if (this.processor) {
|
|
6679
|
+
const processor = this.processor;
|
|
6341
6680
|
if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
|
|
6342
6681
|
const {
|
|
6343
6682
|
job: processorJob,
|
|
@@ -6346,8 +6685,7 @@ var ExuluContext = class {
|
|
|
6346
6685
|
"api",
|
|
6347
6686
|
{
|
|
6348
6687
|
...item,
|
|
6349
|
-
id: record.id
|
|
6350
|
-
field: key
|
|
6688
|
+
id: record.id
|
|
6351
6689
|
},
|
|
6352
6690
|
config,
|
|
6353
6691
|
user,
|
|
@@ -6585,22 +6923,26 @@ var ExuluContext = class {
|
|
|
6585
6923
|
};
|
|
6586
6924
|
// Exports the context as a tool that can be used by an agent
|
|
6587
6925
|
tool = () => {
|
|
6926
|
+
if (this.configuration.enableAsTool === false) {
|
|
6927
|
+
return null;
|
|
6928
|
+
}
|
|
6588
6929
|
return new ExuluTool2({
|
|
6589
6930
|
id: this.id,
|
|
6590
|
-
name: `${this.name}`,
|
|
6931
|
+
name: `${this.name}_context_search`,
|
|
6591
6932
|
type: "context",
|
|
6592
6933
|
category: "contexts",
|
|
6593
6934
|
inputSchema: import_zod.z.object({
|
|
6594
|
-
|
|
6935
|
+
originalQuestion: import_zod.z.string().describe("The original question that the user asked"),
|
|
6936
|
+
relevantKeywords: import_zod.z.array(import_zod.z.string()).describe("The keywords that are relevant to the user's question, for example names of specific products, systems or parts, IDs, etc.")
|
|
6595
6937
|
}),
|
|
6596
6938
|
config: [],
|
|
6597
6939
|
description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
|
|
6598
|
-
execute: async ({
|
|
6940
|
+
execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
|
|
6599
6941
|
const { db: db3 } = await postgresClient();
|
|
6600
6942
|
const result = await vectorSearch({
|
|
6601
6943
|
page: 1,
|
|
6602
|
-
limit:
|
|
6603
|
-
query,
|
|
6944
|
+
limit: this.configuration.maxRetrievalResults ?? 10,
|
|
6945
|
+
query: originalQuestion,
|
|
6604
6946
|
filters: [],
|
|
6605
6947
|
user,
|
|
6606
6948
|
role,
|
|
@@ -6620,7 +6962,13 @@ var ExuluContext = class {
|
|
|
6620
6962
|
role: user?.role?.id
|
|
6621
6963
|
});
|
|
6622
6964
|
return {
|
|
6623
|
-
|
|
6965
|
+
result: JSON.stringify(result.chunks.map((chunk) => ({
|
|
6966
|
+
...chunk,
|
|
6967
|
+
context: {
|
|
6968
|
+
name: this.name,
|
|
6969
|
+
id: this.id
|
|
6970
|
+
}
|
|
6971
|
+
})))
|
|
6624
6972
|
};
|
|
6625
6973
|
}
|
|
6626
6974
|
});
|
|
@@ -7487,7 +7835,7 @@ var import_ai3 = require("ai");
|
|
|
7487
7835
|
var import_crypto_js4 = __toESM(require("crypto-js"), 1);
|
|
7488
7836
|
|
|
7489
7837
|
// src/registry/log-metadata.ts
|
|
7490
|
-
function
|
|
7838
|
+
function logMetadata(id, additionalMetadata) {
|
|
7491
7839
|
return {
|
|
7492
7840
|
__logMetadata: true,
|
|
7493
7841
|
id,
|
|
@@ -7497,9 +7845,32 @@ function logMetadata2(id, additionalMetadata) {
|
|
|
7497
7845
|
|
|
7498
7846
|
// src/registry/workers.ts
|
|
7499
7847
|
var redisConnection;
|
|
7848
|
+
var unhandledRejectionHandlerInstalled = false;
|
|
7849
|
+
var installGlobalErrorHandlers = () => {
|
|
7850
|
+
if (unhandledRejectionHandlerInstalled) return;
|
|
7851
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
7852
|
+
console.error("[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.", {
|
|
7853
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
7854
|
+
stack: reason instanceof Error ? reason.stack : void 0
|
|
7855
|
+
});
|
|
7856
|
+
});
|
|
7857
|
+
process.on("uncaughtException", (error) => {
|
|
7858
|
+
console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
|
|
7859
|
+
error: error.message,
|
|
7860
|
+
stack: error.stack
|
|
7861
|
+
});
|
|
7862
|
+
if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
|
|
7863
|
+
console.error("[EXULU] Fatal error detected, exiting process.");
|
|
7864
|
+
process.exit(1);
|
|
7865
|
+
}
|
|
7866
|
+
});
|
|
7867
|
+
unhandledRejectionHandlerInstalled = true;
|
|
7868
|
+
console.log("[EXULU] Global error handlers installed to prevent worker crashes");
|
|
7869
|
+
};
|
|
7500
7870
|
var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
|
|
7501
7871
|
console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
|
|
7502
7872
|
console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
|
|
7873
|
+
installGlobalErrorHandlers();
|
|
7503
7874
|
if (!redisServer.host || !redisServer.port) {
|
|
7504
7875
|
console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
|
|
7505
7876
|
throw new Error("No redis server configured in the environment, so cannot start worker.");
|
|
@@ -7524,7 +7895,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7524
7895
|
const worker = new import_bullmq4.Worker(
|
|
7525
7896
|
`${queue.queue.name}`,
|
|
7526
7897
|
async (bullmqJob) => {
|
|
7527
|
-
console.log("[EXULU] starting execution for job",
|
|
7898
|
+
console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
|
|
7528
7899
|
name: bullmqJob.name,
|
|
7529
7900
|
jobId: bullmqJob.id,
|
|
7530
7901
|
status: await bullmqJob.getState(),
|
|
@@ -7534,9 +7905,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7534
7905
|
const data = bullmqJob.data;
|
|
7535
7906
|
const timeoutInSeconds = data.timeoutInSeconds || 600;
|
|
7536
7907
|
const timeoutMs = timeoutInSeconds * 1e3;
|
|
7908
|
+
let timeoutHandle;
|
|
7537
7909
|
const timeoutPromise = new Promise((_, reject) => {
|
|
7538
|
-
setTimeout(() => {
|
|
7539
|
-
|
|
7910
|
+
timeoutHandle = setTimeout(() => {
|
|
7911
|
+
const timeoutError = new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`);
|
|
7912
|
+
console.error(`[EXULU] ${timeoutError.message}`);
|
|
7913
|
+
reject(timeoutError);
|
|
7540
7914
|
}, timeoutMs);
|
|
7541
7915
|
});
|
|
7542
7916
|
const workPromise = (async () => {
|
|
@@ -7544,7 +7918,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7544
7918
|
console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
|
|
7545
7919
|
bullmq.validate(bullmqJob.id, data);
|
|
7546
7920
|
if (data.type === "embedder") {
|
|
7547
|
-
console.log("[EXULU] running an embedder job.",
|
|
7921
|
+
console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
|
|
7548
7922
|
const label = `embedder-${bullmqJob.name}`;
|
|
7549
7923
|
await db3.from("job_results").insert({
|
|
7550
7924
|
job_id: bullmqJob.id,
|
|
@@ -7564,12 +7938,12 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7564
7938
|
if (!embedder) {
|
|
7565
7939
|
throw new Error(`Embedder ${data.embedder} not found in the registry.`);
|
|
7566
7940
|
}
|
|
7567
|
-
const
|
|
7941
|
+
const result = await context.createAndUpsertEmbeddings(data.inputs, config, data.user, {
|
|
7568
7942
|
label: embedder.name,
|
|
7569
7943
|
trigger: data.trigger
|
|
7570
7944
|
}, data.role, bullmqJob.id);
|
|
7571
7945
|
return {
|
|
7572
|
-
result
|
|
7946
|
+
result,
|
|
7573
7947
|
metadata: {}
|
|
7574
7948
|
};
|
|
7575
7949
|
}
|
|
@@ -7587,21 +7961,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7587
7961
|
if (!context) {
|
|
7588
7962
|
throw new Error(`Context ${data.context} not found in the registry.`);
|
|
7589
7963
|
}
|
|
7590
|
-
const field = context.fields.find((field2) => {
|
|
7591
|
-
return field2.name.replace("_s3key", "") === data.inputs.field.replace("_s3key", "");
|
|
7592
|
-
});
|
|
7593
|
-
if (!field) {
|
|
7594
|
-
throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
|
|
7595
|
-
}
|
|
7596
|
-
if (!field.processor) {
|
|
7597
|
-
throw new Error(`Processor not set for field ${data.inputs.field} in the context ${data.context}.`);
|
|
7598
|
-
}
|
|
7599
7964
|
if (!data.inputs.id) {
|
|
7600
|
-
throw new Error(`[EXULU] Item not set for processor
|
|
7965
|
+
throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
|
|
7966
|
+
}
|
|
7967
|
+
if (!context.processor) {
|
|
7968
|
+
throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
|
|
7601
7969
|
}
|
|
7602
7970
|
const exuluStorage = new ExuluStorage({ config });
|
|
7603
7971
|
console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
|
|
7604
|
-
const processorResult = await
|
|
7972
|
+
const processorResult = await context.processor.execute({
|
|
7605
7973
|
item: data.inputs,
|
|
7606
7974
|
user: data.user,
|
|
7607
7975
|
role: data.role,
|
|
@@ -7611,16 +7979,17 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7611
7979
|
exuluConfig: config
|
|
7612
7980
|
});
|
|
7613
7981
|
if (!processorResult) {
|
|
7614
|
-
throw new Error(`[EXULU] Processor
|
|
7982
|
+
throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
|
|
7615
7983
|
}
|
|
7616
7984
|
delete processorResult.field;
|
|
7617
7985
|
await db3.from(getTableName(context.id)).where({
|
|
7618
7986
|
id: processorResult.id
|
|
7619
7987
|
}).update({
|
|
7620
|
-
...processorResult
|
|
7988
|
+
...processorResult,
|
|
7989
|
+
last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7621
7990
|
});
|
|
7622
7991
|
let jobs = [];
|
|
7623
|
-
if (
|
|
7992
|
+
if (context.processor?.config?.generateEmbeddings) {
|
|
7624
7993
|
const fullItem = await db3.from(getTableName(context.id)).where({
|
|
7625
7994
|
id: processorResult.id
|
|
7626
7995
|
}).first();
|
|
@@ -7646,7 +8015,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7646
8015
|
};
|
|
7647
8016
|
}
|
|
7648
8017
|
if (data.type === "eval_run") {
|
|
7649
|
-
console.log("[EXULU] running an eval run job.",
|
|
8018
|
+
console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
|
|
7650
8019
|
const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
|
|
7651
8020
|
const existingResult = await db3.from("job_results").where({ label }).first();
|
|
7652
8021
|
if (existingResult) {
|
|
@@ -7695,7 +8064,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7695
8064
|
resolve(messages2);
|
|
7696
8065
|
break;
|
|
7697
8066
|
} catch (error) {
|
|
7698
|
-
console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`,
|
|
8067
|
+
console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
|
|
7699
8068
|
error: error instanceof Error ? error.message : String(error)
|
|
7700
8069
|
}));
|
|
7701
8070
|
attempts++;
|
|
@@ -7706,9 +8075,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7706
8075
|
}
|
|
7707
8076
|
}
|
|
7708
8077
|
});
|
|
7709
|
-
const
|
|
7710
|
-
const messages =
|
|
7711
|
-
const metadata =
|
|
8078
|
+
const result = await promise;
|
|
8079
|
+
const messages = result.messages;
|
|
8080
|
+
const metadata = result.metadata;
|
|
7712
8081
|
const evalFunctions = evalRun.eval_functions;
|
|
7713
8082
|
let evalFunctionResults = [];
|
|
7714
8083
|
for (const evalFunction of evalFunctions) {
|
|
@@ -7716,7 +8085,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7716
8085
|
if (!evalMethod) {
|
|
7717
8086
|
throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
|
|
7718
8087
|
}
|
|
7719
|
-
let
|
|
8088
|
+
let result2;
|
|
7720
8089
|
if (evalMethod.queue) {
|
|
7721
8090
|
const queue2 = await evalMethod.queue;
|
|
7722
8091
|
const jobData = {
|
|
@@ -7747,21 +8116,21 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7747
8116
|
if (!job.id) {
|
|
7748
8117
|
throw new Error(`Tried to add job to queue ${queue2.queue.name} but failed to get the job ID.`);
|
|
7749
8118
|
}
|
|
7750
|
-
|
|
8119
|
+
result2 = await pollJobResult({ queue: queue2, jobId: job.id });
|
|
7751
8120
|
const evalFunctionResult = {
|
|
7752
8121
|
test_case_id: testCase.id,
|
|
7753
8122
|
eval_run_id: evalRun.id,
|
|
7754
8123
|
eval_function_id: evalFunction.id,
|
|
7755
8124
|
eval_function_name: evalFunction.name,
|
|
7756
8125
|
eval_function_config: evalFunction.config || {},
|
|
7757
|
-
result:
|
|
8126
|
+
result: result2 || 0
|
|
7758
8127
|
};
|
|
7759
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${
|
|
7760
|
-
result:
|
|
8128
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
|
|
8129
|
+
result: result2 || 0
|
|
7761
8130
|
}));
|
|
7762
8131
|
evalFunctionResults.push(evalFunctionResult);
|
|
7763
8132
|
} else {
|
|
7764
|
-
|
|
8133
|
+
result2 = await evalMethod.run(
|
|
7765
8134
|
agentInstance,
|
|
7766
8135
|
agentBackend,
|
|
7767
8136
|
testCase,
|
|
@@ -7772,15 +8141,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7772
8141
|
test_case_id: testCase.id,
|
|
7773
8142
|
eval_run_id: evalRun.id,
|
|
7774
8143
|
eval_function_id: evalFunction.id,
|
|
7775
|
-
result:
|
|
8144
|
+
result: result2 || 0
|
|
7776
8145
|
};
|
|
7777
8146
|
evalFunctionResults.push(evalFunctionResult);
|
|
7778
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${
|
|
7779
|
-
result:
|
|
8147
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
|
|
8148
|
+
result: result2 || 0
|
|
7780
8149
|
}));
|
|
7781
8150
|
}
|
|
7782
8151
|
}
|
|
7783
|
-
const scores = evalFunctionResults.map((
|
|
8152
|
+
const scores = evalFunctionResults.map((result2) => result2.result);
|
|
7784
8153
|
console.log("[EXULU] Exulu eval run scores for test case: " + testCase.id, scores);
|
|
7785
8154
|
let score = 0;
|
|
7786
8155
|
switch (data.scoring_method?.toLowerCase()) {
|
|
@@ -7812,7 +8181,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7812
8181
|
};
|
|
7813
8182
|
}
|
|
7814
8183
|
if (data.type === "eval_function") {
|
|
7815
|
-
console.log("[EXULU] running an eval function job.",
|
|
8184
|
+
console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
|
|
7816
8185
|
if (data.eval_functions?.length !== 1) {
|
|
7817
8186
|
throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
|
|
7818
8187
|
}
|
|
@@ -7845,30 +8214,30 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7845
8214
|
messages: inputMessages
|
|
7846
8215
|
} = await validateEvalPayload(data, agents);
|
|
7847
8216
|
const evalFunctions = evalRun.eval_functions;
|
|
7848
|
-
let
|
|
8217
|
+
let result;
|
|
7849
8218
|
for (const evalFunction of evalFunctions) {
|
|
7850
8219
|
const evalMethod = evals.find((e) => e.id === evalFunction.id);
|
|
7851
8220
|
if (!evalMethod) {
|
|
7852
8221
|
throw new Error(`Eval function ${evalFunction.id} not found in the registry, check your code and make sure the eval function is registered correctly.`);
|
|
7853
8222
|
}
|
|
7854
|
-
|
|
8223
|
+
result = await evalMethod.run(
|
|
7855
8224
|
agentInstance,
|
|
7856
8225
|
backend,
|
|
7857
8226
|
testCase,
|
|
7858
8227
|
inputMessages,
|
|
7859
8228
|
evalFunction.config || {}
|
|
7860
8229
|
);
|
|
7861
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${
|
|
7862
|
-
result:
|
|
8230
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
|
|
8231
|
+
result: result || 0
|
|
7863
8232
|
}));
|
|
7864
8233
|
}
|
|
7865
8234
|
return {
|
|
7866
|
-
result
|
|
8235
|
+
result,
|
|
7867
8236
|
metadata: {}
|
|
7868
8237
|
};
|
|
7869
8238
|
}
|
|
7870
8239
|
if (data.type === "source") {
|
|
7871
|
-
console.log("[EXULU] running a source job.",
|
|
8240
|
+
console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
|
|
7872
8241
|
if (!data.source) {
|
|
7873
8242
|
throw new Error(`No source id set for source job.`);
|
|
7874
8243
|
}
|
|
@@ -7883,10 +8252,10 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7883
8252
|
if (!source) {
|
|
7884
8253
|
throw new Error(`Source ${data.source} not found in the context ${context.id}.`);
|
|
7885
8254
|
}
|
|
7886
|
-
const
|
|
8255
|
+
const result = await source.execute(data.inputs);
|
|
7887
8256
|
let jobs = [];
|
|
7888
8257
|
let items = [];
|
|
7889
|
-
for (const item of
|
|
8258
|
+
for (const item of result) {
|
|
7890
8259
|
const { item: createdItem, job } = await context.createItem(
|
|
7891
8260
|
item,
|
|
7892
8261
|
config,
|
|
@@ -7896,14 +8265,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7896
8265
|
);
|
|
7897
8266
|
if (job) {
|
|
7898
8267
|
jobs.push(job);
|
|
7899
|
-
console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`,
|
|
8268
|
+
console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
|
|
7900
8269
|
item: createdItem,
|
|
7901
8270
|
job
|
|
7902
8271
|
}));
|
|
7903
8272
|
}
|
|
7904
8273
|
if (createdItem.id) {
|
|
7905
8274
|
items.push(createdItem.id);
|
|
7906
|
-
console.log(`[EXULU] created item through source update job ${createdItem.id}`,
|
|
8275
|
+
console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
|
|
7907
8276
|
item: createdItem
|
|
7908
8277
|
}));
|
|
7909
8278
|
}
|
|
@@ -7918,7 +8287,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7918
8287
|
role: data?.role
|
|
7919
8288
|
});
|
|
7920
8289
|
return {
|
|
7921
|
-
result
|
|
8290
|
+
result,
|
|
7922
8291
|
metadata: {
|
|
7923
8292
|
jobs,
|
|
7924
8293
|
items
|
|
@@ -7931,8 +8300,15 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7931
8300
|
throw error;
|
|
7932
8301
|
}
|
|
7933
8302
|
})();
|
|
7934
|
-
|
|
7935
|
-
|
|
8303
|
+
try {
|
|
8304
|
+
const result = await Promise.race([workPromise, timeoutPromise]);
|
|
8305
|
+
clearTimeout(timeoutHandle);
|
|
8306
|
+
return result;
|
|
8307
|
+
} catch (error) {
|
|
8308
|
+
clearTimeout(timeoutHandle);
|
|
8309
|
+
console.error(`[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`, error instanceof Error ? error.message : String(error));
|
|
8310
|
+
throw error;
|
|
8311
|
+
}
|
|
7936
8312
|
},
|
|
7937
8313
|
{
|
|
7938
8314
|
autorun: true,
|
|
@@ -7967,7 +8343,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7967
8343
|
});
|
|
7968
8344
|
return;
|
|
7969
8345
|
}
|
|
7970
|
-
console.error(`[EXULU] job failed.`, job?.name ?
|
|
8346
|
+
console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
|
|
7971
8347
|
error: error instanceof Error ? error.message : String(error)
|
|
7972
8348
|
}) : error);
|
|
7973
8349
|
});
|
|
@@ -7975,7 +8351,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7975
8351
|
console.error(`[EXULU] worker error.`, error);
|
|
7976
8352
|
});
|
|
7977
8353
|
worker.on("progress", (job, progress) => {
|
|
7978
|
-
console.log(`[EXULU] job progress ${job.id}.`,
|
|
8354
|
+
console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
|
|
7979
8355
|
progress
|
|
7980
8356
|
}));
|
|
7981
8357
|
});
|
|
@@ -9141,7 +9517,7 @@ var ExuluQueues = class {
|
|
|
9141
9517
|
// method of ExuluQueues we need to store the desired rate limit on the queue
|
|
9142
9518
|
// here so we can use the value when creating workers for the queue instance
|
|
9143
9519
|
// as there is no way to store a rate limit value natively on a bullm queue.
|
|
9144
|
-
register = (name, concurrency, ratelimit = 1) => {
|
|
9520
|
+
register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
|
|
9145
9521
|
const queueConcurrency = concurrency.queue || 1;
|
|
9146
9522
|
const workerConcurrency = concurrency.worker || 1;
|
|
9147
9523
|
const use = async () => {
|
|
@@ -9157,7 +9533,8 @@ var ExuluQueues = class {
|
|
|
9157
9533
|
concurrency: {
|
|
9158
9534
|
worker: workerConcurrency,
|
|
9159
9535
|
queue: queueConcurrency
|
|
9160
|
-
}
|
|
9536
|
+
},
|
|
9537
|
+
timeoutInSeconds
|
|
9161
9538
|
};
|
|
9162
9539
|
}
|
|
9163
9540
|
if (!redisServer.host?.length || !redisServer.port?.length) {
|
|
@@ -9183,7 +9560,8 @@ var ExuluQueues = class {
|
|
|
9183
9560
|
concurrency: {
|
|
9184
9561
|
worker: workerConcurrency,
|
|
9185
9562
|
queue: queueConcurrency
|
|
9186
|
-
}
|
|
9563
|
+
},
|
|
9564
|
+
timeoutInSeconds
|
|
9187
9565
|
});
|
|
9188
9566
|
return {
|
|
9189
9567
|
queue: newQueue,
|
|
@@ -9191,7 +9569,8 @@ var ExuluQueues = class {
|
|
|
9191
9569
|
concurrency: {
|
|
9192
9570
|
worker: workerConcurrency,
|
|
9193
9571
|
queue: queueConcurrency
|
|
9194
|
-
}
|
|
9572
|
+
},
|
|
9573
|
+
timeoutInSeconds
|
|
9195
9574
|
};
|
|
9196
9575
|
};
|
|
9197
9576
|
this.list.set(name, {
|
|
@@ -9201,6 +9580,7 @@ var ExuluQueues = class {
|
|
|
9201
9580
|
queue: queueConcurrency
|
|
9202
9581
|
},
|
|
9203
9582
|
ratelimit,
|
|
9583
|
+
timeoutInSeconds,
|
|
9204
9584
|
use
|
|
9205
9585
|
});
|
|
9206
9586
|
return {
|
|
@@ -10266,7 +10646,7 @@ var ExuluApp = class {
|
|
|
10266
10646
|
...[previewPdfTool],
|
|
10267
10647
|
...todoTools,
|
|
10268
10648
|
// Add contexts as tools
|
|
10269
|
-
...Object.values(contexts || {}).map((context) => context.tool())
|
|
10649
|
+
...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
|
|
10270
10650
|
// Because agents are stored in the database, we add those as tools
|
|
10271
10651
|
// at request time, not during ExuluApp initialization. We add them
|
|
10272
10652
|
// in the grahql tools resolver.
|
|
@@ -10428,10 +10808,7 @@ var ExuluApp = class {
|
|
|
10428
10808
|
console.warn("[EXULU] No queue configured for source", source.name);
|
|
10429
10809
|
continue;
|
|
10430
10810
|
}
|
|
10431
|
-
if (queue) {
|
|
10432
|
-
if (!source.config?.schedule) {
|
|
10433
|
-
throw new Error("Schedule is required for source when configuring a queue: " + source.name);
|
|
10434
|
-
}
|
|
10811
|
+
if (queue && source.config?.schedule) {
|
|
10435
10812
|
console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
|
|
10436
10813
|
await queue.queue?.upsertJobScheduler(source.id, {
|
|
10437
10814
|
pattern: source.config?.schedule
|