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