@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.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");
|
|
@@ -67,7 +67,6 @@ var redisServer = {
|
|
|
67
67
|
// src/redis/client.ts
|
|
68
68
|
var client = {};
|
|
69
69
|
async function redisClient() {
|
|
70
|
-
console.log("[EXULU] redisServer:", redisServer);
|
|
71
70
|
if (!redisServer.host || !redisServer.port) {
|
|
72
71
|
return { client: null };
|
|
73
72
|
}
|
|
@@ -148,7 +147,6 @@ var db = {};
|
|
|
148
147
|
var databaseExistsChecked = false;
|
|
149
148
|
var dbName = process.env.POSTGRES_DB_NAME || "exulu";
|
|
150
149
|
async function ensureDatabaseExists() {
|
|
151
|
-
console.log(`[EXULU] Ensuring ${dbName} database exists...`);
|
|
152
150
|
const defaultKnex = (0, import_knex.default)({
|
|
153
151
|
client: "pg",
|
|
154
152
|
connection: {
|
|
@@ -192,16 +190,7 @@ async function ensureDatabaseExists() {
|
|
|
192
190
|
async function postgresClient() {
|
|
193
191
|
if (!db["exulu"]) {
|
|
194
192
|
try {
|
|
195
|
-
console.log(`[EXULU] Connecting to ${dbName} database.`);
|
|
196
|
-
console.log("[EXULU] POSTGRES_DB_HOST:", process.env.POSTGRES_DB_HOST);
|
|
197
|
-
console.log("[EXULU] POSTGRES_DB_PORT:", process.env.POSTGRES_DB_PORT);
|
|
198
|
-
console.log("[EXULU] POSTGRES_DB_USER:", process.env.POSTGRES_DB_USER);
|
|
199
|
-
console.log("[EXULU] POSTGRES_DB_PASSWORD:", process.env.POSTGRES_DB_PASSWORD);
|
|
200
|
-
console.log("[EXULU] POSTGRES_DB_NAME:", dbName);
|
|
201
|
-
console.log("[EXULU] POSTGRES_DB_SSL:", process.env.POSTGRES_DB_SSL);
|
|
202
|
-
console.log("[EXULU] Database exists checked:", databaseExistsChecked);
|
|
203
193
|
if (!databaseExistsChecked) {
|
|
204
|
-
console.log(`[EXULU] Ensuring ${dbName} database exists...`);
|
|
205
194
|
await ensureDatabaseExists();
|
|
206
195
|
databaseExistsChecked = true;
|
|
207
196
|
}
|
|
@@ -214,21 +203,37 @@ async function postgresClient() {
|
|
|
214
203
|
database: dbName,
|
|
215
204
|
password: process.env.POSTGRES_DB_PASSWORD,
|
|
216
205
|
ssl: process.env.POSTGRES_DB_SSL === "true" ? { rejectUnauthorized: false } : false,
|
|
217
|
-
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
|
|
218
215
|
},
|
|
219
216
|
pool: {
|
|
220
|
-
min:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
224
223
|
createTimeoutMillis: 3e4,
|
|
225
|
-
idleTimeoutMillis:
|
|
224
|
+
idleTimeoutMillis: 6e4,
|
|
225
|
+
// Increased to keep connections alive longer
|
|
226
226
|
reapIntervalMillis: 1e3,
|
|
227
227
|
createRetryIntervalMillis: 200,
|
|
228
228
|
// Log pool events to help debug connection issues
|
|
229
229
|
afterCreate: (conn, done) => {
|
|
230
230
|
console.log("[EXULU] New database connection created");
|
|
231
|
-
|
|
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
|
+
});
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
});
|
|
@@ -526,7 +531,6 @@ var authentication = async ({
|
|
|
526
531
|
}
|
|
527
532
|
if (authtoken) {
|
|
528
533
|
try {
|
|
529
|
-
console.log("[EXULU] authtoken", authtoken);
|
|
530
534
|
if (!authtoken?.email) {
|
|
531
535
|
return {
|
|
532
536
|
error: true,
|
|
@@ -1479,6 +1483,10 @@ var addCoreFields = (schema) => {
|
|
|
1479
1483
|
field.name = field.name + "_s3key";
|
|
1480
1484
|
}
|
|
1481
1485
|
});
|
|
1486
|
+
schema.fields.push({
|
|
1487
|
+
name: "last_processed_at",
|
|
1488
|
+
type: "date"
|
|
1489
|
+
});
|
|
1482
1490
|
if (schema.RBAC) {
|
|
1483
1491
|
if (!schema.fields.some((field) => field.name === "rights_mode")) {
|
|
1484
1492
|
schema.fields.push({
|
|
@@ -2361,33 +2369,38 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2361
2369
|
}
|
|
2362
2370
|
};
|
|
2363
2371
|
if (table.type === "items") {
|
|
2364
|
-
if (table.
|
|
2365
|
-
|
|
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) => {
|
|
2366
2398
|
if (!context.user?.super_admin) {
|
|
2367
2399
|
throw new Error("You are not authorized to process fields via API, user must be super admin.");
|
|
2368
2400
|
}
|
|
2369
|
-
const exists = contexts.find((context2) => context2.id === table.id);
|
|
2370
|
-
if (!exists) {
|
|
2371
|
-
throw new Error(`Context ${table.id} not found.`);
|
|
2372
|
-
}
|
|
2373
|
-
if (!args.field) {
|
|
2374
|
-
throw new Error("Field argument missing, the field argument is required.");
|
|
2375
|
-
}
|
|
2376
2401
|
if (!args.item) {
|
|
2377
2402
|
throw new Error("Item argument missing, the item argument is required.");
|
|
2378
2403
|
}
|
|
2379
|
-
const name = args.field?.replace("_s3key", "");
|
|
2380
|
-
console.log("[EXULU] name", name);
|
|
2381
|
-
console.log("[EXULU] fields", exists.fields.map((field2) => field2.name));
|
|
2382
|
-
const field = exists.fields.find((field2) => {
|
|
2383
|
-
return field2.name.replace("_s3key", "") === name;
|
|
2384
|
-
});
|
|
2385
|
-
if (!field) {
|
|
2386
|
-
throw new Error(`Field ${name} not found in context ${exists.id}].`);
|
|
2387
|
-
}
|
|
2388
|
-
if (!field.processor) {
|
|
2389
|
-
throw new Error(`Processor not set for field ${args.field} in context ${exists.id}.`);
|
|
2390
|
-
}
|
|
2391
2404
|
const { db: db3 } = context;
|
|
2392
2405
|
let query = db3.from(tableNamePlural).select("*").where({ id: args.item });
|
|
2393
2406
|
query = applyAccessControl(table, query, context.user);
|
|
@@ -2395,21 +2408,38 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2395
2408
|
if (!item) {
|
|
2396
2409
|
throw new Error("Item not found, or your user does not have access to it.");
|
|
2397
2410
|
}
|
|
2398
|
-
const
|
|
2399
|
-
|
|
2400
|
-
{
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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,
|
|
2405
2440
|
context.user.id,
|
|
2406
2441
|
context.user.role?.id
|
|
2407
2442
|
);
|
|
2408
|
-
return {
|
|
2409
|
-
message: job ? "Processing job scheduled." : "Item processed successfully.",
|
|
2410
|
-
result,
|
|
2411
|
-
job
|
|
2412
|
-
};
|
|
2413
2443
|
};
|
|
2414
2444
|
}
|
|
2415
2445
|
mutations[`${tableNameSingular}ExecuteSource`] = async (_, args, context, info) => {
|
|
@@ -2577,7 +2607,7 @@ function createMutations(table, agents, contexts, tools, config) {
|
|
|
2577
2607
|
}
|
|
2578
2608
|
return mutations;
|
|
2579
2609
|
}
|
|
2580
|
-
var applyAccessControl = (table, query, user) => {
|
|
2610
|
+
var applyAccessControl = (table, query, user, field_prefix) => {
|
|
2581
2611
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
2582
2612
|
if (table.name.plural !== "agent_sessions" && user?.super_admin === true) {
|
|
2583
2613
|
return query;
|
|
@@ -2593,18 +2623,19 @@ var applyAccessControl = (table, query, user) => {
|
|
|
2593
2623
|
if (!hasRBAC) {
|
|
2594
2624
|
return query;
|
|
2595
2625
|
}
|
|
2626
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2596
2627
|
try {
|
|
2597
2628
|
query = query.where(function() {
|
|
2598
|
-
this.where(
|
|
2599
|
-
this.orWhere(
|
|
2629
|
+
this.where(`${prefix}rights_mode`, "public");
|
|
2630
|
+
this.orWhere(`${prefix}created_by`, user.id);
|
|
2600
2631
|
this.orWhere(function() {
|
|
2601
|
-
this.where(
|
|
2632
|
+
this.where(`${prefix}rights_mode`, "users").whereExists(function() {
|
|
2602
2633
|
this.select("*").from("rbac").whereRaw("rbac.target_resource_id = " + tableNamePlural + ".id").where("rbac.entity", table.name.singular).where("rbac.access_type", "User").where("rbac.user_id", user.id);
|
|
2603
2634
|
});
|
|
2604
2635
|
});
|
|
2605
2636
|
if (user.role) {
|
|
2606
2637
|
this.orWhere(function() {
|
|
2607
|
-
this.where(
|
|
2638
|
+
this.where(`${prefix}rights_mode`, "roles").whereExists(function() {
|
|
2608
2639
|
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);
|
|
2609
2640
|
});
|
|
2610
2641
|
});
|
|
@@ -2616,9 +2647,11 @@ var applyAccessControl = (table, query, user) => {
|
|
|
2616
2647
|
}
|
|
2617
2648
|
return query;
|
|
2618
2649
|
};
|
|
2619
|
-
var converOperatorToQuery = (query, fieldName, operators, table) => {
|
|
2650
|
+
var converOperatorToQuery = (query, fieldName, operators, table, field_prefix) => {
|
|
2620
2651
|
const field = table?.fields.find((f) => f.name === fieldName);
|
|
2621
2652
|
const isJsonField = field?.type === "json";
|
|
2653
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2654
|
+
fieldName = prefix + fieldName;
|
|
2622
2655
|
if (operators.eq !== void 0) {
|
|
2623
2656
|
if (isJsonField) {
|
|
2624
2657
|
query = query.whereRaw(`?? = ?::jsonb`, [fieldName, JSON.stringify(operators.eq)]);
|
|
@@ -2928,53 +2961,95 @@ var finalizeRequestedFields = async ({
|
|
|
2928
2961
|
}
|
|
2929
2962
|
const { db: db3 } = await postgresClient();
|
|
2930
2963
|
const query = db3.from(getChunksTableName(context.id)).where({ source: result.id }).select("id", "content", "source", "chunk_index", "createdAt", "updatedAt");
|
|
2931
|
-
query.select(
|
|
2932
|
-
db3.raw("vector_dims(??) as embedding_size", [`embedding`])
|
|
2933
|
-
);
|
|
2934
2964
|
const chunks = await query;
|
|
2935
2965
|
result.chunks = chunks.map((chunk) => ({
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
hybrid_score: 0,
|
|
2939
|
-
content: chunk.content,
|
|
2940
|
-
source: chunk.source,
|
|
2966
|
+
chunk_content: chunk.content,
|
|
2967
|
+
chunk_source: chunk.source,
|
|
2941
2968
|
chunk_index: chunk.chunk_index,
|
|
2942
2969
|
chunk_id: chunk.id,
|
|
2943
2970
|
chunk_created_at: chunk.createdAt,
|
|
2944
2971
|
chunk_updated_at: chunk.updatedAt,
|
|
2945
|
-
|
|
2972
|
+
item_updated_at: chunk.item_updated_at,
|
|
2973
|
+
item_created_at: chunk.item_created_at,
|
|
2974
|
+
item_id: chunk.item_id,
|
|
2975
|
+
item_external_id: chunk.item_external_id,
|
|
2976
|
+
item_name: chunk.item_name
|
|
2946
2977
|
}));
|
|
2947
2978
|
}
|
|
2948
2979
|
}
|
|
2949
2980
|
}
|
|
2950
2981
|
return result;
|
|
2951
2982
|
};
|
|
2952
|
-
var applyFilters = (query, filters, table) => {
|
|
2983
|
+
var applyFilters = (query, filters, table, field_prefix) => {
|
|
2953
2984
|
filters.forEach((filter) => {
|
|
2954
2985
|
Object.entries(filter).forEach(([fieldName, operators]) => {
|
|
2955
2986
|
if (operators) {
|
|
2956
2987
|
if (operators.and !== void 0) {
|
|
2957
2988
|
operators.and.forEach((operator) => {
|
|
2958
|
-
query = converOperatorToQuery(query, fieldName, operator, table);
|
|
2989
|
+
query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
|
|
2959
2990
|
});
|
|
2960
2991
|
}
|
|
2961
2992
|
if (operators.or !== void 0) {
|
|
2962
2993
|
operators.or.forEach((operator) => {
|
|
2963
|
-
query = converOperatorToQuery(query, fieldName, operator, table);
|
|
2994
|
+
query = converOperatorToQuery(query, fieldName, operator, table, field_prefix);
|
|
2964
2995
|
});
|
|
2965
2996
|
}
|
|
2966
|
-
query = converOperatorToQuery(query, fieldName, operators, table);
|
|
2997
|
+
query = converOperatorToQuery(query, fieldName, operators, table, field_prefix);
|
|
2967
2998
|
}
|
|
2968
2999
|
});
|
|
2969
3000
|
});
|
|
2970
3001
|
return query;
|
|
2971
3002
|
};
|
|
2972
|
-
var applySorting = (query, sort) => {
|
|
3003
|
+
var applySorting = (query, sort, field_prefix) => {
|
|
3004
|
+
const prefix = field_prefix ? field_prefix + "." : "";
|
|
2973
3005
|
if (sort) {
|
|
3006
|
+
sort.field = prefix + sort.field;
|
|
2974
3007
|
query = query.orderBy(sort.field, sort.direction.toLowerCase());
|
|
2975
3008
|
}
|
|
2976
3009
|
return query;
|
|
2977
3010
|
};
|
|
3011
|
+
var paginationRequest = async ({
|
|
3012
|
+
db: db3,
|
|
3013
|
+
limit,
|
|
3014
|
+
page,
|
|
3015
|
+
filters,
|
|
3016
|
+
sort,
|
|
3017
|
+
table,
|
|
3018
|
+
user,
|
|
3019
|
+
fields
|
|
3020
|
+
}) => {
|
|
3021
|
+
if (limit > 1e4) {
|
|
3022
|
+
throw new Error("Limit cannot be greater than 10.000.");
|
|
3023
|
+
}
|
|
3024
|
+
const tableName = table.name.plural.toLowerCase();
|
|
3025
|
+
let countQuery = db3(tableName);
|
|
3026
|
+
countQuery = applyFilters(countQuery, filters, table);
|
|
3027
|
+
countQuery = applyAccessControl(table, countQuery, user);
|
|
3028
|
+
const countResult = await countQuery.count("* as count");
|
|
3029
|
+
const itemCount = Number(countResult[0]?.count || 0);
|
|
3030
|
+
const pageCount = Math.ceil(itemCount / limit);
|
|
3031
|
+
const currentPage = page;
|
|
3032
|
+
const hasPreviousPage = currentPage > 1;
|
|
3033
|
+
const hasNextPage = currentPage < pageCount - 1;
|
|
3034
|
+
let dataQuery = db3(tableName);
|
|
3035
|
+
dataQuery = applyFilters(dataQuery, filters, table);
|
|
3036
|
+
dataQuery = applyAccessControl(table, dataQuery, user);
|
|
3037
|
+
dataQuery = applySorting(dataQuery, sort);
|
|
3038
|
+
if (page > 1) {
|
|
3039
|
+
dataQuery = dataQuery.offset((page - 1) * limit);
|
|
3040
|
+
}
|
|
3041
|
+
let items = await dataQuery.select(fields ? fields : "*").limit(limit);
|
|
3042
|
+
return {
|
|
3043
|
+
items,
|
|
3044
|
+
pageInfo: {
|
|
3045
|
+
pageCount,
|
|
3046
|
+
itemCount,
|
|
3047
|
+
currentPage,
|
|
3048
|
+
hasPreviousPage,
|
|
3049
|
+
hasNextPage
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
};
|
|
2978
3053
|
function createQueries(table, agents, tools, contexts) {
|
|
2979
3054
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
2980
3055
|
const tableNameSingular = table.name.singular.toLowerCase();
|
|
@@ -3010,38 +3085,22 @@ function createQueries(table, agents, tools, contexts) {
|
|
|
3010
3085
|
return finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result, user: context.user });
|
|
3011
3086
|
},
|
|
3012
3087
|
[`${tableNamePlural}Pagination`]: async (_, args, context, info) => {
|
|
3013
|
-
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3014
3088
|
const { db: db3 } = context;
|
|
3015
|
-
|
|
3016
|
-
throw new Error("Limit cannot be greater than 500.");
|
|
3017
|
-
}
|
|
3018
|
-
let countQuery = db3(tableNamePlural);
|
|
3019
|
-
countQuery = applyFilters(countQuery, filters, table);
|
|
3020
|
-
countQuery = applyAccessControl(table, countQuery, context.user);
|
|
3021
|
-
const countResult = await countQuery.count("* as count");
|
|
3022
|
-
const itemCount = Number(countResult[0]?.count || 0);
|
|
3023
|
-
const pageCount = Math.ceil(itemCount / limit);
|
|
3024
|
-
const currentPage = page;
|
|
3025
|
-
const hasPreviousPage = currentPage > 1;
|
|
3026
|
-
const hasNextPage = currentPage < pageCount - 1;
|
|
3027
|
-
let dataQuery = db3(tableNamePlural);
|
|
3028
|
-
dataQuery = applyFilters(dataQuery, filters, table);
|
|
3029
|
-
dataQuery = applyAccessControl(table, dataQuery, context.user);
|
|
3089
|
+
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3030
3090
|
const requestedFields = getRequestedFields(info);
|
|
3031
|
-
dataQuery = applySorting(dataQuery, sort);
|
|
3032
|
-
if (page > 1) {
|
|
3033
|
-
dataQuery = dataQuery.offset((page - 1) * limit);
|
|
3034
|
-
}
|
|
3035
3091
|
const sanitizedFields = sanitizeRequestedFields(table, requestedFields);
|
|
3036
|
-
|
|
3092
|
+
const { items, pageInfo } = await paginationRequest({
|
|
3093
|
+
db: db3,
|
|
3094
|
+
limit,
|
|
3095
|
+
page,
|
|
3096
|
+
filters,
|
|
3097
|
+
sort,
|
|
3098
|
+
table,
|
|
3099
|
+
user: context.user,
|
|
3100
|
+
fields: sanitizedFields
|
|
3101
|
+
});
|
|
3037
3102
|
return {
|
|
3038
|
-
pageInfo
|
|
3039
|
-
pageCount,
|
|
3040
|
-
itemCount,
|
|
3041
|
-
currentPage,
|
|
3042
|
-
hasPreviousPage,
|
|
3043
|
-
hasNextPage
|
|
3044
|
-
},
|
|
3103
|
+
pageInfo,
|
|
3045
3104
|
items: finalizeRequestedFields({ args, table, requestedFields, agents, contexts, tools, result: items, user: context.user })
|
|
3046
3105
|
};
|
|
3047
3106
|
},
|
|
@@ -3090,7 +3149,7 @@ function createQueries(table, agents, tools, contexts) {
|
|
|
3090
3149
|
}
|
|
3091
3150
|
const { limit = 10, page = 0, filters = [], sort } = args;
|
|
3092
3151
|
return await vectorSearch({
|
|
3093
|
-
limit,
|
|
3152
|
+
limit: limit || exists.configuration.maxRetrievalResults || 10,
|
|
3094
3153
|
page,
|
|
3095
3154
|
filters,
|
|
3096
3155
|
sort,
|
|
@@ -3149,106 +3208,111 @@ var vectorSearch = async ({
|
|
|
3149
3208
|
}
|
|
3150
3209
|
const mainTable = getTableName(id);
|
|
3151
3210
|
const chunksTable = getChunksTableName(id);
|
|
3152
|
-
let
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3211
|
+
let chunksQuery = db3(chunksTable + " as chunks").select([
|
|
3212
|
+
"chunks.id as chunk_id",
|
|
3213
|
+
"chunks.source",
|
|
3214
|
+
"chunks.content",
|
|
3215
|
+
"chunks.chunk_index",
|
|
3216
|
+
db3.raw('chunks."createdAt" as chunk_created_at'),
|
|
3217
|
+
db3.raw('chunks."updatedAt" as chunk_updated_at'),
|
|
3218
|
+
"chunks.metadata",
|
|
3219
|
+
"items.id as item_id",
|
|
3220
|
+
"items.name as item_name",
|
|
3221
|
+
"items.external_id as item_external_id",
|
|
3222
|
+
db3.raw('items."updatedAt" as item_updated_at'),
|
|
3223
|
+
db3.raw('items."createdAt" as item_created_at')
|
|
3224
|
+
]);
|
|
3225
|
+
chunksQuery.leftJoin(mainTable + " as items", function() {
|
|
3226
|
+
this.on("chunks.source", "=", "items.id");
|
|
3227
|
+
});
|
|
3228
|
+
chunksQuery = applyFilters(chunksQuery, filters, table, "items");
|
|
3229
|
+
chunksQuery = applyAccessControl(table, chunksQuery, user, "items");
|
|
3230
|
+
chunksQuery = applySorting(chunksQuery, sort, "items");
|
|
3160
3231
|
if (queryRewriter) {
|
|
3161
3232
|
query = await queryRewriter(query);
|
|
3162
3233
|
}
|
|
3163
|
-
|
|
3164
|
-
itemsQuery.leftJoin(chunksTable, function() {
|
|
3165
|
-
this.on(chunksTable + ".source", "=", mainTable + ".id");
|
|
3166
|
-
});
|
|
3167
|
-
itemsQuery.select(chunksTable + ".id as chunk_id");
|
|
3168
|
-
itemsQuery.select(chunksTable + ".source");
|
|
3169
|
-
itemsQuery.select(chunksTable + ".content");
|
|
3170
|
-
itemsQuery.select(chunksTable + ".chunk_index");
|
|
3171
|
-
itemsQuery.select(chunksTable + ".createdAt as chunk_created_at");
|
|
3172
|
-
itemsQuery.select(chunksTable + ".updatedAt as chunk_updated_at");
|
|
3173
|
-
itemsQuery.select(db3.raw("vector_dims(??) as embedding_size", [`${chunksTable}.embedding`]));
|
|
3174
|
-
const { chunks } = await embedder.generateFromQuery(context.id, query, {
|
|
3234
|
+
const { chunks: queryChunks } = await embedder.generateFromQuery(context.id, query, {
|
|
3175
3235
|
label: table.name.singular,
|
|
3176
3236
|
trigger
|
|
3177
3237
|
}, user?.id, role);
|
|
3178
|
-
if (!
|
|
3238
|
+
if (!queryChunks?.[0]?.vector) {
|
|
3179
3239
|
throw new Error("No vector generated for query.");
|
|
3180
3240
|
}
|
|
3181
|
-
const vector =
|
|
3241
|
+
const vector = queryChunks[0].vector;
|
|
3182
3242
|
const vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
3183
3243
|
const vectorExpr = `${vectorStr}::vector`;
|
|
3184
3244
|
const language = configuration.language || "english";
|
|
3185
|
-
let
|
|
3245
|
+
let resultChunks = [];
|
|
3186
3246
|
switch (method) {
|
|
3187
3247
|
case "tsvector":
|
|
3188
|
-
|
|
3189
|
-
|
|
3248
|
+
chunksQuery.limit(limit * 2);
|
|
3249
|
+
chunksQuery.select(db3.raw(
|
|
3250
|
+
`ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) as fts_rank`,
|
|
3190
3251
|
[language, query]
|
|
3191
3252
|
)).whereRaw(
|
|
3192
|
-
|
|
3253
|
+
`chunks.fts @@ websearch_to_tsquery(?, ?)`,
|
|
3193
3254
|
[language, query]
|
|
3194
3255
|
).orderByRaw(`fts_rank DESC`);
|
|
3195
|
-
|
|
3256
|
+
resultChunks = await chunksQuery;
|
|
3196
3257
|
break;
|
|
3197
3258
|
case "cosineDistance":
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3259
|
+
chunksQuery.limit(limit * 2);
|
|
3260
|
+
chunksQuery.whereNotNull(`chunks.embedding`);
|
|
3261
|
+
console.log("[EXULU] Chunks query:", chunksQuery.toQuery());
|
|
3262
|
+
chunksQuery.select(
|
|
3263
|
+
db3.raw(`1 - (chunks.embedding <=> ${vectorExpr}) AS cosine_distance`)
|
|
3202
3264
|
);
|
|
3203
|
-
|
|
3204
|
-
|
|
3265
|
+
chunksQuery.orderByRaw(
|
|
3266
|
+
`chunks.embedding <=> ${vectorExpr} ASC NULLS LAST`
|
|
3205
3267
|
);
|
|
3206
|
-
|
|
3268
|
+
resultChunks = await chunksQuery;
|
|
3207
3269
|
break;
|
|
3208
3270
|
case "hybridSearch":
|
|
3209
|
-
const matchCount = Math.min(limit *
|
|
3271
|
+
const matchCount = Math.min(limit * 2, 100);
|
|
3210
3272
|
const fullTextWeight = 1;
|
|
3211
3273
|
const semanticWeight = 1;
|
|
3212
3274
|
const rrfK = 50;
|
|
3213
3275
|
const hybridSQL = `
|
|
3214
3276
|
WITH full_text AS (
|
|
3215
3277
|
SELECT
|
|
3216
|
-
|
|
3217
|
-
|
|
3278
|
+
chunks.id,
|
|
3279
|
+
chunks.source,
|
|
3218
3280
|
row_number() OVER (
|
|
3219
|
-
ORDER BY
|
|
3281
|
+
ORDER BY ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) DESC
|
|
3220
3282
|
) AS rank_ix
|
|
3221
|
-
FROM ${chunksTable}
|
|
3222
|
-
WHERE
|
|
3283
|
+
FROM ${chunksTable} as chunks
|
|
3284
|
+
WHERE chunks.fts @@ websearch_to_tsquery(?, ?)
|
|
3223
3285
|
ORDER BY rank_ix
|
|
3224
3286
|
LIMIT LEAST(?, 15) * 2
|
|
3225
3287
|
),
|
|
3226
3288
|
semantic AS (
|
|
3227
3289
|
SELECT
|
|
3228
|
-
|
|
3229
|
-
|
|
3290
|
+
chunks.id,
|
|
3291
|
+
chunks.source,
|
|
3230
3292
|
row_number() OVER (
|
|
3231
|
-
ORDER BY
|
|
3293
|
+
ORDER BY chunks.embedding <=> ${vectorExpr} ASC
|
|
3232
3294
|
) AS rank_ix
|
|
3233
|
-
FROM ${chunksTable}
|
|
3234
|
-
WHERE
|
|
3295
|
+
FROM ${chunksTable} as chunks
|
|
3296
|
+
WHERE chunks.embedding IS NOT NULL
|
|
3235
3297
|
ORDER BY rank_ix
|
|
3236
3298
|
LIMIT LEAST(?, 50) * 2
|
|
3237
3299
|
)
|
|
3238
3300
|
SELECT
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3301
|
+
items.id as item_id,
|
|
3302
|
+
items.name as item_name,
|
|
3303
|
+
items.external_id as item_external_id,
|
|
3304
|
+
chunks.id AS chunk_id,
|
|
3305
|
+
chunks.source,
|
|
3306
|
+
chunks.content,
|
|
3307
|
+
chunks.chunk_index,
|
|
3308
|
+
chunks.metadata,
|
|
3309
|
+
chunks."createdAt" as chunk_created_at,
|
|
3310
|
+
chunks."updatedAt" as chunk_updated_at,
|
|
3311
|
+
items."updatedAt" as item_updated_at,
|
|
3312
|
+
items."createdAt" as item_created_at,
|
|
3249
3313
|
/* Per-signal scores for introspection */
|
|
3250
|
-
ts_rank(
|
|
3251
|
-
(1 - (
|
|
3314
|
+
ts_rank(chunks.fts, websearch_to_tsquery(?, ?)) AS fts_rank,
|
|
3315
|
+
(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance,
|
|
3252
3316
|
|
|
3253
3317
|
/* Hybrid RRF score */
|
|
3254
3318
|
(
|
|
@@ -3260,10 +3324,10 @@ var vectorSearch = async ({
|
|
|
3260
3324
|
FROM full_text ft
|
|
3261
3325
|
FULL OUTER JOIN semantic se
|
|
3262
3326
|
ON ft.id = se.id
|
|
3263
|
-
JOIN ${chunksTable}
|
|
3264
|
-
ON COALESCE(ft.id, se.id) =
|
|
3265
|
-
JOIN ${mainTable}
|
|
3266
|
-
ON
|
|
3327
|
+
JOIN ${chunksTable} as chunks
|
|
3328
|
+
ON COALESCE(ft.id, se.id) = chunks.id
|
|
3329
|
+
JOIN ${mainTable} as items
|
|
3330
|
+
ON items.id = chunks.source
|
|
3267
3331
|
ORDER BY hybrid_score DESC
|
|
3268
3332
|
LIMIT LEAST(?, 50)
|
|
3269
3333
|
OFFSET 0
|
|
@@ -3289,86 +3353,57 @@ var vectorSearch = async ({
|
|
|
3289
3353
|
matchCount
|
|
3290
3354
|
// final limit
|
|
3291
3355
|
];
|
|
3292
|
-
|
|
3293
|
-
}
|
|
3294
|
-
console.log("[EXULU] Vector search results:",
|
|
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
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
return acc;
|
|
3338
|
-
}, []);
|
|
3339
|
-
console.log("[EXULU] Vector search results after deduplication:", items?.length);
|
|
3340
|
-
items.forEach((item) => {
|
|
3341
|
-
if (!item.chunks?.length) {
|
|
3342
|
-
return;
|
|
3343
|
-
}
|
|
3344
|
-
if (method === "tsvector") {
|
|
3345
|
-
const ranks = item.chunks.map((c) => typeof c.fts_rank === "number" ? c.fts_rank : 0);
|
|
3346
|
-
const total = ranks.reduce((a, b) => a + b, 0);
|
|
3347
|
-
const average = ranks.length ? total / ranks.length : 0;
|
|
3348
|
-
item.averageRelevance = average;
|
|
3349
|
-
item.totalRelevance = total;
|
|
3350
|
-
} else if (method === "cosineDistance") {
|
|
3351
|
-
let methodProperty = "cosine_distance";
|
|
3352
|
-
const average = item.chunks.reduce((acc, item2) => {
|
|
3353
|
-
return acc + item2[methodProperty];
|
|
3354
|
-
}, 0) / item.chunks.length;
|
|
3355
|
-
const total = item.chunks.reduce((acc, item2) => {
|
|
3356
|
-
return acc + item2[methodProperty];
|
|
3357
|
-
}, 0);
|
|
3358
|
-
item.averageRelevance = average;
|
|
3359
|
-
item.totalRelevance = total;
|
|
3360
|
-
} else if (method === "hybridSearch") {
|
|
3361
|
-
const scores = item.chunks.map((c) => typeof c.hybrid_score === "number" ? c.hybrid_score * 10 + 1 : 0);
|
|
3362
|
-
const total = scores.reduce((a, b) => a + b, 0);
|
|
3363
|
-
const average = scores.length ? total / scores.length : 0;
|
|
3364
|
-
item.averageRelevance = average;
|
|
3365
|
-
item.totalRelevance = total;
|
|
3356
|
+
resultChunks = await db3.raw(hybridSQL, bindings).then((r) => r.rows ?? r);
|
|
3357
|
+
}
|
|
3358
|
+
console.log("[EXULU] Vector search chunk results:", resultChunks?.length);
|
|
3359
|
+
resultChunks = resultChunks.map((chunk) => ({
|
|
3360
|
+
chunk_content: chunk.content,
|
|
3361
|
+
chunk_index: chunk.chunk_index,
|
|
3362
|
+
chunk_id: chunk.chunk_id,
|
|
3363
|
+
chunk_source: chunk.source,
|
|
3364
|
+
chunk_metadata: chunk.metadata,
|
|
3365
|
+
chunk_created_at: chunk.chunk_created_at,
|
|
3366
|
+
chunk_updated_at: chunk.chunk_updated_at,
|
|
3367
|
+
item_updated_at: chunk.item_updated_at,
|
|
3368
|
+
item_created_at: chunk.item_created_at,
|
|
3369
|
+
item_id: chunk.item_id,
|
|
3370
|
+
item_external_id: chunk.item_external_id,
|
|
3371
|
+
item_name: chunk.item_name,
|
|
3372
|
+
context: {
|
|
3373
|
+
name: table.name.singular,
|
|
3374
|
+
id: table.id || ""
|
|
3375
|
+
},
|
|
3376
|
+
...method === "cosineDistance" && { chunk_cosine_distance: chunk.cosine_distance },
|
|
3377
|
+
...(method === "tsvector" || method === "hybridSearch") && { chunk_fts_rank: chunk.fts_rank },
|
|
3378
|
+
...method === "hybridSearch" && { chunk_hybrid_score: chunk.hybrid_score }
|
|
3379
|
+
}));
|
|
3380
|
+
if (resultChunks.length > 0 && (method === "cosineDistance" || method === "hybridSearch")) {
|
|
3381
|
+
const scoreKey = method === "cosineDistance" ? "chunk_cosine_distance" : "chunk_hybrid_score";
|
|
3382
|
+
const topScore = resultChunks[0][scoreKey];
|
|
3383
|
+
const bottomScore = resultChunks[resultChunks.length - 1][scoreKey];
|
|
3384
|
+
const medianScore = resultChunks[Math.floor(resultChunks.length / 2)][scoreKey];
|
|
3385
|
+
console.log("[EXULU] Score distribution:", {
|
|
3386
|
+
method,
|
|
3387
|
+
count: resultChunks.length,
|
|
3388
|
+
topScore: topScore?.toFixed(4),
|
|
3389
|
+
bottomScore: bottomScore?.toFixed(4),
|
|
3390
|
+
medianScore: medianScore?.toFixed(4)
|
|
3391
|
+
});
|
|
3392
|
+
const adaptiveThreshold = topScore * 0.7;
|
|
3393
|
+
const beforeFilterCount = resultChunks.length;
|
|
3394
|
+
resultChunks = resultChunks.filter((chunk) => {
|
|
3395
|
+
const score = chunk[scoreKey];
|
|
3396
|
+
return score !== void 0 && score >= adaptiveThreshold;
|
|
3397
|
+
});
|
|
3398
|
+
const filteredCount = beforeFilterCount - resultChunks.length;
|
|
3399
|
+
if (filteredCount > 0) {
|
|
3400
|
+
console.log(`[EXULU] Filtered ${filteredCount} low-quality results (threshold: ${adaptiveThreshold.toFixed(4)})`);
|
|
3366
3401
|
}
|
|
3367
|
-
}
|
|
3402
|
+
}
|
|
3368
3403
|
if (resultReranker && query) {
|
|
3369
|
-
|
|
3404
|
+
resultChunks = await resultReranker(resultChunks);
|
|
3370
3405
|
}
|
|
3371
|
-
|
|
3406
|
+
resultChunks = resultChunks.slice(0, limit);
|
|
3372
3407
|
await updateStatistic({
|
|
3373
3408
|
name: "count",
|
|
3374
3409
|
label: table.name.singular,
|
|
@@ -3386,7 +3421,7 @@ var vectorSearch = async ({
|
|
|
3386
3421
|
id: table.id || "",
|
|
3387
3422
|
embedder: embedder.name
|
|
3388
3423
|
},
|
|
3389
|
-
|
|
3424
|
+
chunks: resultChunks
|
|
3390
3425
|
};
|
|
3391
3426
|
};
|
|
3392
3427
|
var RBACResolver = async (db3, entityName, resourceId, rights_mode) => {
|
|
@@ -3416,10 +3451,10 @@ var contextToTableDefinition = (context) => {
|
|
|
3416
3451
|
plural: tableName?.endsWith("s") ? tableName : tableName + "s"
|
|
3417
3452
|
},
|
|
3418
3453
|
RBAC: true,
|
|
3454
|
+
processor: context.processor,
|
|
3419
3455
|
fields: context.fields.map((field) => ({
|
|
3420
3456
|
name: sanitizeName(field.name),
|
|
3421
3457
|
type: field.type,
|
|
3422
|
-
processor: field.processor,
|
|
3423
3458
|
required: field.required,
|
|
3424
3459
|
default: field.default,
|
|
3425
3460
|
index: field.index,
|
|
@@ -3549,7 +3584,6 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3549
3584
|
const tableNamePlural = table.name.plural.toLowerCase();
|
|
3550
3585
|
const tableNameSingular = table.name.singular.toLowerCase();
|
|
3551
3586
|
const tableNameSingularUpperCaseFirst = table.name.singular.charAt(0).toUpperCase() + table.name.singular.slice(1);
|
|
3552
|
-
const processorFields = table.fields.filter((field) => field.processor?.execute);
|
|
3553
3587
|
typeDefs += `
|
|
3554
3588
|
${tableNameSingular === "agent" ? `${tableNameSingular}ById(id: ID!, project: ID): ${tableNameSingular}` : `${tableNameSingular}ById(id: ID!): ${tableNameSingular}`}
|
|
3555
3589
|
|
|
@@ -3576,9 +3610,10 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3576
3610
|
${tableNameSingular}ExecuteSource(source: ID!, inputs: JSON!): ${tableNameSingular}ExecuteSourceReturnPayload
|
|
3577
3611
|
${tableNameSingular}DeleteChunks(where: [Filter${tableNameSingularUpperCaseFirst}]): ${tableNameSingular}DeleteChunksReturnPayload
|
|
3578
3612
|
`;
|
|
3579
|
-
if (
|
|
3613
|
+
if (table.processor) {
|
|
3580
3614
|
mutationDefs += `
|
|
3581
|
-
${tableNameSingular}
|
|
3615
|
+
${tableNameSingular}ProcessItem(item: ID!): ${tableNameSingular}ProcessItemFieldReturnPayload
|
|
3616
|
+
${tableNameSingular}ProcessItems(limit: Int, filters: [Filter${tableNameSingularUpperCaseFirst}], sort: SortBy): ${tableNameSingular}ProcessItemFieldReturnPayload
|
|
3582
3617
|
`;
|
|
3583
3618
|
}
|
|
3584
3619
|
modelDefs += `
|
|
@@ -3596,8 +3631,8 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3596
3631
|
|
|
3597
3632
|
type ${tableNameSingular}ProcessItemFieldReturnPayload {
|
|
3598
3633
|
message: String!
|
|
3599
|
-
|
|
3600
|
-
|
|
3634
|
+
results: [String]
|
|
3635
|
+
jobs: [String]
|
|
3601
3636
|
}
|
|
3602
3637
|
|
|
3603
3638
|
type ${tableNameSingular}DeleteChunksReturnPayload {
|
|
@@ -3612,20 +3647,31 @@ function createSDL(tables, contexts, agents, tools, config, evals, queues2) {
|
|
|
3612
3647
|
tsvector
|
|
3613
3648
|
}
|
|
3614
3649
|
|
|
3615
|
-
${
|
|
3616
|
-
|
|
3617
|
-
${processorFields.map((field) => field.name).join("\n")}
|
|
3618
|
-
}
|
|
3619
|
-
` : ""}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
type ${tableNameSingular}VectorSearchResult {
|
|
3623
|
-
items: [${tableNameSingular}]!
|
|
3650
|
+
type ${tableNameSingular}VectorSearchResult {
|
|
3651
|
+
chunks: [${tableNameSingular}VectorSearchChunk!]!
|
|
3624
3652
|
context: VectoSearchResultContext!
|
|
3625
3653
|
filters: JSON!
|
|
3626
3654
|
query: String!
|
|
3627
3655
|
method: VectorMethodEnum!
|
|
3628
3656
|
}
|
|
3657
|
+
|
|
3658
|
+
type ${tableNameSingular}VectorSearchChunk {
|
|
3659
|
+
chunk_content: String
|
|
3660
|
+
chunk_index: Int
|
|
3661
|
+
chunk_id: String
|
|
3662
|
+
chunk_source: String
|
|
3663
|
+
chunk_metadata: JSON
|
|
3664
|
+
chunk_created_at: Date
|
|
3665
|
+
chunk_updated_at: Date
|
|
3666
|
+
item_updated_at: Date
|
|
3667
|
+
item_created_at: Date
|
|
3668
|
+
item_id: String!
|
|
3669
|
+
item_external_id: String
|
|
3670
|
+
item_name: String!
|
|
3671
|
+
chunk_cosine_distance: Float
|
|
3672
|
+
chunk_fts_rank: Float
|
|
3673
|
+
chunk_hybrid_score: Float
|
|
3674
|
+
}
|
|
3629
3675
|
|
|
3630
3676
|
type VectoSearchResultContext {
|
|
3631
3677
|
name: String!
|
|
@@ -3730,7 +3776,11 @@ type PageInfo {
|
|
|
3730
3776
|
const config2 = await queue.use();
|
|
3731
3777
|
return {
|
|
3732
3778
|
name: config2.queue.name,
|
|
3733
|
-
concurrency:
|
|
3779
|
+
concurrency: {
|
|
3780
|
+
worker: config2.concurrency?.worker || void 0,
|
|
3781
|
+
queue: config2.concurrency?.queue || void 0
|
|
3782
|
+
},
|
|
3783
|
+
timeoutInSeconds: config2.timeoutInSeconds,
|
|
3734
3784
|
ratelimit: config2.ratelimit,
|
|
3735
3785
|
isMaxed: await config2.queue.isMaxed(),
|
|
3736
3786
|
isPaused: await config2.queue.isPaused(),
|
|
@@ -3780,7 +3830,10 @@ type PageInfo {
|
|
|
3780
3830
|
if (!agentInstance) {
|
|
3781
3831
|
throw new Error("Agent instance not found for eval run.");
|
|
3782
3832
|
}
|
|
3783
|
-
const evalQueue = await queues.register("eval_runs",
|
|
3833
|
+
const evalQueue = await queues.register("eval_runs", {
|
|
3834
|
+
worker: 1,
|
|
3835
|
+
queue: 1
|
|
3836
|
+
}, 1).use();
|
|
3784
3837
|
const jobIds = [];
|
|
3785
3838
|
for (const testCase of testCases) {
|
|
3786
3839
|
const jobData = {
|
|
@@ -3904,7 +3957,6 @@ type PageInfo {
|
|
|
3904
3957
|
if (!client2) {
|
|
3905
3958
|
throw new Error("Redis client not created properly");
|
|
3906
3959
|
}
|
|
3907
|
-
console.log("[EXULU] Jobs pagination args", args);
|
|
3908
3960
|
const {
|
|
3909
3961
|
jobs,
|
|
3910
3962
|
count
|
|
@@ -3914,7 +3966,6 @@ type PageInfo {
|
|
|
3914
3966
|
args.page || 1,
|
|
3915
3967
|
args.limit || 100
|
|
3916
3968
|
);
|
|
3917
|
-
console.log("[EXULU] jobs", jobs.map((job) => job.name));
|
|
3918
3969
|
const requestedFields = getRequestedFields(info);
|
|
3919
3970
|
return {
|
|
3920
3971
|
items: await Promise.all(jobs.map(async (job) => {
|
|
@@ -3943,6 +3994,21 @@ type PageInfo {
|
|
|
3943
3994
|
};
|
|
3944
3995
|
resolvers.Query["contexts"] = async (_, args, context, info) => {
|
|
3945
3996
|
const data = await Promise.all(contexts.map(async (context2) => {
|
|
3997
|
+
let processor = null;
|
|
3998
|
+
if (context2.processor) {
|
|
3999
|
+
processor = await new Promise(async (resolve, reject) => {
|
|
4000
|
+
const config2 = await context2.processor?.config;
|
|
4001
|
+
const queue = await config2?.queue;
|
|
4002
|
+
resolve({
|
|
4003
|
+
name: context2.processor.name,
|
|
4004
|
+
description: context2.processor.description,
|
|
4005
|
+
queue: queue?.queue?.name || void 0,
|
|
4006
|
+
trigger: context2.processor?.config?.trigger || "manual",
|
|
4007
|
+
timeoutInSeconds: queue?.timeoutInSeconds || 600,
|
|
4008
|
+
generateEmbeddings: context2.processor?.config?.generateEmbeddings || false
|
|
4009
|
+
});
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
3946
4012
|
const sources = await Promise.all(context2.sources.map(async (source) => {
|
|
3947
4013
|
let queueName = void 0;
|
|
3948
4014
|
if (source.config) {
|
|
@@ -3974,6 +4040,7 @@ type PageInfo {
|
|
|
3974
4040
|
slug: "/contexts/" + context2.id,
|
|
3975
4041
|
active: context2.active,
|
|
3976
4042
|
sources,
|
|
4043
|
+
processor,
|
|
3977
4044
|
fields: context2.fields.map((field) => {
|
|
3978
4045
|
return {
|
|
3979
4046
|
...field,
|
|
@@ -3999,6 +4066,21 @@ type PageInfo {
|
|
|
3999
4066
|
if (!data) {
|
|
4000
4067
|
return null;
|
|
4001
4068
|
}
|
|
4069
|
+
let processor = null;
|
|
4070
|
+
if (data.processor) {
|
|
4071
|
+
processor = await new Promise(async (resolve, reject) => {
|
|
4072
|
+
const config2 = await data.processor?.config;
|
|
4073
|
+
const queue = await config2?.queue;
|
|
4074
|
+
resolve({
|
|
4075
|
+
name: data.processor.name,
|
|
4076
|
+
description: data.processor.description,
|
|
4077
|
+
queue: queue?.queue?.name || void 0,
|
|
4078
|
+
trigger: data.processor?.config?.trigger || "manual",
|
|
4079
|
+
timeoutInSeconds: queue?.timeoutInSeconds || 600,
|
|
4080
|
+
generateEmbeddings: data.processor?.config?.generateEmbeddings || false
|
|
4081
|
+
});
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4002
4084
|
const sources = await Promise.all(data.sources.map(async (source) => {
|
|
4003
4085
|
let queueName = void 0;
|
|
4004
4086
|
if (source.config) {
|
|
@@ -4035,35 +4117,18 @@ type PageInfo {
|
|
|
4035
4117
|
slug: "/contexts/" + data.id,
|
|
4036
4118
|
active: data.active,
|
|
4037
4119
|
sources,
|
|
4120
|
+
processor,
|
|
4038
4121
|
fields: await Promise.all(data.fields.map(async (field) => {
|
|
4039
4122
|
const label = field.name?.replace("_s3key", "");
|
|
4040
4123
|
if (field.type === "file" && !field.name.endsWith("_s3key")) {
|
|
4041
4124
|
field.name = field.name + "_s3key";
|
|
4042
4125
|
}
|
|
4043
|
-
let queue = null;
|
|
4044
|
-
if (field.processor?.config?.queue) {
|
|
4045
|
-
queue = await field.processor.config.queue;
|
|
4046
|
-
}
|
|
4047
4126
|
return {
|
|
4048
4127
|
...field,
|
|
4049
4128
|
name: sanitizeName(field.name),
|
|
4050
4129
|
...field.type === "file" ? {
|
|
4051
4130
|
allowedFileTypes: field.allowedFileTypes
|
|
4052
4131
|
} : {},
|
|
4053
|
-
...field.processor ? {
|
|
4054
|
-
processor: {
|
|
4055
|
-
description: field.processor?.description,
|
|
4056
|
-
config: {
|
|
4057
|
-
trigger: field.processor?.config?.trigger,
|
|
4058
|
-
queue: {
|
|
4059
|
-
name: queue?.queue.name || void 0,
|
|
4060
|
-
ratelimit: queue?.ratelimit || void 0,
|
|
4061
|
-
concurrency: queue?.concurrency || void 0
|
|
4062
|
-
}
|
|
4063
|
-
},
|
|
4064
|
-
execute: "function"
|
|
4065
|
-
}
|
|
4066
|
-
} : {},
|
|
4067
4132
|
label
|
|
4068
4133
|
};
|
|
4069
4134
|
})),
|
|
@@ -4131,13 +4196,20 @@ type PageInfo {
|
|
|
4131
4196
|
modelDefs += `
|
|
4132
4197
|
type QueueResult {
|
|
4133
4198
|
name: String!
|
|
4134
|
-
concurrency:
|
|
4199
|
+
concurrency: QueueConcurrency!
|
|
4200
|
+
timeoutInSeconds: Int!
|
|
4135
4201
|
ratelimit: Int!
|
|
4136
4202
|
isMaxed: Boolean!
|
|
4137
4203
|
isPaused: Boolean!
|
|
4138
4204
|
jobs: QueueJobsCounts
|
|
4139
4205
|
}
|
|
4140
4206
|
`;
|
|
4207
|
+
modelDefs += `
|
|
4208
|
+
type QueueConcurrency {
|
|
4209
|
+
worker: Int
|
|
4210
|
+
queue: Int
|
|
4211
|
+
}
|
|
4212
|
+
`;
|
|
4141
4213
|
modelDefs += `
|
|
4142
4214
|
type QueueJobsCounts {
|
|
4143
4215
|
paused: Int!
|
|
@@ -4207,17 +4279,12 @@ type AgentEvalFunctionConfig {
|
|
|
4207
4279
|
}
|
|
4208
4280
|
|
|
4209
4281
|
type ItemChunks {
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
chunk_id: ID
|
|
4217
|
-
chunk_created_at: Date
|
|
4218
|
-
chunk_updated_at: Date
|
|
4219
|
-
embedding_size: Float
|
|
4220
|
-
metadata: JSON
|
|
4282
|
+
chunk_id: String!
|
|
4283
|
+
chunk_index: Int!
|
|
4284
|
+
chunk_content: String!
|
|
4285
|
+
chunk_source: String!
|
|
4286
|
+
chunk_created_at: Date!
|
|
4287
|
+
chunk_updated_at: Date!
|
|
4221
4288
|
}
|
|
4222
4289
|
|
|
4223
4290
|
type Provider {
|
|
@@ -4252,7 +4319,8 @@ type Context {
|
|
|
4252
4319
|
active: Boolean
|
|
4253
4320
|
fields: JSON
|
|
4254
4321
|
configuration: JSON
|
|
4255
|
-
sources: [ContextSource
|
|
4322
|
+
sources: [ContextSource]
|
|
4323
|
+
processor: ContextProcessor
|
|
4256
4324
|
}
|
|
4257
4325
|
type Embedder {
|
|
4258
4326
|
name: String!
|
|
@@ -4265,6 +4333,14 @@ type EmbedderConfig {
|
|
|
4265
4333
|
description: String
|
|
4266
4334
|
default: String
|
|
4267
4335
|
}
|
|
4336
|
+
type ContextProcessor {
|
|
4337
|
+
name: String!
|
|
4338
|
+
description: String
|
|
4339
|
+
queue: String
|
|
4340
|
+
trigger: String
|
|
4341
|
+
timeoutInSeconds: Int
|
|
4342
|
+
generateEmbeddings: Boolean
|
|
4343
|
+
}
|
|
4268
4344
|
|
|
4269
4345
|
type ContextSource {
|
|
4270
4346
|
id: String!
|
|
@@ -4322,6 +4398,9 @@ type Job {
|
|
|
4322
4398
|
name: String!
|
|
4323
4399
|
returnvalue: JSON
|
|
4324
4400
|
stacktrace: [String]
|
|
4401
|
+
finishedOn: Date
|
|
4402
|
+
processedOn: Date
|
|
4403
|
+
attemptsMade: Int
|
|
4325
4404
|
failedReason: String
|
|
4326
4405
|
state: String!
|
|
4327
4406
|
data: JSON
|
|
@@ -4383,10 +4462,7 @@ async function getJobsByQueueName(queueName, statusses, page, limit) {
|
|
|
4383
4462
|
const config = await queue.use();
|
|
4384
4463
|
const startIndex = (page || 1) - 1;
|
|
4385
4464
|
const endIndex = startIndex - 1 + (limit || 100);
|
|
4386
|
-
console.log("[EXULU] Jobs pagination startIndex", startIndex);
|
|
4387
|
-
console.log("[EXULU] Jobs pagination endIndex", endIndex);
|
|
4388
4465
|
const jobs = await config.queue.getJobs(statusses || [], startIndex, endIndex, false);
|
|
4389
|
-
console.log("[EXULU] Jobs pagination jobs", jobs?.length);
|
|
4390
4466
|
const counts = await config.queue.getJobCounts(...statusses || []);
|
|
4391
4467
|
let total = 0;
|
|
4392
4468
|
if (counts) {
|
|
@@ -4427,21 +4503,12 @@ function getS3Client(config) {
|
|
|
4427
4503
|
});
|
|
4428
4504
|
return s3Client;
|
|
4429
4505
|
}
|
|
4430
|
-
var getPresignedUrl = async (key, config) => {
|
|
4506
|
+
var getPresignedUrl = async (bucket, key, config) => {
|
|
4431
4507
|
if (!config.fileUploads) {
|
|
4432
4508
|
throw new Error("File uploads are not configured");
|
|
4433
4509
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
console.log("[EXULU] key includes [bucket:name]", key);
|
|
4437
|
-
bucket = key.split("[bucket:")[1]?.split("]")[0] || "";
|
|
4438
|
-
if (!bucket?.length) {
|
|
4439
|
-
throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
|
|
4440
|
-
}
|
|
4441
|
-
key = key.split("]")[1] || "";
|
|
4442
|
-
console.log("[EXULU] bucket", bucket);
|
|
4443
|
-
console.log("[EXULU] key", key);
|
|
4444
|
-
}
|
|
4510
|
+
console.log("[EXULU] getting presigned url for bucket", bucket);
|
|
4511
|
+
console.log("[EXULU] getting presigned url for key", key);
|
|
4445
4512
|
const url = await (0, import_s3_request_presigner.getSignedUrl)(
|
|
4446
4513
|
getS3Client(config),
|
|
4447
4514
|
new import_client_s3.GetObjectCommand({
|
|
@@ -4452,7 +4519,7 @@ var getPresignedUrl = async (key, config) => {
|
|
|
4452
4519
|
);
|
|
4453
4520
|
return url;
|
|
4454
4521
|
};
|
|
4455
|
-
var
|
|
4522
|
+
var addGeneralPrefixToKey = (keyPath, config) => {
|
|
4456
4523
|
if (!config.fileUploads) {
|
|
4457
4524
|
throw new Error("File uploads are not configured");
|
|
4458
4525
|
}
|
|
@@ -4465,58 +4532,50 @@ var addPrefixToKey = (keyPath, config) => {
|
|
|
4465
4532
|
}
|
|
4466
4533
|
return `${prefix}/${keyPath}`;
|
|
4467
4534
|
};
|
|
4468
|
-
var
|
|
4535
|
+
var addUserPrefixToKey = (key, user) => {
|
|
4536
|
+
if (!user) {
|
|
4537
|
+
return key;
|
|
4538
|
+
}
|
|
4539
|
+
if (key.includes(`/user_${user}/`)) {
|
|
4540
|
+
return key;
|
|
4541
|
+
}
|
|
4542
|
+
return `user_${user}/${key}`;
|
|
4543
|
+
};
|
|
4544
|
+
var addBucketPrefixToKey = (key, bucket) => {
|
|
4545
|
+
if (key.includes(`/${bucket}/`)) {
|
|
4546
|
+
return key;
|
|
4547
|
+
}
|
|
4548
|
+
return `${bucket}/${key}`;
|
|
4549
|
+
};
|
|
4550
|
+
var uploadFile = async (file, fileName, config, options = {}, user, customBucket) => {
|
|
4469
4551
|
if (!config.fileUploads) {
|
|
4470
4552
|
throw new Error("File uploads are not configured (in the exported uploadFile function)");
|
|
4471
4553
|
}
|
|
4472
4554
|
const client2 = getS3Client(config);
|
|
4473
4555
|
let defaultBucket = config.fileUploads.s3Bucket;
|
|
4474
|
-
let
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
if (!customBucket?.length) {
|
|
4479
|
-
throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
|
|
4480
|
-
}
|
|
4481
|
-
key = key.split("]")[1] || "";
|
|
4482
|
-
console.log("[EXULU] custom bucket", customBucket);
|
|
4483
|
-
}
|
|
4484
|
-
let folder = user ? `${user}/` : "";
|
|
4485
|
-
const fullKey = addPrefixToKey(!key.includes(folder) ? folder + key : key, config);
|
|
4486
|
-
console.log("[EXULU] uploading file to s3 into bucket", customBucket || defaultBucket, "with key", fullKey);
|
|
4556
|
+
let key = fileName;
|
|
4557
|
+
key = addGeneralPrefixToKey(key, config);
|
|
4558
|
+
key = addUserPrefixToKey(key, user || "api");
|
|
4559
|
+
console.log("[EXULU] uploading file to s3 into bucket", defaultBucket, "with key", key);
|
|
4487
4560
|
const command = new import_client_s3.PutObjectCommand({
|
|
4488
4561
|
Bucket: customBucket || defaultBucket,
|
|
4489
|
-
Key:
|
|
4562
|
+
Key: key,
|
|
4490
4563
|
Body: file,
|
|
4491
4564
|
ContentType: options.contentType,
|
|
4492
4565
|
Metadata: options.metadata,
|
|
4493
4566
|
ContentLength: file.byteLength
|
|
4494
4567
|
});
|
|
4495
4568
|
await client2.send(command);
|
|
4496
|
-
console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key",
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4569
|
+
console.log("[EXULU] file uploaded to s3 into bucket", customBucket || defaultBucket, "with key", key);
|
|
4570
|
+
return addBucketPrefixToKey(
|
|
4571
|
+
key,
|
|
4572
|
+
customBucket || defaultBucket
|
|
4573
|
+
);
|
|
4501
4574
|
};
|
|
4502
|
-
var createUppyRoutes = async (app, config) => {
|
|
4575
|
+
var createUppyRoutes = async (app, contexts, config) => {
|
|
4503
4576
|
if (!config.fileUploads) {
|
|
4504
4577
|
throw new Error("File uploads are not configured");
|
|
4505
4578
|
}
|
|
4506
|
-
const extractUserPrefix = (key) => {
|
|
4507
|
-
if (!config.fileUploads) {
|
|
4508
|
-
throw new Error("File uploads are not configured");
|
|
4509
|
-
}
|
|
4510
|
-
if (!config.fileUploads.s3prefix) {
|
|
4511
|
-
return key.split("/")[0];
|
|
4512
|
-
}
|
|
4513
|
-
const prefix = config.fileUploads.s3prefix.replace(/\/$/, "");
|
|
4514
|
-
if (key.startsWith(prefix + "/")) {
|
|
4515
|
-
const keyWithoutPrefix = key.slice(prefix.length + 1);
|
|
4516
|
-
return keyWithoutPrefix.split("/")[0];
|
|
4517
|
-
}
|
|
4518
|
-
return key.split("/")[0];
|
|
4519
|
-
};
|
|
4520
4579
|
const policy = {
|
|
4521
4580
|
Version: "2012-10-17",
|
|
4522
4581
|
Statement: [
|
|
@@ -4568,20 +4627,16 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4568
4627
|
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4569
4628
|
return;
|
|
4570
4629
|
}
|
|
4630
|
+
const user = authenticationResult.user;
|
|
4571
4631
|
let { key } = req.query;
|
|
4572
4632
|
if (typeof key !== "string" || key.trim() === "") {
|
|
4573
4633
|
res.status(400).json({ error: "Missing or invalid `key` query parameter." });
|
|
4574
4634
|
return;
|
|
4575
4635
|
}
|
|
4576
|
-
let bucket =
|
|
4577
|
-
if (key.includes(
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
if (!bucket?.length) {
|
|
4581
|
-
throw new Error("Invalid key, does not contain a bucket name like '[bucket:name]'.");
|
|
4582
|
-
}
|
|
4583
|
-
key = key.split("]")[1] || "";
|
|
4584
|
-
console.log("[EXULU] bucket", bucket);
|
|
4636
|
+
let bucket = key.split("/")[0];
|
|
4637
|
+
if (user.type !== "api" && !key.includes(`/user_${user.id}/`) && !user.super_admin) {
|
|
4638
|
+
res.status(405).json({ error: "Not allowed to access the files in the folder based on authenticated user." });
|
|
4639
|
+
return;
|
|
4585
4640
|
}
|
|
4586
4641
|
const client2 = getS3Client(config);
|
|
4587
4642
|
const command = new import_client_s3.DeleteObjectCommand({
|
|
@@ -4609,13 +4664,32 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4609
4664
|
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4610
4665
|
return;
|
|
4611
4666
|
}
|
|
4612
|
-
const
|
|
4667
|
+
const user = authenticationResult.user;
|
|
4668
|
+
let { key } = req.query;
|
|
4669
|
+
if (!key || typeof key !== "string" || key.trim() === "") {
|
|
4670
|
+
res.status(400).json({ error: "Missing or invalid `key` query parameter." });
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
let bucket = key.split("/")[0];
|
|
4674
|
+
if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
|
|
4675
|
+
res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
key = key.split("/").slice(1).join("/");
|
|
4613
4679
|
if (typeof key !== "string" || key.trim() === "") {
|
|
4614
4680
|
res.status(400).json({ error: "Missing or invalid `key` query parameter." });
|
|
4615
4681
|
return;
|
|
4616
4682
|
}
|
|
4683
|
+
let allowed = false;
|
|
4684
|
+
if (user.type === "api" || user.super_admin || key.includes(`user_${user.id}/`)) {
|
|
4685
|
+
allowed = true;
|
|
4686
|
+
}
|
|
4687
|
+
if (!allowed) {
|
|
4688
|
+
res.status(405).json({ error: "Not allowed to access the file based on authenticated user." });
|
|
4689
|
+
return;
|
|
4690
|
+
}
|
|
4617
4691
|
try {
|
|
4618
|
-
const url = await getPresignedUrl(key, config);
|
|
4692
|
+
const url = await getPresignedUrl(bucket, key, config);
|
|
4619
4693
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4620
4694
|
res.json({ url, method: "GET", expiresIn });
|
|
4621
4695
|
} catch (err) {
|
|
@@ -4644,16 +4718,15 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4644
4718
|
return;
|
|
4645
4719
|
}
|
|
4646
4720
|
let { key } = req.body;
|
|
4647
|
-
let bucket =
|
|
4648
|
-
if (
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
}
|
|
4655
|
-
|
|
4656
|
-
console.log("[EXULU] key", key);
|
|
4721
|
+
let bucket = key.split("/")[0];
|
|
4722
|
+
if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
|
|
4723
|
+
res.status(400).json({ error: "Missing or invalid `bucket` (should be the first part of the key before the first slash)." });
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4726
|
+
key = key.split("/").slice(1).join("/");
|
|
4727
|
+
if (!key || typeof key !== "string" || key.trim() === "") {
|
|
4728
|
+
res.status(400).json({ error: "Missing or invalid `key` query parameter." });
|
|
4729
|
+
return;
|
|
4657
4730
|
}
|
|
4658
4731
|
const client2 = getS3Client(config);
|
|
4659
4732
|
const command = new import_client_s3.HeadObjectCommand({
|
|
@@ -4757,11 +4830,12 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4757
4830
|
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4758
4831
|
return;
|
|
4759
4832
|
}
|
|
4833
|
+
const user = authenticationResult.user;
|
|
4760
4834
|
const { filename, contentType } = extractFileParameters(req);
|
|
4761
4835
|
validateFileParameters(filename, contentType);
|
|
4762
4836
|
const key = generateS3Key2(filename);
|
|
4763
|
-
let
|
|
4764
|
-
|
|
4837
|
+
let fullKey = addGeneralPrefixToKey(key, config);
|
|
4838
|
+
fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
|
|
4765
4839
|
(0, import_s3_request_presigner.getSignedUrl)(
|
|
4766
4840
|
getS3Client(config),
|
|
4767
4841
|
new import_client_s3.PutObjectCommand({
|
|
@@ -4805,6 +4879,7 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4805
4879
|
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
4806
4880
|
return;
|
|
4807
4881
|
}
|
|
4882
|
+
const user = authenticationResult.user;
|
|
4808
4883
|
const client2 = getS3Client(config);
|
|
4809
4884
|
const { type, metadata, filename } = req.body;
|
|
4810
4885
|
if (typeof filename !== "string") {
|
|
@@ -4814,13 +4889,8 @@ var createUppyRoutes = async (app, config) => {
|
|
|
4814
4889
|
return res.status(400).json({ error: "s3: content type must be a string" });
|
|
4815
4890
|
}
|
|
4816
4891
|
const key = `${(0, import_node_crypto.randomUUID)()}-_EXULU_${filename}`;
|
|
4817
|
-
let
|
|
4818
|
-
|
|
4819
|
-
folder = `api/`;
|
|
4820
|
-
} else {
|
|
4821
|
-
folder = `${authenticationResult.user.id}/`;
|
|
4822
|
-
}
|
|
4823
|
-
const fullKey = addPrefixToKey(folder + key, config);
|
|
4892
|
+
let fullKey = addGeneralPrefixToKey(key, config);
|
|
4893
|
+
fullKey = addUserPrefixToKey(fullKey, user.type === "api" ? "api" : user.id);
|
|
4824
4894
|
const params = {
|
|
4825
4895
|
Bucket: config.fileUploads.s3Bucket,
|
|
4826
4896
|
Key: fullKey,
|
|
@@ -5023,7 +5093,7 @@ var createProjectRetrievalTool = async ({
|
|
|
5023
5093
|
if (!context) {
|
|
5024
5094
|
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.");
|
|
5025
5095
|
}
|
|
5026
|
-
const id = item.split("/")
|
|
5096
|
+
const id = item.split("/").slice(1).join("/");
|
|
5027
5097
|
if (set[context]) {
|
|
5028
5098
|
set[context].push(id);
|
|
5029
5099
|
} else {
|
|
@@ -5969,12 +6039,20 @@ var ExuluStorage = class {
|
|
|
5969
6039
|
this.config = config;
|
|
5970
6040
|
}
|
|
5971
6041
|
getPresignedUrl = async (key) => {
|
|
5972
|
-
|
|
6042
|
+
const bucket = key.split("/")[0];
|
|
6043
|
+
if (!bucket || typeof bucket !== "string" || bucket.trim() === "") {
|
|
6044
|
+
throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
|
|
6045
|
+
}
|
|
6046
|
+
key = key.split("/").slice(1).join("/");
|
|
6047
|
+
if (!key || typeof key !== "string" || key.trim() === "") {
|
|
6048
|
+
throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
|
|
6049
|
+
}
|
|
6050
|
+
return await getPresignedUrl(bucket, key, this.config);
|
|
5973
6051
|
};
|
|
5974
|
-
uploadFile = async (file,
|
|
6052
|
+
uploadFile = async (file, fileName, type, user, metadata, customBucket) => {
|
|
5975
6053
|
return await uploadFile(
|
|
5976
6054
|
file,
|
|
5977
|
-
|
|
6055
|
+
fileName,
|
|
5978
6056
|
this.config,
|
|
5979
6057
|
{
|
|
5980
6058
|
contentType: type,
|
|
@@ -5983,7 +6061,8 @@ var ExuluStorage = class {
|
|
|
5983
6061
|
type
|
|
5984
6062
|
}
|
|
5985
6063
|
},
|
|
5986
|
-
user
|
|
6064
|
+
user,
|
|
6065
|
+
customBucket
|
|
5987
6066
|
);
|
|
5988
6067
|
};
|
|
5989
6068
|
// todo add upload and delete methods
|
|
@@ -5996,12 +6075,12 @@ var ExuluContext = class {
|
|
|
5996
6075
|
name;
|
|
5997
6076
|
active;
|
|
5998
6077
|
fields;
|
|
6078
|
+
processor;
|
|
5999
6079
|
rateLimit;
|
|
6000
6080
|
description;
|
|
6001
6081
|
embedder;
|
|
6002
6082
|
queryRewriter;
|
|
6003
6083
|
resultReranker;
|
|
6004
|
-
// todo typings
|
|
6005
6084
|
configuration;
|
|
6006
6085
|
sources = [];
|
|
6007
6086
|
constructor({
|
|
@@ -6009,6 +6088,7 @@ var ExuluContext = class {
|
|
|
6009
6088
|
name,
|
|
6010
6089
|
description,
|
|
6011
6090
|
embedder,
|
|
6091
|
+
processor,
|
|
6012
6092
|
active,
|
|
6013
6093
|
rateLimit,
|
|
6014
6094
|
fields,
|
|
@@ -6021,10 +6101,12 @@ var ExuluContext = class {
|
|
|
6021
6101
|
this.name = name;
|
|
6022
6102
|
this.fields = fields || [];
|
|
6023
6103
|
this.sources = sources || [];
|
|
6104
|
+
this.processor = processor;
|
|
6024
6105
|
this.configuration = configuration || {
|
|
6025
6106
|
calculateVectors: "manual",
|
|
6026
6107
|
language: "english",
|
|
6027
|
-
defaultRightsMode: "private"
|
|
6108
|
+
defaultRightsMode: "private",
|
|
6109
|
+
maxRetrievalResults: 10
|
|
6028
6110
|
};
|
|
6029
6111
|
this.description = description;
|
|
6030
6112
|
this.embedder = embedder;
|
|
@@ -6034,23 +6116,18 @@ var ExuluContext = class {
|
|
|
6034
6116
|
this.resultReranker = resultReranker;
|
|
6035
6117
|
}
|
|
6036
6118
|
processField = async (trigger, item, exuluConfig, user, role) => {
|
|
6037
|
-
console.log("[EXULU] processing
|
|
6038
|
-
console.log("[EXULU] fields", this.fields.map((field2) => field2.name));
|
|
6039
|
-
const field = this.fields.find((field2) => {
|
|
6040
|
-
return field2.name.replace("_s3key", "") === item.field.replace("_s3key", "");
|
|
6041
|
-
});
|
|
6042
|
-
if (!field || !field.processor) {
|
|
6043
|
-
console.error("[EXULU] field not found or processor not set for field", item.field, " in context", this.id);
|
|
6044
|
-
throw new Error("Field not found or processor not set for field " + item.field + " in context " + this.id);
|
|
6045
|
-
}
|
|
6119
|
+
console.log("[EXULU] processing item, ", item, " in context", this.id);
|
|
6046
6120
|
const exuluStorage = new ExuluStorage({ config: exuluConfig });
|
|
6047
|
-
|
|
6121
|
+
if (!this.processor) {
|
|
6122
|
+
throw new Error(`Processor is not set for this context: ${this.id}.`);
|
|
6123
|
+
}
|
|
6124
|
+
const queue = await this.processor.config?.queue;
|
|
6048
6125
|
if (queue?.queue.name) {
|
|
6049
6126
|
console.log("[EXULU] processor is in queue mode, scheduling job.");
|
|
6050
6127
|
const job = await bullmqDecorator({
|
|
6051
|
-
timeoutInSeconds:
|
|
6052
|
-
label: `${this.name} ${
|
|
6053
|
-
processor: `${this.id}-${
|
|
6128
|
+
timeoutInSeconds: this.processor.config?.timeoutInSeconds || 600,
|
|
6129
|
+
label: `${this.name} ${this.processor.name} data processor`,
|
|
6130
|
+
processor: `${this.id}-${this.processor.name}`,
|
|
6054
6131
|
context: this.id,
|
|
6055
6132
|
inputs: item,
|
|
6056
6133
|
item: item.id,
|
|
@@ -6065,27 +6142,33 @@ var ExuluContext = class {
|
|
|
6065
6142
|
trigger
|
|
6066
6143
|
});
|
|
6067
6144
|
return {
|
|
6068
|
-
result:
|
|
6145
|
+
result: void 0,
|
|
6069
6146
|
job: job.id
|
|
6070
6147
|
};
|
|
6071
6148
|
}
|
|
6072
6149
|
console.log("[EXULU] POS 1 -- EXULU CONTEXT PROCESS FIELD");
|
|
6073
|
-
const
|
|
6150
|
+
const processorResult = await this.processor.execute({
|
|
6074
6151
|
item,
|
|
6075
6152
|
user,
|
|
6076
6153
|
role,
|
|
6077
6154
|
utils: {
|
|
6078
|
-
storage: exuluStorage
|
|
6079
|
-
items: {
|
|
6080
|
-
update: this.updateItem,
|
|
6081
|
-
create: this.createItem,
|
|
6082
|
-
delete: this.deleteItem
|
|
6083
|
-
}
|
|
6155
|
+
storage: exuluStorage
|
|
6084
6156
|
},
|
|
6085
6157
|
exuluConfig
|
|
6086
6158
|
});
|
|
6159
|
+
if (!processorResult) {
|
|
6160
|
+
throw new Error("Processor result is required for updating the item in the db.");
|
|
6161
|
+
}
|
|
6162
|
+
const { db: db3 } = await postgresClient();
|
|
6163
|
+
delete processorResult.field;
|
|
6164
|
+
await db3.from(getTableName(this.id)).where({
|
|
6165
|
+
id: processorResult.id
|
|
6166
|
+
}).update({
|
|
6167
|
+
...processorResult,
|
|
6168
|
+
last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6169
|
+
});
|
|
6087
6170
|
return {
|
|
6088
|
-
result,
|
|
6171
|
+
result: processorResult,
|
|
6089
6172
|
job: void 0
|
|
6090
6173
|
};
|
|
6091
6174
|
};
|
|
@@ -6096,7 +6179,8 @@ var ExuluContext = class {
|
|
|
6096
6179
|
user: options.user,
|
|
6097
6180
|
role: options.role,
|
|
6098
6181
|
context: this,
|
|
6099
|
-
db: db3
|
|
6182
|
+
db: db3,
|
|
6183
|
+
limit: options?.limit || this.configuration.maxRetrievalResults || 10
|
|
6100
6184
|
});
|
|
6101
6185
|
return result;
|
|
6102
6186
|
};
|
|
@@ -6170,6 +6254,8 @@ var ExuluContext = class {
|
|
|
6170
6254
|
};
|
|
6171
6255
|
};
|
|
6172
6256
|
createItem = async (item, config, user, role, upsert, generateEmbeddingsOverwrite) => {
|
|
6257
|
+
console.log("[EXULU] creating item", item);
|
|
6258
|
+
console.log("[EXULU] upsert", upsert);
|
|
6173
6259
|
if (upsert && (!item.id && !item.external_id)) {
|
|
6174
6260
|
throw new Error("Item id or external id is required for upsert.");
|
|
6175
6261
|
}
|
|
@@ -6197,10 +6283,9 @@ var ExuluContext = class {
|
|
|
6197
6283
|
}
|
|
6198
6284
|
console.log("[EXULU] context configuration", this.configuration);
|
|
6199
6285
|
let jobs = [];
|
|
6200
|
-
let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
const processor = this.fields.find((field) => field.name === key.replace("_s3key", ""))?.processor;
|
|
6286
|
+
let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onInsert" || this.configuration.calculateVectors === "always");
|
|
6287
|
+
if (this.processor) {
|
|
6288
|
+
const processor = this.processor;
|
|
6204
6289
|
console.log("[EXULU] Processor found", processor);
|
|
6205
6290
|
if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
|
|
6206
6291
|
const {
|
|
@@ -6210,8 +6295,7 @@ var ExuluContext = class {
|
|
|
6210
6295
|
"api",
|
|
6211
6296
|
{
|
|
6212
6297
|
...item,
|
|
6213
|
-
id: results[0].id
|
|
6214
|
-
field: key
|
|
6298
|
+
id: results[0].id
|
|
6215
6299
|
},
|
|
6216
6300
|
config,
|
|
6217
6301
|
user,
|
|
@@ -6220,8 +6304,13 @@ var ExuluContext = class {
|
|
|
6220
6304
|
if (processorJob) {
|
|
6221
6305
|
jobs.push(processorJob);
|
|
6222
6306
|
}
|
|
6223
|
-
if (!processorJob
|
|
6224
|
-
|
|
6307
|
+
if (!processorJob) {
|
|
6308
|
+
await db3.from(getTableName(this.id)).where({ id: results[0].id }).update({
|
|
6309
|
+
...processorResult
|
|
6310
|
+
});
|
|
6311
|
+
if (processor.config?.generateEmbeddings) {
|
|
6312
|
+
shouldGenerateEmbeddings = true;
|
|
6313
|
+
}
|
|
6225
6314
|
}
|
|
6226
6315
|
}
|
|
6227
6316
|
}
|
|
@@ -6247,6 +6336,7 @@ var ExuluContext = class {
|
|
|
6247
6336
|
};
|
|
6248
6337
|
};
|
|
6249
6338
|
updateItem = async (item, config, user, role, generateEmbeddingsOverwrite) => {
|
|
6339
|
+
console.log("[EXULU] updating item", item);
|
|
6250
6340
|
const { db: db3 } = await postgresClient();
|
|
6251
6341
|
if (item.field) {
|
|
6252
6342
|
delete item.field;
|
|
@@ -6272,8 +6362,8 @@ var ExuluContext = class {
|
|
|
6272
6362
|
await mutation;
|
|
6273
6363
|
let jobs = [];
|
|
6274
6364
|
let shouldGenerateEmbeddings = this.embedder && generateEmbeddingsOverwrite !== false && (generateEmbeddingsOverwrite || this.configuration.calculateVectors === "onUpdate" || this.configuration.calculateVectors === "always");
|
|
6275
|
-
|
|
6276
|
-
const processor = this.
|
|
6365
|
+
if (this.processor) {
|
|
6366
|
+
const processor = this.processor;
|
|
6277
6367
|
if (processor && (processor?.config?.trigger === "onInsert" || processor?.config?.trigger === "onUpdate" || processor?.config?.trigger === "always")) {
|
|
6278
6368
|
const {
|
|
6279
6369
|
job: processorJob,
|
|
@@ -6282,8 +6372,7 @@ var ExuluContext = class {
|
|
|
6282
6372
|
"api",
|
|
6283
6373
|
{
|
|
6284
6374
|
...item,
|
|
6285
|
-
id: record.id
|
|
6286
|
-
field: key
|
|
6375
|
+
id: record.id
|
|
6287
6376
|
},
|
|
6288
6377
|
config,
|
|
6289
6378
|
user,
|
|
@@ -6292,8 +6381,13 @@ var ExuluContext = class {
|
|
|
6292
6381
|
if (processorJob) {
|
|
6293
6382
|
jobs.push(processorJob);
|
|
6294
6383
|
}
|
|
6295
|
-
if (!processorJob
|
|
6296
|
-
|
|
6384
|
+
if (!processorJob) {
|
|
6385
|
+
await db3.from(getTableName(this.id)).where({ id: record.id }).update({
|
|
6386
|
+
...processorResult
|
|
6387
|
+
});
|
|
6388
|
+
if (processor.config?.generateEmbeddings) {
|
|
6389
|
+
shouldGenerateEmbeddings = true;
|
|
6390
|
+
}
|
|
6297
6391
|
}
|
|
6298
6392
|
}
|
|
6299
6393
|
}
|
|
@@ -6317,23 +6411,23 @@ var ExuluContext = class {
|
|
|
6317
6411
|
};
|
|
6318
6412
|
deleteItem = async (item, user, role) => {
|
|
6319
6413
|
const { db: db3 } = await postgresClient();
|
|
6414
|
+
if (!item.id && !item.external_id) {
|
|
6415
|
+
throw new Error("Item id or external id is required for deleting an item.");
|
|
6416
|
+
}
|
|
6320
6417
|
if (!item.id?.length && item?.external_id) {
|
|
6321
6418
|
item = await db3.from(getTableName(this.id)).where({ external_id: item.external_id }).first();
|
|
6322
6419
|
if (!item || !item.id) {
|
|
6323
6420
|
throw new Error(`Item not found for external id ${item?.external_id || "undefined"}.`);
|
|
6324
6421
|
}
|
|
6325
6422
|
}
|
|
6326
|
-
|
|
6327
|
-
if (
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
}
|
|
6332
|
-
}
|
|
6333
|
-
const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
|
|
6334
|
-
if (chunks.length > 0) {
|
|
6335
|
-
await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
|
|
6423
|
+
const chunkTableExists = await this.chunksTableExists();
|
|
6424
|
+
if (chunkTableExists) {
|
|
6425
|
+
const chunks = await db3.from(getChunksTableName(this.id)).where({ source: item.id }).select("id");
|
|
6426
|
+
if (chunks.length > 0) {
|
|
6427
|
+
await db3.from(getChunksTableName(this.id)).where({ source: item.id }).delete();
|
|
6428
|
+
}
|
|
6336
6429
|
}
|
|
6430
|
+
await db3.from(getTableName(this.id)).where({ id: item.id }).delete();
|
|
6337
6431
|
return {
|
|
6338
6432
|
id: item.id,
|
|
6339
6433
|
job: void 0
|
|
@@ -6516,22 +6610,26 @@ var ExuluContext = class {
|
|
|
6516
6610
|
};
|
|
6517
6611
|
// Exports the context as a tool that can be used by an agent
|
|
6518
6612
|
tool = () => {
|
|
6613
|
+
if (this.configuration.enableAsTool === false) {
|
|
6614
|
+
return null;
|
|
6615
|
+
}
|
|
6519
6616
|
return new ExuluTool2({
|
|
6520
6617
|
id: this.id,
|
|
6521
|
-
name: `${this.name}`,
|
|
6618
|
+
name: `${this.name}_context_search`,
|
|
6522
6619
|
type: "context",
|
|
6523
6620
|
category: "contexts",
|
|
6524
6621
|
inputSchema: import_zod.z.object({
|
|
6525
|
-
|
|
6622
|
+
originalQuestion: import_zod.z.string().describe("The original question that the user asked"),
|
|
6623
|
+
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.")
|
|
6526
6624
|
}),
|
|
6527
6625
|
config: [],
|
|
6528
6626
|
description: `Gets information from the context called: ${this.name}. The context description is: ${this.description}.`,
|
|
6529
|
-
execute: async ({
|
|
6627
|
+
execute: async ({ originalQuestion, relevantKeywords, user, role }) => {
|
|
6530
6628
|
const { db: db3 } = await postgresClient();
|
|
6531
6629
|
const result = await vectorSearch({
|
|
6532
6630
|
page: 1,
|
|
6533
|
-
limit:
|
|
6534
|
-
query,
|
|
6631
|
+
limit: this.configuration.maxRetrievalResults ?? 10,
|
|
6632
|
+
query: originalQuestion,
|
|
6535
6633
|
filters: [],
|
|
6536
6634
|
user,
|
|
6537
6635
|
role,
|
|
@@ -6551,7 +6649,13 @@ var ExuluContext = class {
|
|
|
6551
6649
|
role: user?.role?.id
|
|
6552
6650
|
});
|
|
6553
6651
|
return {
|
|
6554
|
-
|
|
6652
|
+
result: JSON.stringify(result.chunks.map((chunk) => ({
|
|
6653
|
+
...chunk,
|
|
6654
|
+
context: {
|
|
6655
|
+
name: this.name,
|
|
6656
|
+
id: this.id
|
|
6657
|
+
}
|
|
6658
|
+
})))
|
|
6555
6659
|
};
|
|
6556
6660
|
}
|
|
6557
6661
|
});
|
|
@@ -7144,7 +7248,7 @@ Mood: friendly and intelligent.
|
|
|
7144
7248
|
});
|
|
7145
7249
|
});
|
|
7146
7250
|
if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
|
|
7147
|
-
await createUppyRoutes(app, config);
|
|
7251
|
+
await createUppyRoutes(app, contexts ?? [], config);
|
|
7148
7252
|
} else {
|
|
7149
7253
|
console.log("[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance.");
|
|
7150
7254
|
}
|
|
@@ -7418,7 +7522,7 @@ var import_ai3 = require("ai");
|
|
|
7418
7522
|
var import_crypto_js4 = __toESM(require("crypto-js"), 1);
|
|
7419
7523
|
|
|
7420
7524
|
// src/registry/log-metadata.ts
|
|
7421
|
-
function
|
|
7525
|
+
function logMetadata(id, additionalMetadata) {
|
|
7422
7526
|
return {
|
|
7423
7527
|
__logMetadata: true,
|
|
7424
7528
|
id,
|
|
@@ -7428,9 +7532,32 @@ function logMetadata2(id, additionalMetadata) {
|
|
|
7428
7532
|
|
|
7429
7533
|
// src/registry/workers.ts
|
|
7430
7534
|
var redisConnection;
|
|
7535
|
+
var unhandledRejectionHandlerInstalled = false;
|
|
7536
|
+
var installGlobalErrorHandlers = () => {
|
|
7537
|
+
if (unhandledRejectionHandlerInstalled) return;
|
|
7538
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
7539
|
+
console.error("[EXULU] Unhandled Promise Rejection detected! This would have crashed the worker.", {
|
|
7540
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
7541
|
+
stack: reason instanceof Error ? reason.stack : void 0
|
|
7542
|
+
});
|
|
7543
|
+
});
|
|
7544
|
+
process.on("uncaughtException", (error) => {
|
|
7545
|
+
console.error("[EXULU] Uncaught Exception detected! This would have crashed the worker.", {
|
|
7546
|
+
error: error.message,
|
|
7547
|
+
stack: error.stack
|
|
7548
|
+
});
|
|
7549
|
+
if (error.message.includes("FATAL") || error.message.includes("Cannot find module")) {
|
|
7550
|
+
console.error("[EXULU] Fatal error detected, exiting process.");
|
|
7551
|
+
process.exit(1);
|
|
7552
|
+
}
|
|
7553
|
+
});
|
|
7554
|
+
unhandledRejectionHandlerInstalled = true;
|
|
7555
|
+
console.log("[EXULU] Global error handlers installed to prevent worker crashes");
|
|
7556
|
+
};
|
|
7431
7557
|
var createWorkers = async (agents, queues2, config, contexts, evals, tools, tracer) => {
|
|
7432
7558
|
console.log("[EXULU] creating workers for " + queues2?.length + " queues.");
|
|
7433
7559
|
console.log("[EXULU] queues", queues2.map((q) => q.queue.name));
|
|
7560
|
+
installGlobalErrorHandlers();
|
|
7434
7561
|
if (!redisServer.host || !redisServer.port) {
|
|
7435
7562
|
console.error("[EXULU] you are trying to start worker, but no redis server is configured in the environment.");
|
|
7436
7563
|
throw new Error("No redis server configured in the environment, so cannot start worker.");
|
|
@@ -7455,8 +7582,9 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7455
7582
|
const worker = new import_bullmq4.Worker(
|
|
7456
7583
|
`${queue.queue.name}`,
|
|
7457
7584
|
async (bullmqJob) => {
|
|
7458
|
-
console.log("[EXULU] starting execution for job",
|
|
7585
|
+
console.log("[EXULU] starting execution for job", logMetadata(bullmqJob.name, {
|
|
7459
7586
|
name: bullmqJob.name,
|
|
7587
|
+
jobId: bullmqJob.id,
|
|
7460
7588
|
status: await bullmqJob.getState(),
|
|
7461
7589
|
type: bullmqJob.data.type
|
|
7462
7590
|
}));
|
|
@@ -7464,16 +7592,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7464
7592
|
const data = bullmqJob.data;
|
|
7465
7593
|
const timeoutInSeconds = data.timeoutInSeconds || 600;
|
|
7466
7594
|
const timeoutMs = timeoutInSeconds * 1e3;
|
|
7595
|
+
let timeoutHandle;
|
|
7467
7596
|
const timeoutPromise = new Promise((_, reject) => {
|
|
7468
|
-
setTimeout(() => {
|
|
7469
|
-
|
|
7597
|
+
timeoutHandle = setTimeout(() => {
|
|
7598
|
+
const timeoutError = new Error(`Timeout for job ${bullmqJob.id} reached after ${timeoutInSeconds}s`);
|
|
7599
|
+
console.error(`[EXULU] ${timeoutError.message}`);
|
|
7600
|
+
reject(timeoutError);
|
|
7470
7601
|
}, timeoutMs);
|
|
7471
7602
|
});
|
|
7472
7603
|
const workPromise = (async () => {
|
|
7473
7604
|
try {
|
|
7605
|
+
console.log(`[EXULU] Job ${bullmqJob.id} - Log file: logs/jobs/job-${bullmqJob.id}.log`);
|
|
7474
7606
|
bullmq.validate(bullmqJob.id, data);
|
|
7475
7607
|
if (data.type === "embedder") {
|
|
7476
|
-
console.log("[EXULU] running an embedder job.",
|
|
7608
|
+
console.log("[EXULU] running an embedder job.", logMetadata(bullmqJob.name));
|
|
7477
7609
|
const label = `embedder-${bullmqJob.name}`;
|
|
7478
7610
|
await db3.from("job_results").insert({
|
|
7479
7611
|
job_id: bullmqJob.id,
|
|
@@ -7503,7 +7635,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7503
7635
|
};
|
|
7504
7636
|
}
|
|
7505
7637
|
if (data.type === "processor") {
|
|
7506
|
-
console.log("[EXULU] running a processor job.",
|
|
7638
|
+
console.log("[EXULU] running a processor job, job name: ", bullmqJob.name, " job id: ", bullmqJob.id, " job data: ", data, " job queue: ", bullmqJob.queueName);
|
|
7507
7639
|
const label = `processor-${bullmqJob.name}`;
|
|
7508
7640
|
await db3.from("job_results").insert({
|
|
7509
7641
|
job_id: bullmqJob.id,
|
|
@@ -7516,44 +7648,46 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7516
7648
|
if (!context) {
|
|
7517
7649
|
throw new Error(`Context ${data.context} not found in the registry.`);
|
|
7518
7650
|
}
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
});
|
|
7522
|
-
if (!field) {
|
|
7523
|
-
throw new Error(`Field ${data.inputs.field} not found in the context ${data.context}.`);
|
|
7651
|
+
if (!data.inputs.id) {
|
|
7652
|
+
throw new Error(`[EXULU] Item not set for processor in context ${context.id}, running in job ${bullmqJob.id}.`);
|
|
7524
7653
|
}
|
|
7525
|
-
if (!
|
|
7526
|
-
throw new Error(`
|
|
7654
|
+
if (!context.processor) {
|
|
7655
|
+
throw new Error(`Tried to run a processor job for context ${context.id}, but no processor is set.`);
|
|
7527
7656
|
}
|
|
7528
7657
|
const exuluStorage = new ExuluStorage({ config });
|
|
7529
|
-
if (!data.user) {
|
|
7530
|
-
throw new Error(`User not set for processor job.`);
|
|
7531
|
-
}
|
|
7532
|
-
if (!data.role) {
|
|
7533
|
-
throw new Error(`Role not set for processor job.`);
|
|
7534
|
-
}
|
|
7535
7658
|
console.log("[EXULU] POS 2 -- EXULU CONTEXT PROCESS FIELD");
|
|
7536
|
-
const
|
|
7659
|
+
const processorResult = await context.processor.execute({
|
|
7537
7660
|
item: data.inputs,
|
|
7538
7661
|
user: data.user,
|
|
7539
7662
|
role: data.role,
|
|
7540
7663
|
utils: {
|
|
7541
|
-
storage: exuluStorage
|
|
7542
|
-
items: {
|
|
7543
|
-
update: context.updateItem,
|
|
7544
|
-
create: context.createItem,
|
|
7545
|
-
delete: context.deleteItem
|
|
7546
|
-
}
|
|
7664
|
+
storage: exuluStorage
|
|
7547
7665
|
},
|
|
7548
|
-
config
|
|
7666
|
+
exuluConfig: config
|
|
7667
|
+
});
|
|
7668
|
+
if (!processorResult) {
|
|
7669
|
+
throw new Error(`[EXULU] Processor in context ${context.id}, running in job ${bullmqJob.id} did not return an item.`);
|
|
7670
|
+
}
|
|
7671
|
+
delete processorResult.field;
|
|
7672
|
+
await db3.from(getTableName(context.id)).where({
|
|
7673
|
+
id: processorResult.id
|
|
7674
|
+
}).update({
|
|
7675
|
+
...processorResult,
|
|
7676
|
+
last_processed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
7549
7677
|
});
|
|
7550
7678
|
let jobs = [];
|
|
7551
|
-
if (
|
|
7679
|
+
if (context.processor?.config?.generateEmbeddings) {
|
|
7680
|
+
const fullItem = await db3.from(getTableName(context.id)).where({
|
|
7681
|
+
id: processorResult.id
|
|
7682
|
+
}).first();
|
|
7683
|
+
if (!fullItem) {
|
|
7684
|
+
throw new Error(`[EXULU] Item ${processorResult.id} not found after processor update in context ${context.id}`);
|
|
7685
|
+
}
|
|
7552
7686
|
const { job: embeddingsJob } = await context.embeddings.generate.one({
|
|
7553
|
-
item:
|
|
7687
|
+
item: fullItem,
|
|
7554
7688
|
user: data.user,
|
|
7555
7689
|
role: data.role,
|
|
7556
|
-
trigger: "
|
|
7690
|
+
trigger: "processor",
|
|
7557
7691
|
config
|
|
7558
7692
|
});
|
|
7559
7693
|
if (embeddingsJob) {
|
|
@@ -7561,14 +7695,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7561
7695
|
}
|
|
7562
7696
|
}
|
|
7563
7697
|
return {
|
|
7564
|
-
result,
|
|
7698
|
+
result: processorResult,
|
|
7565
7699
|
metadata: {
|
|
7566
7700
|
jobs: jobs.length > 0 ? jobs.join(",") : void 0
|
|
7567
7701
|
}
|
|
7568
7702
|
};
|
|
7569
7703
|
}
|
|
7570
7704
|
if (data.type === "eval_run") {
|
|
7571
|
-
console.log("[EXULU] running an eval run job.",
|
|
7705
|
+
console.log("[EXULU] running an eval run job.", logMetadata(bullmqJob.name));
|
|
7572
7706
|
const label = `eval-run-${data.eval_run_id}-${data.test_case_id}`;
|
|
7573
7707
|
const existingResult = await db3.from("job_results").where({ label }).first();
|
|
7574
7708
|
if (existingResult) {
|
|
@@ -7617,7 +7751,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7617
7751
|
resolve(messages2);
|
|
7618
7752
|
break;
|
|
7619
7753
|
} catch (error) {
|
|
7620
|
-
console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`,
|
|
7754
|
+
console.error(`[EXULU] error processing UI messages flow for agent ${agentInstance.name} (${agentInstance.id}).`, logMetadata(bullmqJob.name, {
|
|
7621
7755
|
error: error instanceof Error ? error.message : String(error)
|
|
7622
7756
|
}));
|
|
7623
7757
|
attempts++;
|
|
@@ -7678,7 +7812,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7678
7812
|
eval_function_config: evalFunction.config || {},
|
|
7679
7813
|
result: result2 || 0
|
|
7680
7814
|
};
|
|
7681
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`,
|
|
7815
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
|
|
7682
7816
|
result: result2 || 0
|
|
7683
7817
|
}));
|
|
7684
7818
|
evalFunctionResults.push(evalFunctionResult);
|
|
@@ -7697,7 +7831,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7697
7831
|
result: result2 || 0
|
|
7698
7832
|
};
|
|
7699
7833
|
evalFunctionResults.push(evalFunctionResult);
|
|
7700
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`,
|
|
7834
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result2}`, logMetadata(bullmqJob.name, {
|
|
7701
7835
|
result: result2 || 0
|
|
7702
7836
|
}));
|
|
7703
7837
|
}
|
|
@@ -7734,7 +7868,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7734
7868
|
};
|
|
7735
7869
|
}
|
|
7736
7870
|
if (data.type === "eval_function") {
|
|
7737
|
-
console.log("[EXULU] running an eval function job.",
|
|
7871
|
+
console.log("[EXULU] running an eval function job.", logMetadata(bullmqJob.name));
|
|
7738
7872
|
if (data.eval_functions?.length !== 1) {
|
|
7739
7873
|
throw new Error(`Expected 1 eval function for eval function job, got ${data.eval_functions?.length}.`);
|
|
7740
7874
|
}
|
|
@@ -7780,7 +7914,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7780
7914
|
inputMessages,
|
|
7781
7915
|
evalFunction.config || {}
|
|
7782
7916
|
);
|
|
7783
|
-
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`,
|
|
7917
|
+
console.log(`[EXULU] eval function ${evalFunction.id} result: ${result}`, logMetadata(bullmqJob.name, {
|
|
7784
7918
|
result: result || 0
|
|
7785
7919
|
}));
|
|
7786
7920
|
}
|
|
@@ -7790,7 +7924,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7790
7924
|
};
|
|
7791
7925
|
}
|
|
7792
7926
|
if (data.type === "source") {
|
|
7793
|
-
console.log("[EXULU] running a source job.",
|
|
7927
|
+
console.log("[EXULU] running a source job.", logMetadata(bullmqJob.name));
|
|
7794
7928
|
if (!data.source) {
|
|
7795
7929
|
throw new Error(`No source id set for source job.`);
|
|
7796
7930
|
}
|
|
@@ -7818,14 +7952,14 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7818
7952
|
);
|
|
7819
7953
|
if (job) {
|
|
7820
7954
|
jobs.push(job);
|
|
7821
|
-
console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`,
|
|
7955
|
+
console.log(`[EXULU] Scheduled job through source update job for item ${createdItem.id} (Job ID: ${job})`, logMetadata(bullmqJob.name, {
|
|
7822
7956
|
item: createdItem,
|
|
7823
7957
|
job
|
|
7824
7958
|
}));
|
|
7825
7959
|
}
|
|
7826
7960
|
if (createdItem.id) {
|
|
7827
7961
|
items.push(createdItem.id);
|
|
7828
|
-
console.log(`[EXULU] created item through source update job ${createdItem.id}`,
|
|
7962
|
+
console.log(`[EXULU] created item through source update job ${createdItem.id}`, logMetadata(bullmqJob.name, {
|
|
7829
7963
|
item: createdItem
|
|
7830
7964
|
}));
|
|
7831
7965
|
}
|
|
@@ -7853,11 +7987,20 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7853
7987
|
throw error;
|
|
7854
7988
|
}
|
|
7855
7989
|
})();
|
|
7856
|
-
|
|
7990
|
+
try {
|
|
7991
|
+
const result = await Promise.race([workPromise, timeoutPromise]);
|
|
7992
|
+
clearTimeout(timeoutHandle);
|
|
7993
|
+
return result;
|
|
7994
|
+
} catch (error) {
|
|
7995
|
+
clearTimeout(timeoutHandle);
|
|
7996
|
+
console.error(`[EXULU] job ${bullmqJob.id} failed (error caught in race handler).`, error instanceof Error ? error.message : String(error));
|
|
7997
|
+
throw error;
|
|
7998
|
+
}
|
|
7857
7999
|
},
|
|
7858
8000
|
{
|
|
7859
8001
|
autorun: true,
|
|
7860
8002
|
connection: redisConnection,
|
|
8003
|
+
concurrency: queue.concurrency?.worker || 1,
|
|
7861
8004
|
removeOnComplete: { count: 1e3 },
|
|
7862
8005
|
removeOnFail: { count: 5e3 },
|
|
7863
8006
|
...queue.ratelimit && {
|
|
@@ -7887,7 +8030,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7887
8030
|
});
|
|
7888
8031
|
return;
|
|
7889
8032
|
}
|
|
7890
|
-
console.error(`[EXULU] job failed.`, job?.name ?
|
|
8033
|
+
console.error(`[EXULU] job failed.`, job?.name ? logMetadata(job.name, {
|
|
7891
8034
|
error: error instanceof Error ? error.message : String(error)
|
|
7892
8035
|
}) : error);
|
|
7893
8036
|
});
|
|
@@ -7895,7 +8038,7 @@ var createWorkers = async (agents, queues2, config, contexts, evals, tools, trac
|
|
|
7895
8038
|
console.error(`[EXULU] worker error.`, error);
|
|
7896
8039
|
});
|
|
7897
8040
|
worker.on("progress", (job, progress) => {
|
|
7898
|
-
console.log(`[EXULU] job progress ${job.id}.`,
|
|
8041
|
+
console.log(`[EXULU] job progress ${job.id}.`, logMetadata(job.name, {
|
|
7899
8042
|
progress
|
|
7900
8043
|
}));
|
|
7901
8044
|
});
|
|
@@ -9061,18 +9204,24 @@ var ExuluQueues = class {
|
|
|
9061
9204
|
// method of ExuluQueues we need to store the desired rate limit on the queue
|
|
9062
9205
|
// here so we can use the value when creating workers for the queue instance
|
|
9063
9206
|
// as there is no way to store a rate limit value natively on a bullm queue.
|
|
9064
|
-
register = (name, concurrency = 1,
|
|
9207
|
+
register = (name, concurrency, ratelimit = 1, timeoutInSeconds = 180) => {
|
|
9208
|
+
const queueConcurrency = concurrency.queue || 1;
|
|
9209
|
+
const workerConcurrency = concurrency.worker || 1;
|
|
9065
9210
|
const use = async () => {
|
|
9066
9211
|
const existing = this.queues.find((x) => x.queue?.name === name);
|
|
9067
9212
|
if (existing) {
|
|
9068
9213
|
const globalConcurrency = await existing.queue.getGlobalConcurrency();
|
|
9069
|
-
if (globalConcurrency !==
|
|
9070
|
-
await existing.queue.setGlobalConcurrency(
|
|
9214
|
+
if (globalConcurrency !== queueConcurrency) {
|
|
9215
|
+
await existing.queue.setGlobalConcurrency(queueConcurrency);
|
|
9071
9216
|
}
|
|
9072
9217
|
return {
|
|
9073
9218
|
queue: existing.queue,
|
|
9074
9219
|
ratelimit,
|
|
9075
|
-
concurrency
|
|
9220
|
+
concurrency: {
|
|
9221
|
+
worker: workerConcurrency,
|
|
9222
|
+
queue: queueConcurrency
|
|
9223
|
+
},
|
|
9224
|
+
timeoutInSeconds
|
|
9076
9225
|
};
|
|
9077
9226
|
}
|
|
9078
9227
|
if (!redisServer.host?.length || !redisServer.port?.length) {
|
|
@@ -9091,22 +9240,34 @@ var ExuluQueues = class {
|
|
|
9091
9240
|
telemetry: new import_bullmq_otel.BullMQOtel("simple-guide")
|
|
9092
9241
|
}
|
|
9093
9242
|
);
|
|
9094
|
-
await newQueue.setGlobalConcurrency(
|
|
9243
|
+
await newQueue.setGlobalConcurrency(queueConcurrency);
|
|
9095
9244
|
this.queues.push({
|
|
9096
9245
|
queue: newQueue,
|
|
9097
9246
|
ratelimit,
|
|
9098
|
-
concurrency
|
|
9247
|
+
concurrency: {
|
|
9248
|
+
worker: workerConcurrency,
|
|
9249
|
+
queue: queueConcurrency
|
|
9250
|
+
},
|
|
9251
|
+
timeoutInSeconds
|
|
9099
9252
|
});
|
|
9100
9253
|
return {
|
|
9101
9254
|
queue: newQueue,
|
|
9102
9255
|
ratelimit,
|
|
9103
|
-
concurrency
|
|
9256
|
+
concurrency: {
|
|
9257
|
+
worker: workerConcurrency,
|
|
9258
|
+
queue: queueConcurrency
|
|
9259
|
+
},
|
|
9260
|
+
timeoutInSeconds
|
|
9104
9261
|
};
|
|
9105
9262
|
};
|
|
9106
9263
|
this.list.set(name, {
|
|
9107
9264
|
name,
|
|
9108
|
-
concurrency
|
|
9265
|
+
concurrency: {
|
|
9266
|
+
worker: workerConcurrency,
|
|
9267
|
+
queue: queueConcurrency
|
|
9268
|
+
},
|
|
9109
9269
|
ratelimit,
|
|
9270
|
+
timeoutInSeconds,
|
|
9110
9271
|
use
|
|
9111
9272
|
});
|
|
9112
9273
|
return {
|
|
@@ -9161,7 +9322,10 @@ var llmAsJudgeEval = () => {
|
|
|
9161
9322
|
name: "prompt",
|
|
9162
9323
|
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."
|
|
9163
9324
|
}],
|
|
9164
|
-
queue: queues.register("llm_as_judge",
|
|
9325
|
+
queue: queues.register("llm_as_judge", {
|
|
9326
|
+
worker: 1,
|
|
9327
|
+
queue: 1
|
|
9328
|
+
}, 1).use(),
|
|
9165
9329
|
llm: true
|
|
9166
9330
|
});
|
|
9167
9331
|
}
|
|
@@ -9796,11 +9960,16 @@ var previewPdfTool = new ExuluTool2({
|
|
|
9796
9960
|
type: "function",
|
|
9797
9961
|
config: [],
|
|
9798
9962
|
inputSchema: import_zod5.z.object({
|
|
9799
|
-
s3key: import_zod5.z.string().describe("The S3 key of the PDF file to preview
|
|
9963
|
+
s3key: import_zod5.z.string().describe("The S3 key of the PDF file to preview."),
|
|
9800
9964
|
page: import_zod5.z.number().describe("The page number to preview, defaults to 1.").optional()
|
|
9801
9965
|
}),
|
|
9802
9966
|
execute: async ({ s3key, page, exuluConfig }) => {
|
|
9803
|
-
const
|
|
9967
|
+
const bucket = s3key.split("/")[0];
|
|
9968
|
+
const key = s3key.split("/").slice(1).join("/");
|
|
9969
|
+
if (!bucket || !key) {
|
|
9970
|
+
throw new Error("Invalid S3 key, must be in the format of <bucket>/<key>.");
|
|
9971
|
+
}
|
|
9972
|
+
const url = await getPresignedUrl(bucket, key, exuluConfig);
|
|
9804
9973
|
if (!url) {
|
|
9805
9974
|
throw new Error("No URL provided for PDF preview");
|
|
9806
9975
|
}
|
|
@@ -10164,7 +10333,7 @@ var ExuluApp = class {
|
|
|
10164
10333
|
...[previewPdfTool],
|
|
10165
10334
|
...todoTools,
|
|
10166
10335
|
// Add contexts as tools
|
|
10167
|
-
...Object.values(contexts || {}).map((context) => context.tool())
|
|
10336
|
+
...Object.values(contexts || {}).map((context) => context.tool()).filter(Boolean)
|
|
10168
10337
|
// Because agents are stored in the database, we add those as tools
|
|
10169
10338
|
// at request time, not during ExuluApp initialization. We add them
|
|
10170
10339
|
// in the grahql tools resolver.
|
|
@@ -10194,7 +10363,10 @@ var ExuluApp = class {
|
|
|
10194
10363
|
}
|
|
10195
10364
|
const queueSet = /* @__PURE__ */ new Set();
|
|
10196
10365
|
if (redisServer.host?.length && redisServer.port?.length) {
|
|
10197
|
-
queues.register(global_queues.eval_runs,
|
|
10366
|
+
queues.register(global_queues.eval_runs, {
|
|
10367
|
+
worker: 1,
|
|
10368
|
+
queue: 1
|
|
10369
|
+
}, 1);
|
|
10198
10370
|
for (const queue of queues.list.values()) {
|
|
10199
10371
|
const config2 = await queue.use();
|
|
10200
10372
|
queueSet.add(config2);
|
|
@@ -10323,10 +10495,7 @@ var ExuluApp = class {
|
|
|
10323
10495
|
console.warn("[EXULU] No queue configured for source", source.name);
|
|
10324
10496
|
continue;
|
|
10325
10497
|
}
|
|
10326
|
-
if (queue) {
|
|
10327
|
-
if (!source.config?.schedule) {
|
|
10328
|
-
throw new Error("Schedule is required for source when configuring a queue: " + source.name);
|
|
10329
|
-
}
|
|
10498
|
+
if (queue && source.config?.schedule) {
|
|
10330
10499
|
console.log("[EXULU] Creating ContextSource scheduler for", source.name, "in queue", queue.queue?.name);
|
|
10331
10500
|
await queue.queue?.upsertJobScheduler(source.id, {
|
|
10332
10501
|
pattern: source.config?.schedule
|