@exulu/backend 1.58.0 → 1.60.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/dist/catalog-EOKGOHTY.js +10 -0
- package/dist/{chunk-RVLZ5EL3.js → chunk-23YNGK3V.js} +645 -66
- package/dist/chunk-YS27XOXI.js +62 -0
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-K4W6OJ3G.js → convert-exulu-tools-to-ai-sdk-tools-PLLM2CJL.js} +1 -1
- package/dist/index.cjs +2826 -1239
- package/dist/index.d.cts +13 -14
- package/dist/index.d.ts +13 -14
- package/dist/index.js +2031 -1141
- package/ee/python/.litellm/config.yaml.example +64 -0
- package/ee/python/requirements.txt +15 -0
- package/ee/python/setup.sh +13 -0
- package/ee/workers.ts +15 -29
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -2,18 +2,22 @@ import {
|
|
|
2
2
|
ExuluContext,
|
|
3
3
|
ExuluStorage,
|
|
4
4
|
ExuluTool,
|
|
5
|
+
ResolveModelError,
|
|
5
6
|
STATISTICS_TYPE_ENUM,
|
|
6
7
|
applyAccessControl,
|
|
7
8
|
applyFilters,
|
|
8
9
|
applySorting,
|
|
9
10
|
authentication,
|
|
11
|
+
buildTags,
|
|
10
12
|
checkLicense,
|
|
13
|
+
checkRecordAccess,
|
|
11
14
|
convertContextToTableDefinition,
|
|
12
15
|
convertExuluToolsToAiSdkTools,
|
|
13
16
|
copyS3Object,
|
|
14
17
|
coreSchemas,
|
|
15
18
|
createAgenticRetrievalToolV3,
|
|
16
19
|
createProjectItemsRetrievalTool,
|
|
20
|
+
createTaggedFetch,
|
|
17
21
|
createUppyRoutes,
|
|
18
22
|
deleteS3Object,
|
|
19
23
|
downloadKeyIntoSandbox,
|
|
@@ -26,18 +30,26 @@ import {
|
|
|
26
30
|
getS3SignedUploadUrl,
|
|
27
31
|
getTableName,
|
|
28
32
|
getToken,
|
|
33
|
+
isLiteLLMEnabled,
|
|
29
34
|
listS3ObjectsByPrefix,
|
|
30
35
|
mapType,
|
|
31
36
|
postgresClient,
|
|
32
37
|
reportSystemDependencies,
|
|
38
|
+
resolveModel,
|
|
33
39
|
sanitizeName,
|
|
34
40
|
sanitizeToolName,
|
|
41
|
+
setLiteLLMPackageRoot,
|
|
42
|
+
startLiteLLMSupervisor,
|
|
35
43
|
trajectoryRegistry,
|
|
36
44
|
updateStatistic,
|
|
37
45
|
uploadFile,
|
|
38
46
|
vectorSearch,
|
|
47
|
+
waitForLiteLLMReady,
|
|
39
48
|
withRetry
|
|
40
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-23YNGK3V.js";
|
|
50
|
+
import {
|
|
51
|
+
findLiteLLMModel
|
|
52
|
+
} from "./chunk-YS27XOXI.js";
|
|
41
53
|
|
|
42
54
|
// src/index.ts
|
|
43
55
|
import "dotenv/config";
|
|
@@ -267,71 +279,15 @@ import { makeExecutableSchema } from "@graphql-tools/schema";
|
|
|
267
279
|
import GraphQLJSON from "graphql-type-json";
|
|
268
280
|
import cron from "cron-validator";
|
|
269
281
|
|
|
270
|
-
// src/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
};
|
|
280
|
-
const cachedAccess = checkRecordAccessCache.get(`${record.id}-${request}-${user?.id}`);
|
|
281
|
-
if (cachedAccess && cachedAccess.expiresAt > /* @__PURE__ */ new Date()) {
|
|
282
|
-
return cachedAccess.hasAccess;
|
|
283
|
-
}
|
|
284
|
-
const isPublic = record.rights_mode === "public";
|
|
285
|
-
const byUsers = record.rights_mode === "users";
|
|
286
|
-
const byRoles = record.rights_mode === "roles";
|
|
287
|
-
const createdBy = typeof record.created_by === "string" ? record.created_by : record.created_by?.toString();
|
|
288
|
-
const isCreator = user ? createdBy === user.id.toString() : false;
|
|
289
|
-
const isAdmin = user ? user.super_admin : false;
|
|
290
|
-
const isApi = user ? user.type === "api" : false;
|
|
291
|
-
const isAdminApi = isApi && (!user.scope_mode || user.scope_mode === "admin");
|
|
292
|
-
const isAgentsScopedApi = isApi && user.scope_mode === "agents" && request === "read" && Array.isArray(user.agent_ids) && user.agent_ids.includes(String(record.id));
|
|
293
|
-
let hasAccess = "none";
|
|
294
|
-
if (isPublic || isCreator || isAdmin || isAdminApi || isAgentsScopedApi) {
|
|
295
|
-
setRecordAccessCache(true);
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
if (byUsers) {
|
|
299
|
-
if (!user) {
|
|
300
|
-
setRecordAccessCache(false);
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
hasAccess = record.RBAC?.users?.find((x) => x.id === user.id)?.rights || "none";
|
|
304
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
305
|
-
console.error(
|
|
306
|
-
`[EXULU] Your current user ${user.id} does not have access to this record, current access type is: ${hasAccess}.`
|
|
307
|
-
);
|
|
308
|
-
setRecordAccessCache(false);
|
|
309
|
-
return false;
|
|
310
|
-
} else {
|
|
311
|
-
setRecordAccessCache(true);
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
if (byRoles) {
|
|
316
|
-
if (!user) {
|
|
317
|
-
setRecordAccessCache(false);
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
hasAccess = record.RBAC?.roles?.find((x) => x.id === user.role?.id)?.rights || "none";
|
|
321
|
-
if (!hasAccess || hasAccess === "none" || hasAccess !== request) {
|
|
322
|
-
console.error(
|
|
323
|
-
`[EXULU] Your current role ${user.role?.name} does not have access to this record, current access type is: ${hasAccess}.`
|
|
324
|
-
);
|
|
325
|
-
setRecordAccessCache(false);
|
|
326
|
-
return false;
|
|
327
|
-
} else {
|
|
328
|
-
setRecordAccessCache(true);
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
setRecordAccessCache(false);
|
|
333
|
-
return false;
|
|
334
|
-
};
|
|
282
|
+
// src/exulu/resolve-agent-provider.ts
|
|
283
|
+
async function resolveAgentProvider(agent, providers) {
|
|
284
|
+
if (isLiteLLMEnabled()) return void 0;
|
|
285
|
+
if (!agent.model) return void 0;
|
|
286
|
+
const { db } = await postgresClient();
|
|
287
|
+
const modelRow = await db.from("models").where({ id: agent.model }).first();
|
|
288
|
+
if (!modelRow?.provider) return void 0;
|
|
289
|
+
return providers.find((p) => p.id === modelRow.provider);
|
|
290
|
+
}
|
|
335
291
|
|
|
336
292
|
// ee/queues/queues.ts
|
|
337
293
|
import { Queue } from "bullmq";
|
|
@@ -577,15 +533,26 @@ var exuluProviderFields = [
|
|
|
577
533
|
|
|
578
534
|
// src/graphql/utilities/sanitize-and-hydrate-fields.ts
|
|
579
535
|
var addProviderFields = async (args, requestedFields, providers, result, tools, user, contexts, rerankers) => {
|
|
580
|
-
let provider
|
|
536
|
+
let provider;
|
|
537
|
+
let modelRow;
|
|
538
|
+
let litellmEntry;
|
|
539
|
+
if (isLiteLLMEnabled() && result?.model) {
|
|
540
|
+
litellmEntry = await findLiteLLMModel(result.model);
|
|
541
|
+
} else if (result?.model) {
|
|
542
|
+
const { db } = await postgresClient();
|
|
543
|
+
modelRow = await db.from("models").where({ id: result.model }).first();
|
|
544
|
+
if (modelRow?.provider) {
|
|
545
|
+
provider = providers.find((a) => a.id === modelRow.provider);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
581
548
|
if (requestedFields.includes("providerName")) {
|
|
582
|
-
result.providerName = provider?.providerName || "";
|
|
549
|
+
result.providerName = isLiteLLMEnabled() ? "LiteLLM" : provider?.providerName || "";
|
|
583
550
|
}
|
|
584
551
|
if (requestedFields.includes("modelName")) {
|
|
585
|
-
result.modelName = provider?.modelName || "";
|
|
552
|
+
result.modelName = isLiteLLMEnabled() ? result?.model ?? "" : modelRow?.name || provider?.modelName || "";
|
|
586
553
|
}
|
|
587
554
|
if (requestedFields.includes("slug")) {
|
|
588
|
-
result.slug = provider?.slug || "";
|
|
555
|
+
result.slug = isLiteLLMEnabled() ? "/agents/litellm/run" : provider?.slug || "";
|
|
589
556
|
}
|
|
590
557
|
if (requestedFields.includes("rateLimit")) {
|
|
591
558
|
result.rateLimit = provider?.rateLimit || "";
|
|
@@ -594,9 +561,9 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
594
561
|
if (result.tools) {
|
|
595
562
|
result.tools = await Promise.all(
|
|
596
563
|
result.tools.map(
|
|
597
|
-
async (
|
|
564
|
+
async (tool2) => {
|
|
598
565
|
let hydrated;
|
|
599
|
-
if (
|
|
566
|
+
if (tool2.id === "agentic_context_search") {
|
|
600
567
|
const instance = createAgenticRetrievalToolV3({
|
|
601
568
|
contexts: [],
|
|
602
569
|
rerankers: [],
|
|
@@ -612,38 +579,60 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
612
579
|
name: instance.name,
|
|
613
580
|
description: instance.description,
|
|
614
581
|
category: instance.category,
|
|
615
|
-
config:
|
|
582
|
+
config: tool2.config
|
|
616
583
|
};
|
|
617
584
|
}
|
|
618
|
-
if (
|
|
619
|
-
if (
|
|
585
|
+
if (tool2.type === "agent") {
|
|
586
|
+
if (tool2.id === result.id) {
|
|
620
587
|
return null;
|
|
621
588
|
}
|
|
622
|
-
const instance = await exuluApp.get().agent(
|
|
589
|
+
const instance = await exuluApp.get().agent(tool2.id);
|
|
623
590
|
if (!instance) {
|
|
624
591
|
throw new Error(
|
|
625
|
-
"Trying to load a tool of type 'agent', but the associated agent with id " +
|
|
592
|
+
"Trying to load a tool of type 'agent', but the associated agent with id " + tool2.id + " was not found in the database."
|
|
626
593
|
);
|
|
627
594
|
}
|
|
628
|
-
|
|
629
|
-
if (!provider2) {
|
|
595
|
+
if (!instance.model) {
|
|
630
596
|
throw new Error(
|
|
631
|
-
"Trying to load a tool of type 'agent', but the associated agent with id " +
|
|
597
|
+
"Trying to load a tool of type 'agent', but the associated agent with id " + tool2.id + " does not have a model set for it."
|
|
632
598
|
);
|
|
633
599
|
}
|
|
634
600
|
const hasAccessToAgent = await checkRecordAccess(instance, "read", user);
|
|
635
601
|
if (!hasAccessToAgent) {
|
|
636
602
|
return null;
|
|
637
603
|
}
|
|
638
|
-
|
|
604
|
+
if (isLiteLLMEnabled()) {
|
|
605
|
+
hydrated = {
|
|
606
|
+
id: instance.id,
|
|
607
|
+
name: instance.name,
|
|
608
|
+
description: `This tool calls an agent named: ${instance.name}. The agent does the following: ${instance.description ?? ""}.`,
|
|
609
|
+
type: "agent",
|
|
610
|
+
category: "agents"
|
|
611
|
+
};
|
|
612
|
+
} else {
|
|
613
|
+
const { db } = await postgresClient();
|
|
614
|
+
const innerModelRow = await db.from("models").where({ id: instance.model }).first();
|
|
615
|
+
const provider2 = innerModelRow?.provider ? providers.find((a) => a.id === innerModelRow.provider) : void 0;
|
|
616
|
+
if (!provider2) {
|
|
617
|
+
throw new Error(
|
|
618
|
+
"Trying to load a tool of type 'agent', but the model referenced by agent with id " + tool2.id + " does not point at a registered ExuluProvider."
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
hydrated = await provider2.tool(
|
|
622
|
+
instance.id,
|
|
623
|
+
providers,
|
|
624
|
+
contexts,
|
|
625
|
+
rerankers
|
|
626
|
+
);
|
|
627
|
+
}
|
|
639
628
|
} else {
|
|
640
|
-
hydrated = tools.find((t) => t.id ===
|
|
629
|
+
hydrated = tools.find((t) => t.id === tool2.id);
|
|
641
630
|
}
|
|
642
631
|
const hydratedTool = {
|
|
643
|
-
...
|
|
632
|
+
...tool2,
|
|
644
633
|
name: hydrated?.name || "",
|
|
645
634
|
description: hydrated?.description || "",
|
|
646
|
-
category:
|
|
635
|
+
category: tool2?.category || "default"
|
|
647
636
|
};
|
|
648
637
|
console.log("[EXULU] hydratedTool", hydratedTool);
|
|
649
638
|
return hydratedTool;
|
|
@@ -661,28 +650,42 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
661
650
|
result.tools.unshift(projectTool);
|
|
662
651
|
}
|
|
663
652
|
}
|
|
664
|
-
result.tools = result.tools.filter((
|
|
653
|
+
result.tools = result.tools.filter((tool2) => tool2 !== null);
|
|
665
654
|
} else {
|
|
666
655
|
result.tools = [];
|
|
667
656
|
}
|
|
668
657
|
}
|
|
669
658
|
if (requestedFields.includes("streaming")) {
|
|
670
|
-
result.streaming = provider?.streaming || false;
|
|
659
|
+
result.streaming = isLiteLLMEnabled() ? true : provider?.streaming || false;
|
|
671
660
|
}
|
|
672
661
|
if (requestedFields.includes("capabilities")) {
|
|
673
|
-
|
|
662
|
+
if (isLiteLLMEnabled()) {
|
|
663
|
+
result.capabilities = {
|
|
664
|
+
text: true,
|
|
665
|
+
images: litellmEntry?.supports_vision ? [".png", ".jpg", ".jpeg", ".webp", ".gif"] : [],
|
|
666
|
+
files: litellmEntry?.supports_pdf_input ? [".pdf"] : [],
|
|
667
|
+
audio: litellmEntry?.supports_audio_input ? [".mp3", ".wav", ".m4a"] : [],
|
|
668
|
+
video: []
|
|
669
|
+
};
|
|
670
|
+
} else {
|
|
671
|
+
result.capabilities = provider?.capabilities || [];
|
|
672
|
+
}
|
|
674
673
|
}
|
|
675
674
|
if (requestedFields.includes("maxContextLength")) {
|
|
676
|
-
|
|
675
|
+
if (isLiteLLMEnabled()) {
|
|
676
|
+
result.maxContextLength = litellmEntry?.max_input_tokens ?? litellmEntry?.max_tokens ?? 0;
|
|
677
|
+
} else {
|
|
678
|
+
result.maxContextLength = provider?.maxContextLength || 0;
|
|
679
|
+
}
|
|
677
680
|
}
|
|
678
681
|
if (requestedFields.includes("authenticationInformation")) {
|
|
679
|
-
result.authenticationInformation = provider?.authenticationInformation || "";
|
|
682
|
+
result.authenticationInformation = isLiteLLMEnabled() ? "" : provider?.authenticationInformation || "";
|
|
680
683
|
}
|
|
681
684
|
if (requestedFields.includes("provider")) {
|
|
682
|
-
result.provider = provider?.provider || "";
|
|
685
|
+
result.provider = isLiteLLMEnabled() ? "litellm" : provider?.provider || "";
|
|
683
686
|
}
|
|
684
687
|
if (requestedFields.includes("systemInstructions")) {
|
|
685
|
-
result.systemInstructions = provider?.config?.instructions || void 0;
|
|
688
|
+
result.systemInstructions = isLiteLLMEnabled() ? void 0 : provider?.config?.instructions || void 0;
|
|
686
689
|
}
|
|
687
690
|
if (!requestedFields.includes("provider")) {
|
|
688
691
|
delete result.provider;
|
|
@@ -690,7 +693,7 @@ var addProviderFields = async (args, requestedFields, providers, result, tools,
|
|
|
690
693
|
if (requestedFields.includes("workflows")) {
|
|
691
694
|
let enabled = false;
|
|
692
695
|
let queueName = void 0;
|
|
693
|
-
if (provider?.workflows) {
|
|
696
|
+
if (!isLiteLLMEnabled() && provider?.workflows) {
|
|
694
697
|
enabled = provider?.workflows?.enabled || false;
|
|
695
698
|
if (provider?.workflows?.queue) {
|
|
696
699
|
const queue = await provider?.workflows?.queue;
|
|
@@ -859,7 +862,7 @@ var itemsPaginationRequest = async ({
|
|
|
859
862
|
};
|
|
860
863
|
var removeProviderFields = (requestedFields) => {
|
|
861
864
|
const filtered = requestedFields.filter((field) => !exuluProviderFields.includes(field));
|
|
862
|
-
filtered.push("
|
|
865
|
+
filtered.push("model");
|
|
863
866
|
return filtered;
|
|
864
867
|
};
|
|
865
868
|
var sanitizeRequestedFields = (table, requestedFields) => {
|
|
@@ -2167,7 +2170,7 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, allRerankers, di
|
|
|
2167
2170
|
}
|
|
2168
2171
|
console.log("[EXULU] available tools", enabledTools?.length);
|
|
2169
2172
|
console.log("[EXULU] disabled tools", disabledTools?.length);
|
|
2170
|
-
enabledTools = enabledTools.filter((
|
|
2173
|
+
enabledTools = enabledTools.filter((tool2) => !disabledTools.includes(tool2.id));
|
|
2171
2174
|
return enabledTools;
|
|
2172
2175
|
};
|
|
2173
2176
|
|
|
@@ -2175,7 +2178,7 @@ var getEnabledTools = async (agent, allExuluTools, allContexts, allRerankers, di
|
|
|
2175
2178
|
import "@opentelemetry/api";
|
|
2176
2179
|
import { v4 as uuidv4 } from "uuid";
|
|
2177
2180
|
import "ai";
|
|
2178
|
-
import
|
|
2181
|
+
import "crypto-js";
|
|
2179
2182
|
var redisConnection;
|
|
2180
2183
|
var unhandledRejectionHandlerInstalled = false;
|
|
2181
2184
|
var poolMonitoringInterval;
|
|
@@ -2312,7 +2315,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2312
2315
|
);
|
|
2313
2316
|
if (attempt < retries) {
|
|
2314
2317
|
const backoffMs = 500 * Math.pow(2, attempt - 1);
|
|
2315
|
-
await new Promise((
|
|
2318
|
+
await new Promise((resolve4) => setTimeout(resolve4, backoffMs));
|
|
2316
2319
|
}
|
|
2317
2320
|
}
|
|
2318
2321
|
}
|
|
@@ -2516,7 +2519,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2516
2519
|
} = await validateWorkflowPayload(data, providers);
|
|
2517
2520
|
const retries2 = 3;
|
|
2518
2521
|
let attempts = 0;
|
|
2519
|
-
const promise = new Promise(async (
|
|
2522
|
+
const promise = new Promise(async (resolve4, reject) => {
|
|
2520
2523
|
while (attempts < retries2) {
|
|
2521
2524
|
try {
|
|
2522
2525
|
const messages2 = await processUiMessagesFlow({
|
|
@@ -2531,7 +2534,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2531
2534
|
config,
|
|
2532
2535
|
variables: data.inputs
|
|
2533
2536
|
});
|
|
2534
|
-
|
|
2537
|
+
resolve4(messages2);
|
|
2535
2538
|
break;
|
|
2536
2539
|
} catch (error) {
|
|
2537
2540
|
console.error(
|
|
@@ -2542,7 +2545,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2542
2545
|
if (attempts >= retries2) {
|
|
2543
2546
|
reject(new Error(error instanceof Error ? error.message : String(error)));
|
|
2544
2547
|
}
|
|
2545
|
-
await new Promise((
|
|
2548
|
+
await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
|
|
2546
2549
|
}
|
|
2547
2550
|
}
|
|
2548
2551
|
});
|
|
@@ -2592,7 +2595,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2592
2595
|
} = await validateEvalPayload(data, providers);
|
|
2593
2596
|
const retries2 = 3;
|
|
2594
2597
|
let attempts = 0;
|
|
2595
|
-
const promise = new Promise(async (
|
|
2598
|
+
const promise = new Promise(async (resolve4, reject) => {
|
|
2596
2599
|
while (attempts < retries2) {
|
|
2597
2600
|
try {
|
|
2598
2601
|
const messages2 = await processUiMessagesFlow({
|
|
@@ -2606,7 +2609,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2606
2609
|
tools,
|
|
2607
2610
|
config
|
|
2608
2611
|
});
|
|
2609
|
-
|
|
2612
|
+
resolve4(messages2);
|
|
2610
2613
|
break;
|
|
2611
2614
|
} catch (error) {
|
|
2612
2615
|
console.error(
|
|
@@ -2617,7 +2620,7 @@ var createWorkers = async (providers, queues2, config, contexts, rerankers, eval
|
|
|
2617
2620
|
if (attempts >= retries2) {
|
|
2618
2621
|
reject(new Error(error instanceof Error ? error.message : String(error)));
|
|
2619
2622
|
}
|
|
2620
|
-
await new Promise((
|
|
2623
|
+
await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
|
|
2621
2624
|
}
|
|
2622
2625
|
}
|
|
2623
2626
|
});
|
|
@@ -3092,7 +3095,7 @@ var pollJobResult = async ({
|
|
|
3092
3095
|
attempts++;
|
|
3093
3096
|
const job = await Job.fromId(queue.queue, jobId);
|
|
3094
3097
|
if (!job) {
|
|
3095
|
-
await new Promise((
|
|
3098
|
+
await new Promise((resolve4) => setTimeout((resolve5) => resolve5(true), 2e3));
|
|
3096
3099
|
continue;
|
|
3097
3100
|
}
|
|
3098
3101
|
const elapsedTime = Date.now() - startTime;
|
|
@@ -3122,7 +3125,7 @@ var pollJobResult = async ({
|
|
|
3122
3125
|
console.log(`[EXULU] eval function ${job.id} result: ${result}`);
|
|
3123
3126
|
break;
|
|
3124
3127
|
}
|
|
3125
|
-
await new Promise((
|
|
3128
|
+
await new Promise((resolve4) => setTimeout(() => resolve4(true), 2e3));
|
|
3126
3129
|
}
|
|
3127
3130
|
return result;
|
|
3128
3131
|
};
|
|
@@ -3158,27 +3161,19 @@ var processUiMessagesFlow = async ({
|
|
|
3158
3161
|
"[EXULU] enabled tools",
|
|
3159
3162
|
enabledTools?.map((x) => x.name + " (" + x.id + ")")
|
|
3160
3163
|
);
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
const variable = await db.from("variables").where({ name: variableName }).first();
|
|
3166
|
-
if (!variable) {
|
|
3167
|
-
throw new Error(
|
|
3168
|
-
`Provider API key variable not found for agent ${agent.name} (${agent.id}).`
|
|
3169
|
-
);
|
|
3170
|
-
}
|
|
3171
|
-
providerapikey = variable.value;
|
|
3172
|
-
if (!variable.encrypted) {
|
|
3173
|
-
throw new Error(
|
|
3174
|
-
`Provider API key variable not encrypted for agent ${agent.name} (${agent.id}), for security reasons you are only allowed to use encrypted variables for provider API keys.`
|
|
3175
|
-
);
|
|
3176
|
-
}
|
|
3177
|
-
if (variable.encrypted) {
|
|
3178
|
-
const bytes = CryptoJS2.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
3179
|
-
providerapikey = bytes.toString(CryptoJS2.enc.Utf8);
|
|
3180
|
-
}
|
|
3164
|
+
if (!agent.model) {
|
|
3165
|
+
throw new Error(
|
|
3166
|
+
`Agent ${agent.name} (${agent.id}) has no model configured.`
|
|
3167
|
+
);
|
|
3181
3168
|
}
|
|
3169
|
+
const resolved = await resolveModel({
|
|
3170
|
+
modelId: agent.model,
|
|
3171
|
+
user,
|
|
3172
|
+
providers,
|
|
3173
|
+
agent: { id: agent.id }
|
|
3174
|
+
});
|
|
3175
|
+
const providerapikey = resolved.apiKey;
|
|
3176
|
+
const resolvedLanguageModel = resolved.languageModel;
|
|
3182
3177
|
const messagesWithoutPlaceholder = inputMessages.filter(
|
|
3183
3178
|
(message) => message.metadata?.type !== "placeholder"
|
|
3184
3179
|
);
|
|
@@ -3208,18 +3203,18 @@ var processUiMessagesFlow = async ({
|
|
|
3208
3203
|
const text = part.text;
|
|
3209
3204
|
const variableNames = [...text.matchAll(/{([^}]+)}/g)].map((match) => match[1]);
|
|
3210
3205
|
if (variableNames) {
|
|
3211
|
-
for (const
|
|
3212
|
-
if (!
|
|
3206
|
+
for (const variableName of variableNames) {
|
|
3207
|
+
if (!variableName) {
|
|
3213
3208
|
continue;
|
|
3214
3209
|
}
|
|
3215
|
-
console.log("[EXULU] variableName",
|
|
3216
|
-
const variableValue = variables?.[
|
|
3210
|
+
console.log("[EXULU] variableName", variableName);
|
|
3211
|
+
const variableValue = variables?.[variableName];
|
|
3217
3212
|
console.log("[EXULU] variableValue", variableValue);
|
|
3218
3213
|
if (variableValue) {
|
|
3219
|
-
part.text = part.text.replaceAll(`{${
|
|
3214
|
+
part.text = part.text.replaceAll(`{${variableName}}`, variableValue);
|
|
3220
3215
|
} else {
|
|
3221
3216
|
throw new Error(
|
|
3222
|
-
`Value for variable ${
|
|
3217
|
+
`Value for variable ${variableName} not provided in variables for processing message flow. Either remove it from the messages, or provide it as an argument.`
|
|
3223
3218
|
);
|
|
3224
3219
|
}
|
|
3225
3220
|
}
|
|
@@ -3230,7 +3225,7 @@ var processUiMessagesFlow = async ({
|
|
|
3230
3225
|
label: agent.name,
|
|
3231
3226
|
trigger: "agent"
|
|
3232
3227
|
};
|
|
3233
|
-
messageHistory = await new Promise(async (
|
|
3228
|
+
messageHistory = await new Promise(async (resolve4, reject) => {
|
|
3234
3229
|
const startTime = Date.now();
|
|
3235
3230
|
try {
|
|
3236
3231
|
const result = await provider.generateStream({
|
|
@@ -3238,13 +3233,14 @@ var processUiMessagesFlow = async ({
|
|
|
3238
3233
|
rerankers,
|
|
3239
3234
|
agent,
|
|
3240
3235
|
user,
|
|
3241
|
-
approvedTools: tools.map((
|
|
3236
|
+
approvedTools: tools.map((tool2) => "tool-" + sanitizeToolName(tool2.name)),
|
|
3242
3237
|
instructions: agent.instructions,
|
|
3243
3238
|
session: void 0,
|
|
3244
3239
|
previousMessages: messageHistory.messages,
|
|
3245
3240
|
message: currentMessage,
|
|
3246
3241
|
currentTools: enabledTools,
|
|
3247
3242
|
allExuluTools: tools,
|
|
3243
|
+
languageModel: resolvedLanguageModel,
|
|
3248
3244
|
providerapikey,
|
|
3249
3245
|
toolConfigs: agent.tools,
|
|
3250
3246
|
exuluConfig: config
|
|
@@ -3307,7 +3303,7 @@ var processUiMessagesFlow = async ({
|
|
|
3307
3303
|
})
|
|
3308
3304
|
] : []
|
|
3309
3305
|
]);
|
|
3310
|
-
|
|
3306
|
+
resolve4({
|
|
3311
3307
|
messages,
|
|
3312
3308
|
metadata: {
|
|
3313
3309
|
tokens: {
|
|
@@ -3361,6 +3357,7 @@ function getAverage(arr) {
|
|
|
3361
3357
|
}
|
|
3362
3358
|
|
|
3363
3359
|
// src/graphql/schemas/index.ts
|
|
3360
|
+
import "fs";
|
|
3364
3361
|
function createExuluContextsTypeDefs(table) {
|
|
3365
3362
|
const enumDefs = table.fields.filter((field) => field.type === "enum" && field.enumValues).map((field) => {
|
|
3366
3363
|
if (!field.enumValues) {
|
|
@@ -3730,6 +3727,9 @@ type PageInfo {
|
|
|
3730
3727
|
typeDefs += `
|
|
3731
3728
|
providers: ProviderPaginationResult
|
|
3732
3729
|
`;
|
|
3730
|
+
typeDefs += `
|
|
3731
|
+
litellmCatalog: [LiteLLMModel!]!
|
|
3732
|
+
`;
|
|
3733
3733
|
typeDefs += `
|
|
3734
3734
|
workflowSchedule(workflow: ID!): WorkflowScheduleResult
|
|
3735
3735
|
`;
|
|
@@ -3793,6 +3793,24 @@ type RateLimitUsageRow {
|
|
|
3793
3793
|
inputTokens: Int!
|
|
3794
3794
|
outputTokens: Int!
|
|
3795
3795
|
}
|
|
3796
|
+
`;
|
|
3797
|
+
modelDefs += `
|
|
3798
|
+
type LiteLLMModel {
|
|
3799
|
+
model_name: String!
|
|
3800
|
+
upstream_model: String
|
|
3801
|
+
active: Boolean
|
|
3802
|
+
tags: [String!]
|
|
3803
|
+
type: String
|
|
3804
|
+
brand: String
|
|
3805
|
+
region: String
|
|
3806
|
+
max_tokens: Int
|
|
3807
|
+
max_input_tokens: Int
|
|
3808
|
+
max_output_tokens: Int
|
|
3809
|
+
supports_vision: Boolean
|
|
3810
|
+
supports_function_calling: Boolean
|
|
3811
|
+
supports_pdf_input: Boolean
|
|
3812
|
+
supports_audio_input: Boolean
|
|
3813
|
+
}
|
|
3796
3814
|
`;
|
|
3797
3815
|
resolvers.Query["agentRateLimitUsage"] = async (_, args, context) => {
|
|
3798
3816
|
if (!checkLicense()["rate-limits"]) return [];
|
|
@@ -3846,6 +3864,10 @@ type RateLimitUsageRow {
|
|
|
3846
3864
|
})
|
|
3847
3865
|
};
|
|
3848
3866
|
};
|
|
3867
|
+
resolvers.Query["litellmCatalog"] = async () => {
|
|
3868
|
+
const { fetchLiteLLMCatalog } = await import("./catalog-EOKGOHTY.js");
|
|
3869
|
+
return fetchLiteLLMCatalog();
|
|
3870
|
+
};
|
|
3849
3871
|
resolvers.Query["workflowSchedule"] = async (_, args, context, info) => {
|
|
3850
3872
|
if (!args.workflow) {
|
|
3851
3873
|
throw new Error("Workflow template ID is required");
|
|
@@ -3866,10 +3888,10 @@ type RateLimitUsageRow {
|
|
|
3866
3888
|
if (!agent) {
|
|
3867
3889
|
throw new Error("Agent instance not found for workflow template.");
|
|
3868
3890
|
}
|
|
3869
|
-
const provider =
|
|
3891
|
+
const provider = await resolveAgentProvider(agent, providers);
|
|
3870
3892
|
if (!provider) {
|
|
3871
3893
|
throw new Error(
|
|
3872
|
-
"
|
|
3894
|
+
"ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
|
|
3873
3895
|
);
|
|
3874
3896
|
}
|
|
3875
3897
|
let queue;
|
|
@@ -3941,10 +3963,10 @@ type RateLimitUsageRow {
|
|
|
3941
3963
|
if (!agent) {
|
|
3942
3964
|
throw new Error("Agent instance not found for workflow template.");
|
|
3943
3965
|
}
|
|
3944
|
-
const provider =
|
|
3966
|
+
const provider = await resolveAgentProvider(agent, providers);
|
|
3945
3967
|
if (!provider) {
|
|
3946
3968
|
throw new Error(
|
|
3947
|
-
"
|
|
3969
|
+
"ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
|
|
3948
3970
|
);
|
|
3949
3971
|
}
|
|
3950
3972
|
let queue;
|
|
@@ -3982,10 +4004,10 @@ type RateLimitUsageRow {
|
|
|
3982
4004
|
if (!agent) {
|
|
3983
4005
|
throw new Error("Agent instance not found for workflow template.");
|
|
3984
4006
|
}
|
|
3985
|
-
const provider =
|
|
4007
|
+
const provider = await resolveAgentProvider(agent, providers);
|
|
3986
4008
|
if (!provider) {
|
|
3987
4009
|
throw new Error(
|
|
3988
|
-
"
|
|
4010
|
+
"ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
|
|
3989
4011
|
);
|
|
3990
4012
|
}
|
|
3991
4013
|
let queue;
|
|
@@ -4046,10 +4068,10 @@ type RateLimitUsageRow {
|
|
|
4046
4068
|
if (!agent) {
|
|
4047
4069
|
throw new Error("Agent instance not found for workflow template.");
|
|
4048
4070
|
}
|
|
4049
|
-
const provider =
|
|
4071
|
+
const provider = await resolveAgentProvider(agent, providers);
|
|
4050
4072
|
if (!provider) {
|
|
4051
4073
|
throw new Error(
|
|
4052
|
-
"
|
|
4074
|
+
"ExuluProvider not registered for the model configured on agent instance " + agent.id + "."
|
|
4053
4075
|
);
|
|
4054
4076
|
}
|
|
4055
4077
|
let queue;
|
|
@@ -4107,7 +4129,7 @@ type RateLimitUsageRow {
|
|
|
4107
4129
|
} = await validateWorkflowPayload(jobData, providers);
|
|
4108
4130
|
const retries = 3;
|
|
4109
4131
|
let attempts = 0;
|
|
4110
|
-
const promise = new Promise(async (
|
|
4132
|
+
const promise = new Promise(async (resolve4, reject) => {
|
|
4111
4133
|
while (attempts < retries) {
|
|
4112
4134
|
try {
|
|
4113
4135
|
const messages2 = await processUiMessagesFlow({
|
|
@@ -4122,7 +4144,7 @@ type RateLimitUsageRow {
|
|
|
4122
4144
|
config,
|
|
4123
4145
|
variables: args.variables
|
|
4124
4146
|
});
|
|
4125
|
-
|
|
4147
|
+
resolve4(messages2);
|
|
4126
4148
|
break;
|
|
4127
4149
|
} catch (error) {
|
|
4128
4150
|
console.error(
|
|
@@ -4136,7 +4158,7 @@ type RateLimitUsageRow {
|
|
|
4136
4158
|
if (attempts >= retries) {
|
|
4137
4159
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
4138
4160
|
}
|
|
4139
|
-
await new Promise((
|
|
4161
|
+
await new Promise((resolve5) => setTimeout((resolve6) => resolve6(true), 2e3));
|
|
4140
4162
|
}
|
|
4141
4163
|
}
|
|
4142
4164
|
});
|
|
@@ -4389,10 +4411,10 @@ type RateLimitUsageRow {
|
|
|
4389
4411
|
contexts.map(async (context2) => {
|
|
4390
4412
|
let processor = null;
|
|
4391
4413
|
if (context2.processor) {
|
|
4392
|
-
processor = await new Promise(async (
|
|
4414
|
+
processor = await new Promise(async (resolve4, reject) => {
|
|
4393
4415
|
const config2 = context2.processor?.config;
|
|
4394
4416
|
const queue = await config2?.queue;
|
|
4395
|
-
|
|
4417
|
+
resolve4({
|
|
4396
4418
|
name: context2.processor.name,
|
|
4397
4419
|
description: context2.processor.description,
|
|
4398
4420
|
queue: queue?.queue?.name || void 0,
|
|
@@ -4473,10 +4495,10 @@ type RateLimitUsageRow {
|
|
|
4473
4495
|
}
|
|
4474
4496
|
let processor = null;
|
|
4475
4497
|
if (data.processor) {
|
|
4476
|
-
processor = await new Promise(async (
|
|
4498
|
+
processor = await new Promise(async (resolve4, reject) => {
|
|
4477
4499
|
const config2 = data.processor?.config;
|
|
4478
4500
|
const queue = await config2?.queue;
|
|
4479
|
-
|
|
4501
|
+
resolve4({
|
|
4480
4502
|
name: data.processor.name,
|
|
4481
4503
|
description: data.processor.description,
|
|
4482
4504
|
queue: queue?.queue?.name || void 0,
|
|
@@ -4557,7 +4579,7 @@ type RateLimitUsageRow {
|
|
|
4557
4579
|
const instances = await exuluApp.get().agents();
|
|
4558
4580
|
let agentTools = await Promise.all(
|
|
4559
4581
|
instances.map(async (agent) => {
|
|
4560
|
-
const provider =
|
|
4582
|
+
const provider = await resolveAgentProvider(agent, providers);
|
|
4561
4583
|
if (!provider) {
|
|
4562
4584
|
return null;
|
|
4563
4585
|
}
|
|
@@ -4565,7 +4587,7 @@ type RateLimitUsageRow {
|
|
|
4565
4587
|
})
|
|
4566
4588
|
);
|
|
4567
4589
|
let agenticRetrievalTool = void 0;
|
|
4568
|
-
const filtered = agentTools.filter((
|
|
4590
|
+
const filtered = agentTools.filter((tool2) => tool2 !== null);
|
|
4569
4591
|
let allTools = [...filtered, ...tools];
|
|
4570
4592
|
if (contexts?.length) {
|
|
4571
4593
|
agenticRetrievalTool = createAgenticRetrievalToolV3({
|
|
@@ -4583,21 +4605,21 @@ type RateLimitUsageRow {
|
|
|
4583
4605
|
if (search && search.trim()) {
|
|
4584
4606
|
const searchTerm = search.toLowerCase().trim();
|
|
4585
4607
|
allTools = allTools.filter(
|
|
4586
|
-
(
|
|
4608
|
+
(tool2) => tool2.name?.toLowerCase().includes(searchTerm) || tool2.description?.toLowerCase().includes(searchTerm)
|
|
4587
4609
|
);
|
|
4588
4610
|
}
|
|
4589
4611
|
if (category && category.trim()) {
|
|
4590
|
-
allTools = allTools.filter((
|
|
4612
|
+
allTools = allTools.filter((tool2) => tool2.category === category);
|
|
4591
4613
|
}
|
|
4592
4614
|
const total = allTools.length;
|
|
4593
4615
|
const start = page * limit;
|
|
4594
4616
|
const end = start + limit;
|
|
4595
4617
|
const paginatedTools = allTools.slice(start, end);
|
|
4596
4618
|
return {
|
|
4597
|
-
items: paginatedTools.map((
|
|
4619
|
+
items: paginatedTools.map((tool2) => {
|
|
4598
4620
|
const object = {};
|
|
4599
4621
|
requestedFields.forEach((field) => {
|
|
4600
|
-
object[field] =
|
|
4622
|
+
object[field] = tool2[field];
|
|
4601
4623
|
});
|
|
4602
4624
|
return object;
|
|
4603
4625
|
}),
|
|
@@ -4607,7 +4629,7 @@ type RateLimitUsageRow {
|
|
|
4607
4629
|
};
|
|
4608
4630
|
};
|
|
4609
4631
|
resolvers.Query["toolCategories"] = async () => {
|
|
4610
|
-
const array = tools.map((
|
|
4632
|
+
const array = tools.map((tool2) => tool2.category).filter((category) => category && typeof category === "string");
|
|
4611
4633
|
array.push("contexts");
|
|
4612
4634
|
array.push("agents");
|
|
4613
4635
|
return [...new Set(array)].sort();
|
|
@@ -4798,6 +4820,9 @@ type Provider {
|
|
|
4798
4820
|
provider: String
|
|
4799
4821
|
modelName: String
|
|
4800
4822
|
type: EnumProviderType!
|
|
4823
|
+
authenticationInformation: String
|
|
4824
|
+
maxContextLength: Int
|
|
4825
|
+
capabilities: JSON
|
|
4801
4826
|
}
|
|
4802
4827
|
|
|
4803
4828
|
type Eval {
|
|
@@ -5158,37 +5183,10 @@ import { InMemoryLRUCache } from "@apollo/utils.keyvaluecache";
|
|
|
5158
5183
|
import bodyParser from "body-parser";
|
|
5159
5184
|
import CryptoJS5 from "crypto-js";
|
|
5160
5185
|
import OpenAI from "openai";
|
|
5161
|
-
import
|
|
5186
|
+
import fs3 from "fs";
|
|
5162
5187
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5163
5188
|
import "@opentelemetry/api";
|
|
5164
|
-
import
|
|
5165
|
-
|
|
5166
|
-
// src/utils/claude-messages.ts
|
|
5167
|
-
var CLAUDE_MESSAGES = {
|
|
5168
|
-
anthropic_token_variable_not_encrypted: `
|
|
5169
|
-
\x1B[41m -- Anthropic token variable set by your admin is not encrypted. This poses a security risk. Please contact your admin to fix the variable used for your key. --
|
|
5170
|
-
\x1B[0m`,
|
|
5171
|
-
anthropic_token_variable_not_found: `
|
|
5172
|
-
\x1B[41m -- Anthropic token variable not found. Please contact your Exulu adminto fix the variable used for the key. --
|
|
5173
|
-
\x1B[0m`,
|
|
5174
|
-
authentication_error: `
|
|
5175
|
-
\x1B[41m -- Authentication error please check your IMP token and try again. --
|
|
5176
|
-
\x1B[0m`,
|
|
5177
|
-
missing_body: `
|
|
5178
|
-
\x1B[41m -- Missing body Anthropic response. --
|
|
5179
|
-
\x1B[0m`,
|
|
5180
|
-
missing_nextauth_secret: `
|
|
5181
|
-
\x1B[41m -- Missing NEXTAUTH_SECRET in environment variables on the server. --
|
|
5182
|
-
\x1B[0m`,
|
|
5183
|
-
not_enabled: `
|
|
5184
|
-
\x1B[41m -- The agent you selected does not have a valid API key set for it. --
|
|
5185
|
-
\x1B[0m`,
|
|
5186
|
-
missing_project: `
|
|
5187
|
-
\x1B[41m -- Project not found or you do not have access to it. --
|
|
5188
|
-
\x1B[0m`
|
|
5189
|
-
};
|
|
5190
|
-
|
|
5191
|
-
// src/exulu/routes.ts
|
|
5189
|
+
import JSZip2 from "jszip";
|
|
5192
5190
|
import { createIdGenerator } from "ai";
|
|
5193
5191
|
import cookieParser from "cookie-parser";
|
|
5194
5192
|
|
|
@@ -5196,7 +5194,6 @@ import cookieParser from "cookie-parser";
|
|
|
5196
5194
|
import { z } from "zod";
|
|
5197
5195
|
import {
|
|
5198
5196
|
convertToModelMessages,
|
|
5199
|
-
Output,
|
|
5200
5197
|
generateText as generateText2,
|
|
5201
5198
|
streamText,
|
|
5202
5199
|
validateUIMessages,
|
|
@@ -5212,7 +5209,7 @@ function generateSlug(name) {
|
|
|
5212
5209
|
}
|
|
5213
5210
|
|
|
5214
5211
|
// src/exulu/provider.ts
|
|
5215
|
-
import
|
|
5212
|
+
import "crypto-js";
|
|
5216
5213
|
import { parseOfficeAsync } from "officeparser";
|
|
5217
5214
|
|
|
5218
5215
|
// src/exulu/task-description.ts
|
|
@@ -5258,7 +5255,7 @@ async function clearSessionCurrentTask(session) {
|
|
|
5258
5255
|
}
|
|
5259
5256
|
|
|
5260
5257
|
// src/exulu/provider.ts
|
|
5261
|
-
import
|
|
5258
|
+
import fs2 from "fs";
|
|
5262
5259
|
var ExuluProvider = class {
|
|
5263
5260
|
// Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
|
|
5264
5261
|
// underscores and be a max length of 80 characters and at least 5 characters long.
|
|
@@ -5360,27 +5357,18 @@ var ExuluProvider = class {
|
|
|
5360
5357
|
providers,
|
|
5361
5358
|
user
|
|
5362
5359
|
);
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
const variable = await db.from("variables").where({ name: variableName }).first();
|
|
5368
|
-
if (!variable) {
|
|
5369
|
-
throw new Error(
|
|
5370
|
-
"Provider API key variable not found for agent: " + agent.name + " (" + agent.id + ") being called as a tool."
|
|
5371
|
-
);
|
|
5372
|
-
}
|
|
5373
|
-
providerapikey = variable.value;
|
|
5374
|
-
if (!variable.encrypted) {
|
|
5375
|
-
throw new Error(
|
|
5376
|
-
"Provider API key variable not encrypted for agent: " + agent.name + " (" + agent.id + ") being called as a tool, for security reasons you are only allowed to use encrypted variables for provider API keys."
|
|
5377
|
-
);
|
|
5378
|
-
}
|
|
5379
|
-
if (variable.encrypted) {
|
|
5380
|
-
const bytes = CryptoJS3.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
5381
|
-
providerapikey = bytes.toString(CryptoJS3.enc.Utf8);
|
|
5382
|
-
}
|
|
5360
|
+
if (!agent.model) {
|
|
5361
|
+
throw new Error(
|
|
5362
|
+
`Agent ${agent.name} (${agent.id}) has no model configured (called as a tool).`
|
|
5363
|
+
);
|
|
5383
5364
|
}
|
|
5365
|
+
const resolved = await resolveModel({
|
|
5366
|
+
modelId: agent.model,
|
|
5367
|
+
user,
|
|
5368
|
+
providers,
|
|
5369
|
+
agent: { id: agent.id }
|
|
5370
|
+
});
|
|
5371
|
+
const providerapikey = resolved.apiKey;
|
|
5384
5372
|
console.log(
|
|
5385
5373
|
"[EXULU] Enabled tools for agent '" + agent.name + " (" + agent.id + ") that is being called as a tool",
|
|
5386
5374
|
enabledTools.map((x) => x.name + " (" + x.id + ")")
|
|
@@ -5399,6 +5387,7 @@ var ExuluProvider = class {
|
|
|
5399
5387
|
rerankers,
|
|
5400
5388
|
instructions: agent.instructions,
|
|
5401
5389
|
prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
|
|
5390
|
+
languageModel: resolved.languageModel,
|
|
5402
5391
|
providerapikey,
|
|
5403
5392
|
user,
|
|
5404
5393
|
currentTools: enabledTools,
|
|
@@ -5436,10 +5425,10 @@ var ExuluProvider = class {
|
|
|
5436
5425
|
statistics,
|
|
5437
5426
|
toolConfigs,
|
|
5438
5427
|
providerapikey,
|
|
5428
|
+
languageModel,
|
|
5439
5429
|
contexts,
|
|
5440
5430
|
rerankers,
|
|
5441
5431
|
exuluConfig,
|
|
5442
|
-
outputSchema,
|
|
5443
5432
|
agent,
|
|
5444
5433
|
instructions,
|
|
5445
5434
|
maxStepCount,
|
|
@@ -5449,9 +5438,6 @@ var ExuluProvider = class {
|
|
|
5449
5438
|
"[EXULU] Called generate sync for agent: " + this.name,
|
|
5450
5439
|
"with prompt: " + prompt?.slice(0, 100) + "..."
|
|
5451
5440
|
);
|
|
5452
|
-
if (!this.model) {
|
|
5453
|
-
throw new Error("Model is required for streaming.");
|
|
5454
|
-
}
|
|
5455
5441
|
if (!this.config) {
|
|
5456
5442
|
throw new Error("Config is required for generating.");
|
|
5457
5443
|
}
|
|
@@ -5468,13 +5454,7 @@ var ExuluProvider = class {
|
|
|
5468
5454
|
sessionItems = sessionData.session_items;
|
|
5469
5455
|
project = sessionData.project;
|
|
5470
5456
|
}
|
|
5471
|
-
const model =
|
|
5472
|
-
...providerapikey ? { apiKey: providerapikey } : {},
|
|
5473
|
-
user: user?.id,
|
|
5474
|
-
role: user?.role?.id,
|
|
5475
|
-
project,
|
|
5476
|
-
agent: agent?.id
|
|
5477
|
-
});
|
|
5457
|
+
const model = languageModel;
|
|
5478
5458
|
console.log("[EXULU] Model for agent: " + this.name, " created for generating sync.");
|
|
5479
5459
|
let messages = inputMessages || [];
|
|
5480
5460
|
if (messages && session && user) {
|
|
@@ -5553,12 +5533,12 @@ var ExuluProvider = class {
|
|
|
5553
5533
|
system += "\n\n" + memoryContext;
|
|
5554
5534
|
}
|
|
5555
5535
|
const includesContextSearchTool = currentTools?.some(
|
|
5556
|
-
(
|
|
5536
|
+
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
5557
5537
|
);
|
|
5558
5538
|
const includesWebSearchTool = currentTools?.some(
|
|
5559
|
-
(
|
|
5539
|
+
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
5560
5540
|
);
|
|
5561
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((
|
|
5541
|
+
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
5562
5542
|
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
5563
5543
|
if (includesContextSearchTool) {
|
|
5564
5544
|
system += `
|
|
@@ -5617,66 +5597,47 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
5617
5597
|
let result = { object: null, text: "" };
|
|
5618
5598
|
let inputTokens = 0;
|
|
5619
5599
|
let outputTokens = 0;
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5600
|
+
console.log(
|
|
5601
|
+
"[EXULU] Generating text for agent: " + this.name,
|
|
5602
|
+
"with prompt: " + prompt?.slice(0, 100) + "..."
|
|
5603
|
+
);
|
|
5604
|
+
const output = await generateText2({
|
|
5605
|
+
temperature: 0,
|
|
5606
|
+
// TODO Make this configurable
|
|
5607
|
+
model,
|
|
5608
|
+
system,
|
|
5609
|
+
prompt,
|
|
5610
|
+
maxRetries: 2,
|
|
5611
|
+
tools: await convertExuluToolsToAiSdkTools(
|
|
5612
|
+
currentTools,
|
|
5613
|
+
currentSkills,
|
|
5614
|
+
approvedTools,
|
|
5615
|
+
allExuluTools,
|
|
5616
|
+
toolConfigs,
|
|
5617
|
+
providerapikey,
|
|
5618
|
+
contexts,
|
|
5619
|
+
rerankers,
|
|
5620
|
+
user,
|
|
5621
|
+
exuluConfig,
|
|
5622
|
+
session,
|
|
5623
|
+
req,
|
|
5624
|
+
project,
|
|
5625
|
+
sessionItems,
|
|
5645
5626
|
model,
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
session,
|
|
5661
|
-
req,
|
|
5662
|
-
project,
|
|
5663
|
-
sessionItems,
|
|
5664
|
-
model,
|
|
5665
|
-
agent,
|
|
5666
|
-
memoryItems
|
|
5667
|
-
),
|
|
5668
|
-
stopWhen: [stepCountIs(maxStepCount || 5)]
|
|
5669
|
-
// make configurable
|
|
5670
|
-
});
|
|
5671
|
-
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
5672
|
-
const {
|
|
5673
|
-
text,
|
|
5674
|
-
totalUsage
|
|
5675
|
-
} = output;
|
|
5676
|
-
result.text = text;
|
|
5677
|
-
inputTokens = totalUsage?.inputTokens || 0;
|
|
5678
|
-
outputTokens = totalUsage?.outputTokens || 0;
|
|
5679
|
-
}
|
|
5627
|
+
agent,
|
|
5628
|
+
memoryItems
|
|
5629
|
+
),
|
|
5630
|
+
stopWhen: [stepCountIs(maxStepCount || 5)]
|
|
5631
|
+
// make configurable
|
|
5632
|
+
});
|
|
5633
|
+
console.log("[EXULU] Output: " + JSON.stringify(output, null, 2));
|
|
5634
|
+
const {
|
|
5635
|
+
text,
|
|
5636
|
+
totalUsage
|
|
5637
|
+
} = output;
|
|
5638
|
+
result.text = text;
|
|
5639
|
+
inputTokens = totalUsage?.inputTokens || 0;
|
|
5640
|
+
outputTokens = totalUsage?.outputTokens || 0;
|
|
5680
5641
|
if (statistics) {
|
|
5681
5642
|
await Promise.all([
|
|
5682
5643
|
updateStatistic({
|
|
@@ -5881,6 +5842,7 @@ ${extractedText}
|
|
|
5881
5842
|
allExuluTools,
|
|
5882
5843
|
toolConfigs,
|
|
5883
5844
|
providerapikey,
|
|
5845
|
+
languageModel,
|
|
5884
5846
|
contexts,
|
|
5885
5847
|
rerankers,
|
|
5886
5848
|
exuluConfig,
|
|
@@ -5888,10 +5850,6 @@ ${extractedText}
|
|
|
5888
5850
|
req,
|
|
5889
5851
|
maxStepCount
|
|
5890
5852
|
}) => {
|
|
5891
|
-
if (!this.model) {
|
|
5892
|
-
console.error("[EXULU] Model is required for streaming.");
|
|
5893
|
-
throw new Error("Model is required for streaming.");
|
|
5894
|
-
}
|
|
5895
5853
|
if (!this.config) {
|
|
5896
5854
|
console.error("[EXULU] Config is required for streaming.");
|
|
5897
5855
|
throw new Error("Config is required for generating.");
|
|
@@ -5917,13 +5875,7 @@ ${extractedText}
|
|
|
5917
5875
|
});
|
|
5918
5876
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
5919
5877
|
}
|
|
5920
|
-
const model =
|
|
5921
|
-
...providerapikey ? { apiKey: providerapikey } : {},
|
|
5922
|
-
user: user?.id,
|
|
5923
|
-
role: user?.role?.id,
|
|
5924
|
-
project,
|
|
5925
|
-
agent: agent?.id
|
|
5926
|
-
});
|
|
5878
|
+
const model = languageModel;
|
|
5927
5879
|
messages = await validateUIMessages({
|
|
5928
5880
|
// append the new message to the previous messages:
|
|
5929
5881
|
messages: [...previousMessagesContent, message]
|
|
@@ -5960,7 +5912,7 @@ ${extractedText}
|
|
|
5960
5912
|
// todo make this configurable?
|
|
5961
5913
|
page: 1
|
|
5962
5914
|
});
|
|
5963
|
-
|
|
5915
|
+
fs2.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
|
|
5964
5916
|
if (result2?.chunks?.length) {
|
|
5965
5917
|
memoryItems = result2.chunks;
|
|
5966
5918
|
memoryContext = `
|
|
@@ -5983,12 +5935,12 @@ ${extractedText}
|
|
|
5983
5935
|
let system = instructions || "You are a helpful assistant. When you use a tool to answer a question do not explicitly comment on the result of the tool call unless the user has explicitly you to do something with the result.";
|
|
5984
5936
|
system += "\n\n" + genericContext;
|
|
5985
5937
|
const includesContextSearchTool = currentTools?.some(
|
|
5986
|
-
(
|
|
5938
|
+
(tool2) => tool2.name.toLowerCase().includes("context_search") || tool2.id.includes("context_search") || tool2.type === "context"
|
|
5987
5939
|
);
|
|
5988
5940
|
const includesWebSearchTool = currentTools?.some(
|
|
5989
|
-
(
|
|
5941
|
+
(tool2) => tool2.name.toLowerCase().includes("web_search") || tool2.id.includes("web_search") || tool2.type === "web_search"
|
|
5990
5942
|
);
|
|
5991
|
-
console.log("[EXULU] Current tools: " + currentTools?.map((
|
|
5943
|
+
console.log("[EXULU] Current tools: " + currentTools?.map((tool2) => tool2.name).join("\n"));
|
|
5992
5944
|
console.log("[EXULU] Includes context search tool: " + includesContextSearchTool);
|
|
5993
5945
|
console.log("[EXULU] Includes web search tool: " + includesWebSearchTool);
|
|
5994
5946
|
if (includesContextSearchTool) {
|
|
@@ -6088,7 +6040,7 @@ ${skillsList}
|
|
|
6088
6040
|
|
|
6089
6041
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
6090
6042
|
'Inform the user that the action was not performed.`;
|
|
6091
|
-
|
|
6043
|
+
fs2.writeFileSync("system-prompt.txt", system);
|
|
6092
6044
|
console.log("[EXULU] Tools", currentTools?.map((x) => x.name));
|
|
6093
6045
|
console.log("[EXULU] Skills", currentSkills?.map((x) => x.name));
|
|
6094
6046
|
const tools = await convertExuluToolsToAiSdkTools(
|
|
@@ -6175,7 +6127,8 @@ var getSession = async ({ sessionID }) => {
|
|
|
6175
6127
|
var saveChat = async ({
|
|
6176
6128
|
session,
|
|
6177
6129
|
user,
|
|
6178
|
-
messages
|
|
6130
|
+
messages,
|
|
6131
|
+
model
|
|
6179
6132
|
}) => {
|
|
6180
6133
|
const { db } = await postgresClient();
|
|
6181
6134
|
for (const message of messages) {
|
|
@@ -6184,13 +6137,168 @@ var saveChat = async ({
|
|
|
6184
6137
|
user,
|
|
6185
6138
|
content: JSON.stringify(message),
|
|
6186
6139
|
message_id: message.id,
|
|
6187
|
-
title: message.role === "user" ? "User" : "Assistant"
|
|
6140
|
+
title: message.role === "user" ? "User" : "Assistant",
|
|
6141
|
+
...model ? { model } : {}
|
|
6188
6142
|
}).returning("id");
|
|
6189
6143
|
mutation.onConflict("message_id").merge();
|
|
6190
6144
|
await mutation;
|
|
6191
6145
|
}
|
|
6192
6146
|
};
|
|
6193
6147
|
|
|
6148
|
+
// src/exulu/suggestions.ts
|
|
6149
|
+
import {
|
|
6150
|
+
convertToModelMessages as convertToModelMessages2,
|
|
6151
|
+
generateText as generateText3,
|
|
6152
|
+
tool
|
|
6153
|
+
} from "ai";
|
|
6154
|
+
import { z as z2 } from "zod";
|
|
6155
|
+
var SUGGESTIONS_SYSTEM_PROMPT = "You generate short follow-up message suggestions for the user. You are NOT continuing the conversation as the assistant \u2014 you are predicting what the user might want to say next. Suggest up to 3 short follow-up questions or messages the user might want to send next. Each suggestion must be written from the user's perspective (first person) and be 12 words or fewer. You MUST submit your answer by calling the `submit_suggestions` tool exactly once. Do not emit any plain text \u2014 only the tool call.";
|
|
6156
|
+
var submitSuggestionsTool = tool({
|
|
6157
|
+
description: "Submit the final list of follow-up message suggestions for the user. Must be called exactly once. Each suggestion is written from the user's perspective (first person) and is 12 words or fewer.",
|
|
6158
|
+
inputSchema: z2.object({
|
|
6159
|
+
suggestions: z2.array(z2.string()).max(3)
|
|
6160
|
+
})
|
|
6161
|
+
});
|
|
6162
|
+
var MAX_CHARS_PER_MESSAGE = 1e4;
|
|
6163
|
+
var trimMessagesForSuggestions = (messages) => {
|
|
6164
|
+
const out = [];
|
|
6165
|
+
for (const m of messages) {
|
|
6166
|
+
const textBlobs = [];
|
|
6167
|
+
for (const p of m.parts ?? []) {
|
|
6168
|
+
if (p.type === "text" && typeof p.text === "string" && p.text.trim().length > 0) {
|
|
6169
|
+
textBlobs.push(p.text);
|
|
6170
|
+
}
|
|
6171
|
+
}
|
|
6172
|
+
if (textBlobs.length === 0) continue;
|
|
6173
|
+
let combined = textBlobs.join("\n\n");
|
|
6174
|
+
if (combined.length > MAX_CHARS_PER_MESSAGE) {
|
|
6175
|
+
combined = combined.slice(0, MAX_CHARS_PER_MESSAGE);
|
|
6176
|
+
}
|
|
6177
|
+
out.push({
|
|
6178
|
+
...m,
|
|
6179
|
+
parts: [{ type: "text", text: combined }]
|
|
6180
|
+
});
|
|
6181
|
+
}
|
|
6182
|
+
return out;
|
|
6183
|
+
};
|
|
6184
|
+
var generateSuggestions = async ({
|
|
6185
|
+
languageModel,
|
|
6186
|
+
messages,
|
|
6187
|
+
agentInstructions
|
|
6188
|
+
}) => {
|
|
6189
|
+
const system = agentInstructions ? `${agentInstructions}
|
|
6190
|
+
|
|
6191
|
+
---
|
|
6192
|
+
|
|
6193
|
+
${SUGGESTIONS_SYSTEM_PROMPT}` : SUGGESTIONS_SYSTEM_PROMPT;
|
|
6194
|
+
const trimmed = trimMessagesForSuggestions(messages);
|
|
6195
|
+
const { toolCalls, totalUsage } = await generateText3({
|
|
6196
|
+
temperature: 0,
|
|
6197
|
+
model: languageModel,
|
|
6198
|
+
system,
|
|
6199
|
+
messages: await convertToModelMessages2(trimmed, {
|
|
6200
|
+
ignoreIncompleteToolCalls: true
|
|
6201
|
+
}),
|
|
6202
|
+
tools: { submit_suggestions: submitSuggestionsTool },
|
|
6203
|
+
toolChoice: { type: "tool", toolName: "submit_suggestions" },
|
|
6204
|
+
maxRetries: 3
|
|
6205
|
+
});
|
|
6206
|
+
const call = toolCalls.find((c) => c.toolName === "submit_suggestions");
|
|
6207
|
+
const input = call?.input;
|
|
6208
|
+
const suggestions = Array.isArray(input?.suggestions) ? input.suggestions.slice(0, 3).map(String) : [];
|
|
6209
|
+
return {
|
|
6210
|
+
suggestions,
|
|
6211
|
+
usage: {
|
|
6212
|
+
inputTokens: totalUsage?.inputTokens ?? 0,
|
|
6213
|
+
outputTokens: totalUsage?.outputTokens ?? 0
|
|
6214
|
+
}
|
|
6215
|
+
};
|
|
6216
|
+
};
|
|
6217
|
+
|
|
6218
|
+
// src/exulu/transcribe.ts
|
|
6219
|
+
var TranscriptionError = class extends Error {
|
|
6220
|
+
constructor(upstreamStatus, message) {
|
|
6221
|
+
super(message);
|
|
6222
|
+
this.upstreamStatus = upstreamStatus;
|
|
6223
|
+
this.name = "TranscriptionError";
|
|
6224
|
+
}
|
|
6225
|
+
};
|
|
6226
|
+
async function transcribeAudio(args) {
|
|
6227
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
6228
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
6229
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
6230
|
+
const model = process.env.TRANSCRIPTION_MODEL;
|
|
6231
|
+
if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
|
|
6232
|
+
if (!model) throw new Error("TRANSCRIPTION_MODEL is not set");
|
|
6233
|
+
const form = new FormData();
|
|
6234
|
+
form.append(
|
|
6235
|
+
"file",
|
|
6236
|
+
new Blob([args.file.buffer], { type: args.file.mimetype }),
|
|
6237
|
+
args.file.originalname
|
|
6238
|
+
);
|
|
6239
|
+
form.append("model", model);
|
|
6240
|
+
if (args.language) form.append("language", args.language);
|
|
6241
|
+
const res = await fetch(`http://${host}:${port}/v1/audio/transcriptions`, {
|
|
6242
|
+
method: "POST",
|
|
6243
|
+
headers: { Authorization: `Bearer ${masterKey}` },
|
|
6244
|
+
body: form
|
|
6245
|
+
});
|
|
6246
|
+
if (!res.ok) {
|
|
6247
|
+
const body = await res.text().catch(() => "");
|
|
6248
|
+
throw new TranscriptionError(
|
|
6249
|
+
res.status,
|
|
6250
|
+
`LiteLLM transcription failed (status ${res.status}): ${body}`.trim()
|
|
6251
|
+
);
|
|
6252
|
+
}
|
|
6253
|
+
const json = await res.json();
|
|
6254
|
+
return { text: typeof json.text === "string" ? json.text : "" };
|
|
6255
|
+
}
|
|
6256
|
+
|
|
6257
|
+
// src/exulu/speech.ts
|
|
6258
|
+
var SpeechError = class extends Error {
|
|
6259
|
+
constructor(upstreamStatus, message) {
|
|
6260
|
+
super(message);
|
|
6261
|
+
this.upstreamStatus = upstreamStatus;
|
|
6262
|
+
this.name = "SpeechError";
|
|
6263
|
+
}
|
|
6264
|
+
};
|
|
6265
|
+
async function synthesizeSpeech(args) {
|
|
6266
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
6267
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
6268
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
6269
|
+
const model = process.env.TTS_MODEL;
|
|
6270
|
+
const voice = process.env.TTS_VOICE;
|
|
6271
|
+
if (!masterKey) throw new Error("LITELLM_MASTER_KEY is not set");
|
|
6272
|
+
if (!model) throw new Error("TTS_MODEL is not set");
|
|
6273
|
+
if (!voice) throw new Error("TTS_VOICE is not set");
|
|
6274
|
+
const body = {
|
|
6275
|
+
model,
|
|
6276
|
+
input: args.text,
|
|
6277
|
+
voice,
|
|
6278
|
+
response_format: "mp3"
|
|
6279
|
+
};
|
|
6280
|
+
const res = await fetch(`http://${host}:${port}/v1/audio/speech`, {
|
|
6281
|
+
method: "POST",
|
|
6282
|
+
headers: {
|
|
6283
|
+
Authorization: `Bearer ${masterKey}`,
|
|
6284
|
+
"Content-Type": "application/json"
|
|
6285
|
+
},
|
|
6286
|
+
body: JSON.stringify(body)
|
|
6287
|
+
});
|
|
6288
|
+
if (!res.ok) {
|
|
6289
|
+
const text = await res.text().catch(() => "");
|
|
6290
|
+
throw new SpeechError(
|
|
6291
|
+
res.status,
|
|
6292
|
+
`LiteLLM speech failed (status ${res.status}): ${text}`.trim()
|
|
6293
|
+
);
|
|
6294
|
+
}
|
|
6295
|
+
const arrayBuf = await res.arrayBuffer();
|
|
6296
|
+
return Buffer.from(arrayBuf);
|
|
6297
|
+
}
|
|
6298
|
+
|
|
6299
|
+
// src/exulu/routes.ts
|
|
6300
|
+
import multer from "multer";
|
|
6301
|
+
|
|
6194
6302
|
// src/utils/check-provider-rate-limit.ts
|
|
6195
6303
|
var checkProviderRateLimit = async (provider) => {
|
|
6196
6304
|
if (provider.rateLimit) {
|
|
@@ -6321,7 +6429,7 @@ async function recordAgentTokenUsage(args) {
|
|
|
6321
6429
|
import "express";
|
|
6322
6430
|
import {
|
|
6323
6431
|
streamText as streamText2,
|
|
6324
|
-
generateText as
|
|
6432
|
+
generateText as generateText4,
|
|
6325
6433
|
stepCountIs as stepCountIs2,
|
|
6326
6434
|
jsonSchema
|
|
6327
6435
|
} from "ai";
|
|
@@ -6412,7 +6520,7 @@ function transformCompletion(text, inputTokens, outputTokens, ctx) {
|
|
|
6412
6520
|
|
|
6413
6521
|
// src/exulu/openai-gateway.ts
|
|
6414
6522
|
import { randomUUID } from "crypto";
|
|
6415
|
-
import
|
|
6523
|
+
import "crypto-js";
|
|
6416
6524
|
import express from "express";
|
|
6417
6525
|
function convertOpenAIToolsToAiSdkTools(tools) {
|
|
6418
6526
|
return Object.fromEntries(
|
|
@@ -6698,41 +6806,33 @@ var registerOpenAIGatewayRoutes = async (app, providers, tools, contexts, config
|
|
|
6698
6806
|
res.status(500).json({ error: { message: "Server configuration error", type: "server_error" } });
|
|
6699
6807
|
return;
|
|
6700
6808
|
}
|
|
6701
|
-
if (!agent.
|
|
6702
|
-
res.status(400).json({
|
|
6703
|
-
error: { message: "Agent has no API key configured", type: "invalid_request_error" }
|
|
6704
|
-
});
|
|
6705
|
-
return;
|
|
6706
|
-
}
|
|
6707
|
-
const variable = await db.from("variables").where({ name: agent.providerapikey }).first();
|
|
6708
|
-
if (!variable) {
|
|
6709
|
-
res.status(400).json({
|
|
6710
|
-
error: { message: "API key variable not found", type: "invalid_request_error" }
|
|
6711
|
-
});
|
|
6712
|
-
return;
|
|
6713
|
-
}
|
|
6714
|
-
if (!variable.encrypted) {
|
|
6809
|
+
if (!agent.model) {
|
|
6715
6810
|
res.status(400).json({
|
|
6716
|
-
error: { message: "
|
|
6811
|
+
error: { message: "Agent has no model configured", type: "invalid_request_error" }
|
|
6717
6812
|
});
|
|
6718
6813
|
return;
|
|
6719
6814
|
}
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6815
|
+
let resolved;
|
|
6816
|
+
try {
|
|
6817
|
+
resolved = await resolveModel({
|
|
6818
|
+
modelId: agent.model,
|
|
6819
|
+
user,
|
|
6820
|
+
providers,
|
|
6821
|
+
agent: { id: agent.id },
|
|
6822
|
+
project: project ? { id: project.id } : void 0
|
|
6726
6823
|
});
|
|
6727
|
-
|
|
6824
|
+
} catch (err) {
|
|
6825
|
+
if (err instanceof ResolveModelError) {
|
|
6826
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
6827
|
+
res.status(status).json({
|
|
6828
|
+
error: { message: err.message, type: "invalid_request_error", code: err.code }
|
|
6829
|
+
});
|
|
6830
|
+
return;
|
|
6831
|
+
}
|
|
6832
|
+
throw err;
|
|
6728
6833
|
}
|
|
6729
|
-
const
|
|
6730
|
-
|
|
6731
|
-
user: user.id,
|
|
6732
|
-
role: user.role?.id,
|
|
6733
|
-
project: project?.id,
|
|
6734
|
-
agent: agent.id
|
|
6735
|
-
});
|
|
6834
|
+
const providerapikey = resolved.apiKey;
|
|
6835
|
+
const languageModel = resolved.languageModel;
|
|
6736
6836
|
const disabledTools = req.body.disabledTools ?? [];
|
|
6737
6837
|
const enabledTools = await getEnabledTools(
|
|
6738
6838
|
agent,
|
|
@@ -6821,7 +6921,7 @@ ${project.description}` : ""}` : "",
|
|
|
6821
6921
|
const usage = await result.usage;
|
|
6822
6922
|
await writeStatistics(agent, project, user, usage.inputTokens ?? 0, usage.outputTokens ?? 0);
|
|
6823
6923
|
} else {
|
|
6824
|
-
const { text, usage } = await
|
|
6924
|
+
const { text, usage } = await generateText4({
|
|
6825
6925
|
model: languageModel,
|
|
6826
6926
|
system: systemPrompt || void 0,
|
|
6827
6927
|
messages: coreMessages,
|
|
@@ -6842,9 +6942,6 @@ ${project.description}` : ""}` : "",
|
|
|
6842
6942
|
);
|
|
6843
6943
|
};
|
|
6844
6944
|
|
|
6845
|
-
// src/exulu/routes.ts
|
|
6846
|
-
import { convertJsonSchemaToZod } from "zod-from-json-schema";
|
|
6847
|
-
|
|
6848
6945
|
// src/utils/enabled-skills.ts
|
|
6849
6946
|
var getEnabledSkills = async (agent, disabledSkills = []) => {
|
|
6850
6947
|
let enabledSkills = [];
|
|
@@ -6862,7 +6959,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
|
|
|
6862
6959
|
var getExuluVersionNumber = async () => {
|
|
6863
6960
|
try {
|
|
6864
6961
|
const path2 = process.cwd();
|
|
6865
|
-
const packageJson =
|
|
6962
|
+
const packageJson = fs3.readFileSync(path2 + "/package.json", "utf8");
|
|
6866
6963
|
const packageData = JSON.parse(packageJson);
|
|
6867
6964
|
const exuluVersion = packageData.dependencies["@exulu/backend"];
|
|
6868
6965
|
console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
|
|
@@ -6885,6 +6982,7 @@ var {
|
|
|
6885
6982
|
platformConfigurationsSchema,
|
|
6886
6983
|
agentSessionsSchema,
|
|
6887
6984
|
agentMessagesSchema,
|
|
6985
|
+
modelsSchema,
|
|
6888
6986
|
rolesSchema,
|
|
6889
6987
|
usersSchema,
|
|
6890
6988
|
skillsSchema,
|
|
@@ -6949,6 +7047,7 @@ var createExpressRoutes = async (app, providers, tools, contexts, config, evals,
|
|
|
6949
7047
|
testCasesSchema(),
|
|
6950
7048
|
agentSessionsSchema(),
|
|
6951
7049
|
agentMessagesSchema(),
|
|
7050
|
+
modelsSchema(),
|
|
6952
7051
|
variablesSchema(),
|
|
6953
7052
|
workflowTemplatesSchema(),
|
|
6954
7053
|
statisticsSchema(),
|
|
@@ -7209,12 +7308,13 @@ Mood: friendly and intelligent.
|
|
|
7209
7308
|
},
|
|
7210
7309
|
redisHost: process.env.REDIS_HOST,
|
|
7211
7310
|
enabled: config?.workers?.enabled
|
|
7311
|
+
},
|
|
7312
|
+
liteLLM: {
|
|
7313
|
+
enabled: process.env.EXULU_USE_LITELLM === "true"
|
|
7212
7314
|
}
|
|
7213
7315
|
});
|
|
7214
7316
|
});
|
|
7215
|
-
|
|
7216
|
-
const slug = provider.slug;
|
|
7217
|
-
if (!slug) return;
|
|
7317
|
+
const registerAgentRunRoute = (slug, provider) => {
|
|
7218
7318
|
app.post(slug + "/:instance", async (req, res) => {
|
|
7219
7319
|
console.log("[EXULU] POST " + slug + "/:instance", req.body);
|
|
7220
7320
|
const headers = {
|
|
@@ -7310,39 +7410,33 @@ Mood: friendly and intelligent.
|
|
|
7310
7410
|
providers,
|
|
7311
7411
|
user
|
|
7312
7412
|
);
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
}
|
|
7334
|
-
providerapikey = variable.value;
|
|
7335
|
-
if (!variable.encrypted) {
|
|
7336
|
-
res.status(400).json({
|
|
7337
|
-
message: "Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
|
|
7338
|
-
});
|
|
7413
|
+
const overrideModelId = req.headers["x-exulu-model-override"];
|
|
7414
|
+
const modelId = overrideModelId ?? agent.model;
|
|
7415
|
+
if (!modelId) {
|
|
7416
|
+
res.status(400).json({
|
|
7417
|
+
message: `Agent ${agent.name} (${agent.id}) has no model configured.`
|
|
7418
|
+
});
|
|
7419
|
+
return;
|
|
7420
|
+
}
|
|
7421
|
+
let resolved;
|
|
7422
|
+
try {
|
|
7423
|
+
resolved = await resolveModel({
|
|
7424
|
+
modelId,
|
|
7425
|
+
user,
|
|
7426
|
+
providers,
|
|
7427
|
+
agent: { id: agent.id }
|
|
7428
|
+
});
|
|
7429
|
+
} catch (err) {
|
|
7430
|
+
if (err instanceof ResolveModelError) {
|
|
7431
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
7432
|
+
res.status(status).json({ message: err.message, code: err.code });
|
|
7339
7433
|
return;
|
|
7340
7434
|
}
|
|
7341
|
-
|
|
7342
|
-
const bytes = CryptoJS5.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
7343
|
-
providerapikey = bytes.toString(CryptoJS5.enc.Utf8);
|
|
7344
|
-
}
|
|
7435
|
+
throw err;
|
|
7345
7436
|
}
|
|
7437
|
+
const providerapikey = resolved.apiKey;
|
|
7438
|
+
const resolvedLanguageModel = resolved.languageModel;
|
|
7439
|
+
const resolvedModelId = resolved.model.id;
|
|
7346
7440
|
if (!!headers.stream) {
|
|
7347
7441
|
const statistics = {
|
|
7348
7442
|
label: agent.name,
|
|
@@ -7374,6 +7468,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
7374
7468
|
currentSkills: enabledSkills,
|
|
7375
7469
|
approvedTools,
|
|
7376
7470
|
allExuluTools: tools,
|
|
7471
|
+
languageModel: resolvedLanguageModel,
|
|
7377
7472
|
providerapikey,
|
|
7378
7473
|
toolConfigs: agent.tools,
|
|
7379
7474
|
exuluConfig: config,
|
|
@@ -7422,7 +7517,8 @@ ${customInstructions}` : agent.instructions;
|
|
|
7422
7517
|
await saveChat({
|
|
7423
7518
|
session: headers.session,
|
|
7424
7519
|
user: user.id,
|
|
7425
|
-
messages
|
|
7520
|
+
messages,
|
|
7521
|
+
model: resolvedModelId
|
|
7426
7522
|
});
|
|
7427
7523
|
clearSessionCurrentTask(headers.session).catch(() => {
|
|
7428
7524
|
});
|
|
@@ -7484,7 +7580,6 @@ ${customInstructions}` : agent.instructions;
|
|
|
7484
7580
|
const response = await provider.generateSync({
|
|
7485
7581
|
contexts,
|
|
7486
7582
|
rerankers: rerankers || [],
|
|
7487
|
-
outputSchema,
|
|
7488
7583
|
agent,
|
|
7489
7584
|
user,
|
|
7490
7585
|
req,
|
|
@@ -7494,6 +7589,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
7494
7589
|
currentTools: enabledTools,
|
|
7495
7590
|
currentSkills: enabledSkills,
|
|
7496
7591
|
allExuluTools: tools,
|
|
7592
|
+
languageModel: resolvedLanguageModel,
|
|
7497
7593
|
providerapikey,
|
|
7498
7594
|
exuluConfig: config,
|
|
7499
7595
|
toolConfigs: agent.tools,
|
|
@@ -7515,289 +7611,414 @@ ${customInstructions}` : agent.instructions;
|
|
|
7515
7611
|
return;
|
|
7516
7612
|
}
|
|
7517
7613
|
});
|
|
7614
|
+
};
|
|
7615
|
+
providers.forEach((provider) => {
|
|
7616
|
+
const slug = provider.slug;
|
|
7617
|
+
if (!slug) return;
|
|
7618
|
+
registerAgentRunRoute(slug, provider);
|
|
7518
7619
|
});
|
|
7519
|
-
if (
|
|
7520
|
-
|
|
7521
|
-
} else {
|
|
7522
|
-
console.log(
|
|
7523
|
-
"[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance."
|
|
7524
|
-
);
|
|
7620
|
+
if (isLiteLLMEnabled() && providers.length > 0) {
|
|
7621
|
+
registerAgentRunRoute("/agents/litellm/run", providers[0]);
|
|
7525
7622
|
}
|
|
7526
|
-
app.
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7623
|
+
app.post("/agents/suggestions/:agentId", async (req, res) => {
|
|
7624
|
+
const agentId = req.params.agentId;
|
|
7625
|
+
if (!agentId) {
|
|
7626
|
+
res.status(400).json({ detail: "Missing agentId" });
|
|
7627
|
+
return;
|
|
7628
|
+
}
|
|
7629
|
+
const agent = await exuluApp.get().agent(agentId);
|
|
7630
|
+
if (!agent) {
|
|
7631
|
+
res.status(404).json({ detail: `Agent ${agentId} not found.` });
|
|
7632
|
+
return;
|
|
7633
|
+
}
|
|
7634
|
+
if (!agent.suggestions_enabled) {
|
|
7635
|
+
res.status(400).json({ detail: "Suggestions are not enabled for this agent." });
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7638
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
7639
|
+
if (!authenticationResult.user?.id && agent.rights_mode !== "public") {
|
|
7640
|
+
res.status(authenticationResult.code || 500).json({ detail: `${authenticationResult.message}` });
|
|
7641
|
+
return;
|
|
7642
|
+
}
|
|
7643
|
+
const user = authenticationResult.user;
|
|
7644
|
+
const scopeCheck = checkApiKeyScope(user, agentId);
|
|
7645
|
+
if (!scopeCheck.allowed) {
|
|
7646
|
+
res.status(scopeCheck.code).json({ detail: scopeCheck.reason });
|
|
7647
|
+
return;
|
|
7648
|
+
}
|
|
7649
|
+
const hasAccessToAgent = await checkRecordAccess(agent, "read", user);
|
|
7650
|
+
if (!hasAccessToAgent) {
|
|
7651
|
+
res.status(401).json({ detail: "You don't have access to this agent." });
|
|
7652
|
+
return;
|
|
7653
|
+
}
|
|
7654
|
+
const callerId = resolveCallerId(req, user?.id);
|
|
7655
|
+
const rateLimitsEnabled = checkLicense()["rate-limits"] === true;
|
|
7656
|
+
const effectiveLimits = rateLimitsEnabled ? agent.rate_limits ?? null : null;
|
|
7657
|
+
const preCheck = await preCheckAgentRateLimit({
|
|
7658
|
+
agentId,
|
|
7659
|
+
callerId,
|
|
7660
|
+
limits: effectiveLimits
|
|
7534
7661
|
});
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
}
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7662
|
+
if (!preCheck.ok) {
|
|
7663
|
+
res.setHeader("Retry-After", String(preCheck.retryAfter));
|
|
7664
|
+
res.status(429).json({
|
|
7665
|
+
detail: `Rate limit exceeded for ${preCheck.metric} on agent ${agent.name}.`,
|
|
7666
|
+
metric: preCheck.metric,
|
|
7667
|
+
retryAfter: preCheck.retryAfter
|
|
7668
|
+
});
|
|
7669
|
+
return;
|
|
7670
|
+
}
|
|
7671
|
+
const messages = Array.isArray(req.body.messages) ? req.body.messages : [];
|
|
7672
|
+
if (messages.length === 0) {
|
|
7673
|
+
res.status(400).json({ detail: "Missing messages in request body." });
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
if (!agent.model) {
|
|
7677
|
+
res.status(400).json({
|
|
7678
|
+
detail: `Agent ${agent.name} (${agent.id}) has no model configured.`
|
|
7679
|
+
});
|
|
7680
|
+
return;
|
|
7681
|
+
}
|
|
7682
|
+
let resolved;
|
|
7683
|
+
try {
|
|
7684
|
+
resolved = await resolveModel({
|
|
7685
|
+
modelId: agent.model,
|
|
7686
|
+
user,
|
|
7687
|
+
providers,
|
|
7688
|
+
agent: { id: agent.id }
|
|
7689
|
+
});
|
|
7690
|
+
} catch (err) {
|
|
7691
|
+
if (err instanceof ResolveModelError) {
|
|
7692
|
+
const status = err.code === "MODEL_FORBIDDEN" ? 403 : 400;
|
|
7693
|
+
res.status(status).json({ detail: err.message, code: err.code });
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
throw err;
|
|
7697
|
+
}
|
|
7698
|
+
try {
|
|
7699
|
+
const { suggestions, usage } = await generateSuggestions({
|
|
7700
|
+
languageModel: resolved.languageModel,
|
|
7701
|
+
messages,
|
|
7702
|
+
agentInstructions: agent.instructions
|
|
7703
|
+
});
|
|
7704
|
+
await Promise.all([
|
|
7705
|
+
updateStatistic({
|
|
7706
|
+
name: "count",
|
|
7707
|
+
label: agent.name,
|
|
7708
|
+
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7709
|
+
trigger: "agent",
|
|
7710
|
+
count: 1,
|
|
7711
|
+
user: user?.id,
|
|
7712
|
+
role: user?.role?.id
|
|
7713
|
+
}),
|
|
7714
|
+
...usage.inputTokens ? [
|
|
7715
|
+
updateStatistic({
|
|
7716
|
+
name: "inputTokens",
|
|
7717
|
+
label: agent.name,
|
|
7718
|
+
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7719
|
+
trigger: "agent",
|
|
7720
|
+
count: usage.inputTokens,
|
|
7721
|
+
user: user?.id,
|
|
7722
|
+
role: user?.role?.id
|
|
7723
|
+
})
|
|
7724
|
+
] : [],
|
|
7725
|
+
...usage.outputTokens ? [
|
|
7726
|
+
updateStatistic({
|
|
7727
|
+
name: "outputTokens",
|
|
7728
|
+
label: agent.name,
|
|
7729
|
+
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7730
|
+
trigger: "agent",
|
|
7731
|
+
count: usage.outputTokens
|
|
7732
|
+
})
|
|
7733
|
+
] : [],
|
|
7734
|
+
recordAgentTokenUsage({
|
|
7735
|
+
agentId,
|
|
7736
|
+
callerId,
|
|
7737
|
+
limits: effectiveLimits,
|
|
7738
|
+
inputTokens: usage.inputTokens,
|
|
7739
|
+
outputTokens: usage.outputTokens
|
|
7740
|
+
})
|
|
7741
|
+
]);
|
|
7742
|
+
res.status(200).json({ suggestions });
|
|
7743
|
+
} catch (err) {
|
|
7744
|
+
console.error("[EXULU] suggestions generation failed", err);
|
|
7745
|
+
res.status(500).json({
|
|
7746
|
+
detail: err instanceof Error ? err.message : "Failed to generate suggestions."
|
|
7747
|
+
});
|
|
7748
|
+
}
|
|
7749
|
+
});
|
|
7750
|
+
const MAX_TRANSCRIBE_BYTES = 25 * 1024 * 1024;
|
|
7751
|
+
const transcribeUpload = multer({
|
|
7752
|
+
storage: multer.memoryStorage(),
|
|
7753
|
+
limits: { fileSize: MAX_TRANSCRIBE_BYTES }
|
|
7754
|
+
});
|
|
7755
|
+
app.post(
|
|
7756
|
+
"/transcribe",
|
|
7757
|
+
(req, res, next) => {
|
|
7758
|
+
transcribeUpload.single("file")(req, res, (err) => {
|
|
7759
|
+
if (!err) return next();
|
|
7760
|
+
const code = err?.code;
|
|
7761
|
+
if (code === "LIMIT_FILE_SIZE") {
|
|
7762
|
+
res.status(413).json({ detail: "Recording too large. Please record a shorter clip." });
|
|
7594
7763
|
return;
|
|
7595
7764
|
}
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7765
|
+
res.status(400).json({ detail: err instanceof Error ? err.message : "Upload failed." });
|
|
7766
|
+
});
|
|
7767
|
+
},
|
|
7768
|
+
async (req, res) => {
|
|
7769
|
+
if (!isLiteLLMEnabled() || !process.env.TRANSCRIPTION_MODEL) {
|
|
7770
|
+
res.status(503).json({
|
|
7771
|
+
detail: "Speech-to-text is not enabled on this deployment. Set EXULU_USE_LITELLM=true and TRANSCRIPTION_MODEL in the environment."
|
|
7772
|
+
});
|
|
7773
|
+
return;
|
|
7774
|
+
}
|
|
7775
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
7776
|
+
if (!authenticationResult.user?.id) {
|
|
7777
|
+
res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
|
|
7778
|
+
return;
|
|
7779
|
+
}
|
|
7780
|
+
const file = req.file;
|
|
7781
|
+
if (!file) {
|
|
7782
|
+
res.status(400).json({ detail: "No audio file provided in 'file' field." });
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7785
|
+
if (!file.mimetype.startsWith("audio/")) {
|
|
7786
|
+
res.status(400).json({
|
|
7787
|
+
detail: `Unsupported mimetype: ${file.mimetype}. Expected audio/*.`
|
|
7788
|
+
});
|
|
7789
|
+
return;
|
|
7790
|
+
}
|
|
7791
|
+
try {
|
|
7792
|
+
await Promise.race([
|
|
7793
|
+
waitForLiteLLMReady(),
|
|
7794
|
+
new Promise(
|
|
7795
|
+
(_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
|
|
7796
|
+
)
|
|
7797
|
+
]);
|
|
7798
|
+
} catch {
|
|
7799
|
+
res.status(503).json({ detail: "Transcription service is not ready. Try again shortly." });
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
const language = typeof req.body?.language === "string" && /^[a-z]{2}$/.test(req.body.language) ? req.body.language : void 0;
|
|
7803
|
+
try {
|
|
7804
|
+
const { text } = await transcribeAudio({ file, language });
|
|
7805
|
+
res.status(200).json({ text });
|
|
7806
|
+
} catch (err) {
|
|
7807
|
+
if (err instanceof TranscriptionError) {
|
|
7808
|
+
const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
|
|
7809
|
+
res.status(code).json({ detail: err.message });
|
|
7600
7810
|
return;
|
|
7601
7811
|
}
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7812
|
+
console.error("[EXULU] /transcribe failed", err);
|
|
7813
|
+
res.status(500).json({
|
|
7814
|
+
detail: err instanceof Error ? err.message : "Transcription failed."
|
|
7815
|
+
});
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
);
|
|
7819
|
+
const MAX_TTS_INPUT_CHARS = 4e3;
|
|
7820
|
+
app.post(
|
|
7821
|
+
"/speech",
|
|
7822
|
+
bodyParser.json({ limit: "64kb" }),
|
|
7823
|
+
async (req, res) => {
|
|
7824
|
+
if (!isLiteLLMEnabled() || !process.env.TTS_MODEL || !process.env.TTS_VOICE) {
|
|
7825
|
+
res.status(503).json({
|
|
7826
|
+
detail: "Text-to-speech is not enabled on this deployment. Set EXULU_USE_LITELLM=true, TTS_MODEL, and TTS_VOICE in the environment."
|
|
7827
|
+
});
|
|
7828
|
+
return;
|
|
7829
|
+
}
|
|
7830
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
7831
|
+
if (!authenticationResult.user?.id) {
|
|
7832
|
+
res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
|
|
7833
|
+
return;
|
|
7834
|
+
}
|
|
7835
|
+
const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
|
|
7836
|
+
if (!text) {
|
|
7837
|
+
res.status(400).json({ detail: "Missing 'text' in request body." });
|
|
7838
|
+
return;
|
|
7839
|
+
}
|
|
7840
|
+
if (text.length > MAX_TTS_INPUT_CHARS) {
|
|
7841
|
+
res.status(400).json({
|
|
7842
|
+
detail: `Text too long (${text.length} chars). Max ${MAX_TTS_INPUT_CHARS}.`
|
|
7843
|
+
});
|
|
7844
|
+
return;
|
|
7845
|
+
}
|
|
7846
|
+
try {
|
|
7847
|
+
await Promise.race([
|
|
7848
|
+
waitForLiteLLMReady(),
|
|
7849
|
+
new Promise(
|
|
7850
|
+
(_, reject) => setTimeout(() => reject(new Error("LiteLLM not ready")), 5e3)
|
|
7851
|
+
)
|
|
7852
|
+
]);
|
|
7853
|
+
} catch {
|
|
7854
|
+
res.status(503).json({ detail: "Speech service is not ready. Try again shortly." });
|
|
7855
|
+
return;
|
|
7856
|
+
}
|
|
7857
|
+
try {
|
|
7858
|
+
const audio = await synthesizeSpeech({ text });
|
|
7859
|
+
res.status(200);
|
|
7860
|
+
res.setHeader("Content-Type", "audio/mpeg");
|
|
7861
|
+
res.setHeader("Content-Length", String(audio.length));
|
|
7862
|
+
res.setHeader("Cache-Control", "no-store");
|
|
7863
|
+
res.send(audio);
|
|
7864
|
+
} catch (err) {
|
|
7865
|
+
if (err instanceof SpeechError) {
|
|
7866
|
+
const code = err.upstreamStatus >= 500 ? 502 : err.upstreamStatus;
|
|
7867
|
+
res.status(code).json({ detail: err.message });
|
|
7610
7868
|
return;
|
|
7611
7869
|
}
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7870
|
+
console.error("[EXULU] /speech failed", err);
|
|
7871
|
+
res.status(500).json({
|
|
7872
|
+
detail: err instanceof Error ? err.message : "Speech generation failed."
|
|
7873
|
+
});
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
);
|
|
7877
|
+
app.use("/litellm/:project", async (req, res) => {
|
|
7878
|
+
if (!isLiteLLMEnabled()) {
|
|
7879
|
+
res.status(503).json({
|
|
7880
|
+
detail: "LiteLLM is not enabled on this deployment. Set EXULU_USE_LITELLM=true."
|
|
7881
|
+
});
|
|
7882
|
+
return;
|
|
7883
|
+
}
|
|
7884
|
+
const masterKey = process.env.LITELLM_MASTER_KEY;
|
|
7885
|
+
if (!masterKey) {
|
|
7886
|
+
res.status(503).json({ detail: "LITELLM_MASTER_KEY is not configured." });
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7889
|
+
const authenticationResult = await requestValidators.authenticate(req);
|
|
7890
|
+
if (!authenticationResult.user?.id) {
|
|
7891
|
+
console.log("[EXULU] /litellm failed authentication", authenticationResult);
|
|
7892
|
+
res.status(authenticationResult.code || 401).json({ detail: authenticationResult.message });
|
|
7893
|
+
return;
|
|
7894
|
+
}
|
|
7895
|
+
const user = authenticationResult.user;
|
|
7896
|
+
if (!req.path.startsWith("/v1/")) {
|
|
7897
|
+
res.status(403).json({
|
|
7898
|
+
detail: `Path ${req.path} is not exposed through the Exulu LiteLLM proxy.`
|
|
7899
|
+
});
|
|
7900
|
+
return;
|
|
7901
|
+
}
|
|
7902
|
+
let project = null;
|
|
7903
|
+
if (req.params.project && req.params.project !== "DEFAULT") {
|
|
7904
|
+
const { db } = await postgresClient();
|
|
7905
|
+
let projectQuery = db("projects");
|
|
7906
|
+
projectQuery.select("*");
|
|
7907
|
+
projectQuery = applyAccessControl(
|
|
7908
|
+
projectsSchema(),
|
|
7909
|
+
projectQuery,
|
|
7910
|
+
authenticationResult.user
|
|
7911
|
+
);
|
|
7912
|
+
projectQuery.where({ id: req.params.project });
|
|
7913
|
+
project = await projectQuery.first();
|
|
7914
|
+
if (!project) {
|
|
7915
|
+
res.status(404).json({
|
|
7916
|
+
detail: "Project not found or you do not have access to it."
|
|
7917
|
+
});
|
|
7918
|
+
return;
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
const host = process.env.LITELLM_HOST ?? "127.0.0.1";
|
|
7922
|
+
const port = process.env.LITELLM_PORT ?? "4000";
|
|
7923
|
+
const upstreamUrl = `http://${host}:${port}${req.url}`;
|
|
7924
|
+
const upstreamHeaders = {};
|
|
7925
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
7926
|
+
if (value === void 0) continue;
|
|
7927
|
+
const lower = name.toLowerCase();
|
|
7928
|
+
if (lower === "authorization" || lower === "host" || lower === "content-length" || lower === "connection" || lower === "transfer-encoding" || lower === "accept-encoding")
|
|
7929
|
+
continue;
|
|
7930
|
+
upstreamHeaders[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
7931
|
+
}
|
|
7932
|
+
upstreamHeaders["authorization"] = `Bearer ${masterKey}`;
|
|
7933
|
+
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
7934
|
+
let body;
|
|
7935
|
+
if (methodHasBody && req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
|
|
7936
|
+
body = JSON.stringify(req.body);
|
|
7937
|
+
upstreamHeaders["content-type"] = "application/json";
|
|
7938
|
+
}
|
|
7939
|
+
const tags = buildTags({
|
|
7940
|
+
user_id: user.id,
|
|
7941
|
+
role_id: user.role?.id,
|
|
7942
|
+
project_id: project?.id,
|
|
7943
|
+
user_name: user.email,
|
|
7944
|
+
role_name: user.role?.name,
|
|
7945
|
+
project_name: project?.name
|
|
7946
|
+
});
|
|
7947
|
+
if (tags?.length) {
|
|
7948
|
+
upstreamHeaders["x-litellm-tags"] = tags.join(",");
|
|
7949
|
+
upstreamHeaders["x-litellm-spend-logs-metadata"] = JSON.stringify({
|
|
7950
|
+
user_id: user.id,
|
|
7951
|
+
role_id: user.role?.id,
|
|
7952
|
+
project_id: project?.id || "DEFAULT",
|
|
7953
|
+
user_name: user.email,
|
|
7954
|
+
role_name: user.role?.name,
|
|
7955
|
+
project_name: project?.name
|
|
7956
|
+
});
|
|
7957
|
+
}
|
|
7958
|
+
console.log("[EXULU] Built tags", tags);
|
|
7959
|
+
const taggedFetch = createTaggedFetch(tags);
|
|
7960
|
+
try {
|
|
7961
|
+
const upstream = await taggedFetch(upstreamUrl, {
|
|
7962
|
+
method: req.method,
|
|
7963
|
+
headers: upstreamHeaders,
|
|
7964
|
+
body
|
|
7965
|
+
});
|
|
7966
|
+
res.status(upstream.status);
|
|
7967
|
+
upstream.headers.forEach((value, name) => {
|
|
7968
|
+
const lower = name.toLowerCase();
|
|
7969
|
+
if (lower === "content-encoding" || lower === "content-length" || lower === "transfer-encoding" || lower === "connection")
|
|
7619
7970
|
return;
|
|
7971
|
+
res.setHeader(name, value);
|
|
7972
|
+
});
|
|
7973
|
+
if (!upstream.body) {
|
|
7974
|
+
res.end();
|
|
7975
|
+
return;
|
|
7976
|
+
}
|
|
7977
|
+
const reader = upstream.body.getReader();
|
|
7978
|
+
try {
|
|
7979
|
+
while (true) {
|
|
7980
|
+
const { done, value } = await reader.read();
|
|
7981
|
+
if (done) break;
|
|
7982
|
+
if (value) res.write(value);
|
|
7620
7983
|
}
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
if (req.headers["accept"]) headers["accept"] = req.headers["accept"];
|
|
7631
|
-
if (req.headers["user-agent"]) headers["user-agent"] = req.headers["user-agent"];
|
|
7632
|
-
const client2 = new Anthropic({
|
|
7633
|
-
apiKey: anthropicApiKey
|
|
7984
|
+
} finally {
|
|
7985
|
+
reader.releaseLock();
|
|
7986
|
+
}
|
|
7987
|
+
res.end();
|
|
7988
|
+
} catch (err) {
|
|
7989
|
+
console.error("[EXULU] /litellm proxy failed", err);
|
|
7990
|
+
if (!res.headersSent) {
|
|
7991
|
+
res.status(502).json({
|
|
7992
|
+
detail: err instanceof Error ? err.message : "LiteLLM proxy failed."
|
|
7634
7993
|
});
|
|
7635
|
-
|
|
7636
|
-
const disabledTools = req.body.disabledTools ? req.body.disabledTools : [];
|
|
7637
|
-
let enabledTools = await getEnabledTools(
|
|
7638
|
-
agent,
|
|
7639
|
-
tools,
|
|
7640
|
-
contexts || [],
|
|
7641
|
-
rerankers || [],
|
|
7642
|
-
disabledTools,
|
|
7643
|
-
providers,
|
|
7644
|
-
user
|
|
7645
|
-
);
|
|
7646
|
-
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
7647
|
-
const agentInstructions = customInstructions ? `${agent?.instructions}
|
|
7648
|
-
|
|
7649
|
-
${customInstructions}` : agent?.instructions;
|
|
7650
|
-
let system = req.body.system;
|
|
7651
|
-
if (Array.isArray(req.body.system)) {
|
|
7652
|
-
system = [
|
|
7653
|
-
...req.body.system,
|
|
7654
|
-
...agent ? [
|
|
7655
|
-
{
|
|
7656
|
-
type: "text",
|
|
7657
|
-
text: `
|
|
7658
|
-
You are an agent named: ${agent?.name}
|
|
7659
|
-
Here are some additional instructions for you: ${agentInstructions}`
|
|
7660
|
-
}
|
|
7661
|
-
] : [],
|
|
7662
|
-
...project ? [
|
|
7663
|
-
{
|
|
7664
|
-
type: "text",
|
|
7665
|
-
text: `Additional information:
|
|
7666
|
-
|
|
7667
|
-
The project you are working on is: ${project?.name}
|
|
7668
|
-
Here is some additional information about the project: ${project?.description}`
|
|
7669
|
-
}
|
|
7670
|
-
] : []
|
|
7671
|
-
];
|
|
7672
|
-
} else {
|
|
7673
|
-
system = `${req.body.system}
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
${agent ? `You are an agent named: ${agent?.name}
|
|
7677
|
-
Here are some additional instructions for you: ${agentInstructions}` : ""}
|
|
7678
|
-
|
|
7679
|
-
${project?.id ? `Additional information:
|
|
7680
|
-
|
|
7681
|
-
The project you are working on is: ${project?.name}
|
|
7682
|
-
The project description is: ${project?.description}` : ""}
|
|
7683
|
-
`;
|
|
7684
|
-
}
|
|
7685
|
-
for await (const event of client2.messages.stream({
|
|
7686
|
-
...req.body,
|
|
7687
|
-
system
|
|
7688
|
-
})) {
|
|
7689
|
-
if (event.message?.id) {
|
|
7690
|
-
tokens[event.message.id] = {
|
|
7691
|
-
input_tokens: event.message.usage.input_tokens,
|
|
7692
|
-
cache_creation_input_tokens: event.message.usage.cache_creation_input_tokens,
|
|
7693
|
-
cache_read_input_tokens: event.message.usage.cache_read_input_tokens,
|
|
7694
|
-
output_tokens: event.message.usage.output_tokens
|
|
7695
|
-
};
|
|
7696
|
-
}
|
|
7697
|
-
if (event.message?.type === "tool_use" && event.message?.name?.includes("exulu_")) {
|
|
7698
|
-
const toolName = event.message?.name;
|
|
7699
|
-
console.log("[EXULU] Using tool", toolName);
|
|
7700
|
-
const inputs = event.message?.input;
|
|
7701
|
-
const id = event.message?.id;
|
|
7702
|
-
const tool = enabledTools.find(
|
|
7703
|
-
(tool2) => tool2.id === toolName.replace("exulu_", "")
|
|
7704
|
-
);
|
|
7705
|
-
if (!tool || !tool.tool.execute) {
|
|
7706
|
-
console.error("[EXULU] Tool not found or not enabled.", toolName);
|
|
7707
|
-
continue;
|
|
7708
|
-
}
|
|
7709
|
-
const toolResult = await tool.tool.execute(inputs, {
|
|
7710
|
-
toolCallId: id,
|
|
7711
|
-
messages: [
|
|
7712
|
-
{
|
|
7713
|
-
...event.message,
|
|
7714
|
-
role: "tool"
|
|
7715
|
-
}
|
|
7716
|
-
]
|
|
7717
|
-
});
|
|
7718
|
-
console.log("[EXULU] Tool result", toolResult);
|
|
7719
|
-
const toolResultMessage = {
|
|
7720
|
-
role: "user",
|
|
7721
|
-
content: [
|
|
7722
|
-
{
|
|
7723
|
-
type: "tool_result",
|
|
7724
|
-
tool_use_id: id,
|
|
7725
|
-
content: toolResult
|
|
7726
|
-
}
|
|
7727
|
-
]
|
|
7728
|
-
};
|
|
7729
|
-
res.write(`event: tool_result
|
|
7730
|
-
data: ${JSON.stringify(toolResultMessage)}
|
|
7731
|
-
|
|
7732
|
-
`);
|
|
7733
|
-
} else {
|
|
7734
|
-
const msg = `event: ${event.type}
|
|
7735
|
-
data: ${JSON.stringify(event)}
|
|
7736
|
-
|
|
7737
|
-
`;
|
|
7738
|
-
res.write(msg);
|
|
7739
|
-
}
|
|
7740
|
-
}
|
|
7741
|
-
let totalInputTokens = 0;
|
|
7742
|
-
let totalOutputTokens = 0;
|
|
7743
|
-
for (const token of Object.values(tokens)) {
|
|
7744
|
-
totalInputTokens += token.input_tokens;
|
|
7745
|
-
totalOutputTokens += token.output_tokens;
|
|
7746
|
-
}
|
|
7747
|
-
const statistics = {
|
|
7748
|
-
label: agent.name,
|
|
7749
|
-
trigger: "agent"
|
|
7750
|
-
};
|
|
7751
|
-
await Promise.all([
|
|
7752
|
-
updateStatistic({
|
|
7753
|
-
name: "count",
|
|
7754
|
-
label: statistics.label,
|
|
7755
|
-
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7756
|
-
trigger: statistics.trigger,
|
|
7757
|
-
count: 1,
|
|
7758
|
-
user: user.id,
|
|
7759
|
-
role: user?.role?.id,
|
|
7760
|
-
...project ? { project: project.id } : {}
|
|
7761
|
-
}),
|
|
7762
|
-
...totalInputTokens ? [
|
|
7763
|
-
updateStatistic({
|
|
7764
|
-
name: "inputTokens",
|
|
7765
|
-
label: statistics.label,
|
|
7766
|
-
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7767
|
-
trigger: statistics.trigger,
|
|
7768
|
-
count: totalInputTokens,
|
|
7769
|
-
user: user.id,
|
|
7770
|
-
role: user?.role?.id,
|
|
7771
|
-
...project ? { project: project.id } : {}
|
|
7772
|
-
})
|
|
7773
|
-
] : [],
|
|
7774
|
-
...totalOutputTokens ? [
|
|
7775
|
-
updateStatistic({
|
|
7776
|
-
name: "outputTokens",
|
|
7777
|
-
label: statistics.label,
|
|
7778
|
-
type: STATISTICS_TYPE_ENUM.AGENT_RUN,
|
|
7779
|
-
trigger: statistics.trigger,
|
|
7780
|
-
count: totalInputTokens,
|
|
7781
|
-
user: user.id,
|
|
7782
|
-
role: user?.role?.id,
|
|
7783
|
-
...project ? { project: project.id } : {}
|
|
7784
|
-
})
|
|
7785
|
-
] : []
|
|
7786
|
-
]);
|
|
7787
|
-
res.write("event: done\ndata: [DONE]\n\n");
|
|
7994
|
+
} else {
|
|
7788
7995
|
res.end();
|
|
7789
|
-
} catch (error) {
|
|
7790
|
-
console.error("[PROXY] Manual proxy error:", error);
|
|
7791
|
-
if (!res.headersSent) {
|
|
7792
|
-
if (error?.message === "Invalid token") {
|
|
7793
|
-
res.status(500).json({ error: "Authentication error, please check your IMP token and try again." });
|
|
7794
|
-
} else {
|
|
7795
|
-
res.status(500).json({ error: error.message });
|
|
7796
|
-
}
|
|
7797
|
-
}
|
|
7798
7996
|
}
|
|
7799
7997
|
}
|
|
7800
|
-
);
|
|
7998
|
+
});
|
|
7999
|
+
if (config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
|
|
8000
|
+
await createUppyRoutes(app, config);
|
|
8001
|
+
} else {
|
|
8002
|
+
console.log(
|
|
8003
|
+
"[EXULU] skipping uppy file upload routes, because no S3 compatible region, key or secret is set in ExuluApp instance."
|
|
8004
|
+
);
|
|
8005
|
+
}
|
|
8006
|
+
app.get("/config", async (req, res) => {
|
|
8007
|
+
res.status(200).json({
|
|
8008
|
+
message: "Config fetched successfully.",
|
|
8009
|
+
config: {
|
|
8010
|
+
workers: {
|
|
8011
|
+
enabled: config?.workers?.enabled || false
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
});
|
|
8015
|
+
});
|
|
8016
|
+
app.use("/gateway/anthropic/:agent/:project", (req, res) => {
|
|
8017
|
+
console.warn("[EXULU] DEPRECATED: Received a request to /gateway/anthropic/:agent/:project, this is a deprecated path and will be removed in a future version. Please use /litellm/:project instead.");
|
|
8018
|
+
const suffix = req.url;
|
|
8019
|
+
req.url = `/litellm/${encodeURIComponent(req.params.project)}${suffix}`;
|
|
8020
|
+
app.handle(req, res);
|
|
8021
|
+
});
|
|
7801
8022
|
function buildFileTree(files, stripPrefix) {
|
|
7802
8023
|
const root = { name: "/", path: "/", key: "", type: "folder", children: [] };
|
|
7803
8024
|
for (const file of files) {
|
|
@@ -8513,13 +8734,139 @@ data: ${JSON.stringify(event)}
|
|
|
8513
8734
|
}
|
|
8514
8735
|
}
|
|
8515
8736
|
);
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
};
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8737
|
+
async function resolveTargetUser(req, res, opts) {
|
|
8738
|
+
const authResult = await requestValidators.authenticate(req);
|
|
8739
|
+
if (!authResult.user?.id) {
|
|
8740
|
+
res.status(authResult.code ?? 401).json({ detail: authResult.message });
|
|
8741
|
+
return null;
|
|
8742
|
+
}
|
|
8743
|
+
if (!authResult.user.super_admin) {
|
|
8744
|
+
res.status(403).json({ detail: "Super admin access required." });
|
|
8745
|
+
return null;
|
|
8746
|
+
}
|
|
8747
|
+
const targetId = Number.parseInt(req.params.id ?? "", 10);
|
|
8748
|
+
if (!Number.isFinite(targetId)) {
|
|
8749
|
+
res.status(400).json({ detail: "Invalid user id." });
|
|
8750
|
+
return null;
|
|
8751
|
+
}
|
|
8752
|
+
if (!opts.allowSelf && targetId === authResult.user.id) {
|
|
8753
|
+
res.status(403).json({ detail: "Super admins cannot delete their own account." });
|
|
8754
|
+
return null;
|
|
8755
|
+
}
|
|
8756
|
+
const { db } = await postgresClient();
|
|
8757
|
+
const targetUser = await db.from("users").where({ id: targetId }).first();
|
|
8758
|
+
if (!targetUser) {
|
|
8759
|
+
res.status(404).json({ detail: "User not found." });
|
|
8760
|
+
return null;
|
|
8761
|
+
}
|
|
8762
|
+
return { targetUser, db };
|
|
8763
|
+
}
|
|
8764
|
+
app.get("/users/:id/data-export", async (req, res) => {
|
|
8765
|
+
const resolved = await resolveTargetUser(req, res, { allowSelf: true });
|
|
8766
|
+
if (!resolved) return;
|
|
8767
|
+
const { targetUser, db } = resolved;
|
|
8768
|
+
try {
|
|
8769
|
+
const role = targetUser.role ? await db.from("roles").where({ id: targetUser.role }).first() : null;
|
|
8770
|
+
const userExport = { ...targetUser, role: role ?? targetUser.role };
|
|
8771
|
+
delete userExport.password;
|
|
8772
|
+
delete userExport.apikey;
|
|
8773
|
+
delete userExport.temporary_token;
|
|
8774
|
+
delete userExport.anthropic_token;
|
|
8775
|
+
const sessions = await db.from("agent_sessions").where({ user: targetUser.id });
|
|
8776
|
+
const sessionIds = sessions.map((s) => s.id);
|
|
8777
|
+
const messages = sessionIds.length ? await db.from("agent_messages").whereIn("session", sessionIds).orderBy("createdAt", "asc") : [];
|
|
8778
|
+
const messagesBySession = /* @__PURE__ */ new Map();
|
|
8779
|
+
for (const m of messages) {
|
|
8780
|
+
const list = messagesBySession.get(m.session) ?? [];
|
|
8781
|
+
list.push(m);
|
|
8782
|
+
messagesBySession.set(m.session, list);
|
|
8783
|
+
}
|
|
8784
|
+
const sessionsWithMessages = sessions.map((s) => ({
|
|
8785
|
+
...s,
|
|
8786
|
+
messages: messagesBySession.get(s.id) ?? []
|
|
8787
|
+
}));
|
|
8788
|
+
const [feedback, promptFavorites, tracking] = await Promise.all([
|
|
8789
|
+
db.from("feedback").where({ user: targetUser.id }).catch(() => []),
|
|
8790
|
+
db.from("prompt_favorites").where({ user_id: targetUser.id }).catch(() => []),
|
|
8791
|
+
db.from("tracking").where({ user: targetUser.id }).catch(() => [])
|
|
8792
|
+
]);
|
|
8793
|
+
const exportedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8794
|
+
const readme = [
|
|
8795
|
+
`Exulu user data export`,
|
|
8796
|
+
``,
|
|
8797
|
+
`User id: ${targetUser.id}`,
|
|
8798
|
+
`Exported at: ${exportedAt}`,
|
|
8799
|
+
``,
|
|
8800
|
+
`This archive contains the personal data Exulu holds for the user`,
|
|
8801
|
+
`referenced above, provided in fulfilment of DSGVO Art. 15`,
|
|
8802
|
+
`(Recht auf Auskunft / GDPR right of access).`,
|
|
8803
|
+
``,
|
|
8804
|
+
`Files:`,
|
|
8805
|
+
` - user_data.json account row (password/api-key hashes stripped)`,
|
|
8806
|
+
` - sessions.json chat sessions with messages inlined`,
|
|
8807
|
+
` - feedback.json feedback the user submitted`,
|
|
8808
|
+
` - prompt_favorites.json prompt-library favourites`,
|
|
8809
|
+
` - tracking.json tracking events linked to the user`,
|
|
8810
|
+
``
|
|
8811
|
+
].join("\n");
|
|
8812
|
+
const zip = new JSZip2();
|
|
8813
|
+
zip.file("README.txt", readme);
|
|
8814
|
+
zip.file("user_data.json", JSON.stringify(userExport, null, 2));
|
|
8815
|
+
zip.file("sessions.json", JSON.stringify(sessionsWithMessages, null, 2));
|
|
8816
|
+
zip.file("feedback.json", JSON.stringify(feedback, null, 2));
|
|
8817
|
+
zip.file(
|
|
8818
|
+
"prompt_favorites.json",
|
|
8819
|
+
JSON.stringify(promptFavorites, null, 2)
|
|
8820
|
+
);
|
|
8821
|
+
zip.file("tracking.json", JSON.stringify(tracking, null, 2));
|
|
8822
|
+
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
8823
|
+
const filename = `user-${targetUser.id}-export-${exportedAt.replace(/[:.]/g, "-")}.zip`;
|
|
8824
|
+
res.setHeader("Content-Type", "application/zip");
|
|
8825
|
+
res.setHeader(
|
|
8826
|
+
"Content-Disposition",
|
|
8827
|
+
`attachment; filename="${filename}"`
|
|
8828
|
+
);
|
|
8829
|
+
res.send(buffer);
|
|
8830
|
+
} catch (err) {
|
|
8831
|
+
console.error("[GDPR] Failed to build data export", err);
|
|
8832
|
+
res.status(500).json({ detail: "Failed to build data export." });
|
|
8833
|
+
}
|
|
8834
|
+
});
|
|
8835
|
+
app.delete("/users/:id", async (req, res) => {
|
|
8836
|
+
const resolved = await resolveTargetUser(req, res, { allowSelf: false });
|
|
8837
|
+
if (!resolved) return;
|
|
8838
|
+
const { targetUser, db } = resolved;
|
|
8839
|
+
try {
|
|
8840
|
+
await db.transaction(async (trx) => {
|
|
8841
|
+
await trx("agent_messages").where({ user: targetUser.id }).delete();
|
|
8842
|
+
await trx("agent_sessions").where({ user: targetUser.id }).delete();
|
|
8843
|
+
await trx("feedback").where({ user: targetUser.id }).delete();
|
|
8844
|
+
await trx("prompt_favorites").where({ user_id: targetUser.id }).delete();
|
|
8845
|
+
await trx("tracking").where({ user: targetUser.id }).delete();
|
|
8846
|
+
await trx("rbac").where({ user_id: targetUser.id }).delete();
|
|
8847
|
+
await trx("users").where({ id: targetUser.id }).delete();
|
|
8848
|
+
});
|
|
8849
|
+
} catch (err) {
|
|
8850
|
+
console.error("[GDPR] Failed to delete user", err);
|
|
8851
|
+
res.status(500).json({ detail: "Failed to delete user." });
|
|
8852
|
+
return;
|
|
8853
|
+
}
|
|
8854
|
+
try {
|
|
8855
|
+
const prefix = `user_${targetUser.id}/`;
|
|
8856
|
+
const files = await listS3ObjectsByPrefix(prefix, config);
|
|
8857
|
+
await Promise.all(files.map((f) => deleteS3Object(f.key, config)));
|
|
8858
|
+
} catch (err) {
|
|
8859
|
+
console.error("[GDPR] DB delete succeeded but S3 cleanup failed", err);
|
|
8860
|
+
}
|
|
8861
|
+
res.status(204).send();
|
|
8862
|
+
});
|
|
8863
|
+
app.use(express2.static("public"));
|
|
8864
|
+
await registerOpenAIGatewayRoutes(app, providers, tools, contexts, config, rerankers);
|
|
8865
|
+
return app;
|
|
8866
|
+
};
|
|
8867
|
+
function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
8868
|
+
function lcs(a, b) {
|
|
8869
|
+
const m = a.length;
|
|
8523
8870
|
const n = b.length;
|
|
8524
8871
|
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
8525
8872
|
for (let i = 1; i <= m; i++) {
|
|
@@ -8577,20 +8924,6 @@ function buildUnifiedDiff(fromLines, toLines, fromLabel, toLabel) {
|
|
|
8577
8924
|
}
|
|
8578
8925
|
return lines.join("\n");
|
|
8579
8926
|
}
|
|
8580
|
-
var createCustomAnthropicStreamingMessage = (message) => {
|
|
8581
|
-
const responseData = {
|
|
8582
|
-
type: "message",
|
|
8583
|
-
content: [
|
|
8584
|
-
{
|
|
8585
|
-
type: "text",
|
|
8586
|
-
text: message
|
|
8587
|
-
}
|
|
8588
|
-
]
|
|
8589
|
-
};
|
|
8590
|
-
const jsonString = JSON.stringify(responseData);
|
|
8591
|
-
const arrayBuffer = new TextEncoder().encode(jsonString).buffer;
|
|
8592
|
-
return arrayBuffer;
|
|
8593
|
-
};
|
|
8594
8927
|
|
|
8595
8928
|
// src/mcp/index.ts
|
|
8596
8929
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -8599,8 +8932,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
8599
8932
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
8600
8933
|
import "express";
|
|
8601
8934
|
import "@opentelemetry/api";
|
|
8602
|
-
import
|
|
8603
|
-
import { z as z2 } from "zod";
|
|
8935
|
+
import { z as z3 } from "zod";
|
|
8604
8936
|
var SESSION_ID_HEADER = "mcp-session-id";
|
|
8605
8937
|
var ExuluMCP = class {
|
|
8606
8938
|
server = {};
|
|
@@ -8638,61 +8970,53 @@ var ExuluMCP = class {
|
|
|
8638
8970
|
allProviders,
|
|
8639
8971
|
user
|
|
8640
8972
|
);
|
|
8641
|
-
|
|
8642
|
-
if (!provider) {
|
|
8973
|
+
if (!agent.model) {
|
|
8643
8974
|
throw new Error(
|
|
8644
|
-
"Agent
|
|
8975
|
+
"Agent has no model configured for agent " + agent.name + " (" + agent.id + ")."
|
|
8645
8976
|
);
|
|
8646
8977
|
}
|
|
8647
|
-
const
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
if (
|
|
8655
|
-
const
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
throw new Error(
|
|
8664
|
-
"Provider API key variable not encrypted, for security reasons you are only allowed to use encrypted variables for provider API keys."
|
|
8665
|
-
);
|
|
8666
|
-
}
|
|
8667
|
-
if (variable.encrypted) {
|
|
8668
|
-
const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
8669
|
-
providerapikey = bytes.toString(CryptoJS6.enc.Utf8);
|
|
8978
|
+
const resolved = await resolveModel({
|
|
8979
|
+
modelId: agent.model,
|
|
8980
|
+
user,
|
|
8981
|
+
providers: allProviders,
|
|
8982
|
+
agent: { id: agent.id }
|
|
8983
|
+
});
|
|
8984
|
+
const providerapikey = resolved.apiKey;
|
|
8985
|
+
if (!isLiteLLMEnabled()) {
|
|
8986
|
+
const agentTool = await resolved.exuluProvider.tool(
|
|
8987
|
+
agent.id,
|
|
8988
|
+
allProviders,
|
|
8989
|
+
allContexts,
|
|
8990
|
+
allRerankers
|
|
8991
|
+
);
|
|
8992
|
+
if (agentTool) {
|
|
8993
|
+
enabledTools = [...enabledTools, agentTool];
|
|
8670
8994
|
}
|
|
8671
8995
|
}
|
|
8672
8996
|
console.log(
|
|
8673
8997
|
"[EXULU] Enabled tools",
|
|
8674
8998
|
enabledTools?.map((x) => x.name + " (" + x.id + ")")
|
|
8675
8999
|
);
|
|
8676
|
-
for (const
|
|
8677
|
-
if (server.tools[
|
|
9000
|
+
for (const tool2 of enabledTools || []) {
|
|
9001
|
+
if (server.tools[tool2.id]) {
|
|
8678
9002
|
continue;
|
|
8679
9003
|
}
|
|
8680
9004
|
server.mcp.registerTool(
|
|
8681
|
-
sanitizeToolName(
|
|
9005
|
+
sanitizeToolName(tool2.name + "_agent_" + tool2.id),
|
|
8682
9006
|
{
|
|
8683
|
-
title:
|
|
8684
|
-
description:
|
|
9007
|
+
title: tool2.name + " agent",
|
|
9008
|
+
description: tool2.description,
|
|
8685
9009
|
inputSchema: {
|
|
8686
|
-
inputs:
|
|
9010
|
+
inputs: tool2.inputSchema || z3.object({})
|
|
8687
9011
|
}
|
|
8688
9012
|
},
|
|
8689
9013
|
async ({ inputs }, args) => {
|
|
8690
|
-
console.log("[EXULU] MCP tool name",
|
|
9014
|
+
console.log("[EXULU] MCP tool name", tool2.name);
|
|
8691
9015
|
console.log("[EXULU] MCP tool inputs", inputs);
|
|
8692
9016
|
console.log("[EXULU] MCP tool args", args);
|
|
8693
9017
|
const configValues = agent.tools;
|
|
8694
9018
|
const tools = await convertExuluToolsToAiSdkTools(
|
|
8695
|
-
[
|
|
9019
|
+
[tool2],
|
|
8696
9020
|
[],
|
|
8697
9021
|
[],
|
|
8698
9022
|
allTools,
|
|
@@ -8706,13 +9030,13 @@ var ExuluMCP = class {
|
|
|
8706
9030
|
void 0,
|
|
8707
9031
|
void 0
|
|
8708
9032
|
);
|
|
8709
|
-
const convertedTool = tools[sanitizeToolName(
|
|
9033
|
+
const convertedTool = tools[sanitizeToolName(tool2.name)];
|
|
8710
9034
|
if (!convertedTool?.execute) {
|
|
8711
9035
|
console.error("[EXULU] Tool not found in converted tools array.", tools);
|
|
8712
9036
|
throw new Error("Tool not found in converted tools array.");
|
|
8713
9037
|
}
|
|
8714
9038
|
const iterator = await convertedTool.execute(inputs, {
|
|
8715
|
-
toolCallId:
|
|
9039
|
+
toolCallId: tool2.id + "_" + randomUUID3(),
|
|
8716
9040
|
messages: []
|
|
8717
9041
|
});
|
|
8718
9042
|
let result;
|
|
@@ -8726,7 +9050,7 @@ var ExuluMCP = class {
|
|
|
8726
9050
|
};
|
|
8727
9051
|
}
|
|
8728
9052
|
);
|
|
8729
|
-
server.tools[
|
|
9053
|
+
server.tools[tool2.id] = tool2.name;
|
|
8730
9054
|
}
|
|
8731
9055
|
const getListOfPromptTemplatesName = "getListOfPromptTemplates";
|
|
8732
9056
|
if (!server.tools[getListOfPromptTemplatesName]) {
|
|
@@ -8736,13 +9060,13 @@ var ExuluMCP = class {
|
|
|
8736
9060
|
title: "Get List of Prompt Templates",
|
|
8737
9061
|
description: "Retrieves a list of prompt templates available for this agent. Returns the name, description, and ID of each template.",
|
|
8738
9062
|
inputSchema: {
|
|
8739
|
-
inputs:
|
|
9063
|
+
inputs: z3.object({})
|
|
8740
9064
|
}
|
|
8741
9065
|
},
|
|
8742
9066
|
async ({ inputs }, args) => {
|
|
8743
9067
|
console.log("[EXULU] Getting list of prompt templates for agent", agent.id);
|
|
8744
|
-
const { db
|
|
8745
|
-
const prompts = await
|
|
9068
|
+
const { db } = await postgresClient();
|
|
9069
|
+
const prompts = await db.from("prompt_library").select("id", "name", "description").whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).orderBy("updatedAt", "desc");
|
|
8746
9070
|
console.log("[EXULU] Found", prompts.length, "prompt templates");
|
|
8747
9071
|
return {
|
|
8748
9072
|
content: [
|
|
@@ -8782,15 +9106,15 @@ var ExuluMCP = class {
|
|
|
8782
9106
|
title: "Get Prompt Template Details",
|
|
8783
9107
|
description: "Retrieves the full details of a specific prompt template by ID, including the actual template content with variables.",
|
|
8784
9108
|
inputSchema: {
|
|
8785
|
-
inputs:
|
|
8786
|
-
id:
|
|
9109
|
+
inputs: z3.object({
|
|
9110
|
+
id: z3.string().describe("The ID of the prompt template to retrieve")
|
|
8787
9111
|
})
|
|
8788
9112
|
}
|
|
8789
9113
|
},
|
|
8790
9114
|
async ({ inputs }, args) => {
|
|
8791
9115
|
console.log("[EXULU] Getting prompt template details for ID", inputs.id);
|
|
8792
|
-
const { db
|
|
8793
|
-
const prompt = await
|
|
9116
|
+
const { db } = await postgresClient();
|
|
9117
|
+
const prompt = await db.from("prompt_library").select(
|
|
8794
9118
|
"id",
|
|
8795
9119
|
"name",
|
|
8796
9120
|
"description",
|
|
@@ -8803,7 +9127,7 @@ var ExuluMCP = class {
|
|
|
8803
9127
|
if (!prompt) {
|
|
8804
9128
|
throw new Error(`Prompt template with ID ${inputs.id} not found`);
|
|
8805
9129
|
}
|
|
8806
|
-
const isAssignedToAgent = await
|
|
9130
|
+
const isAssignedToAgent = await db.from("prompt_library").select("id").where({ id: inputs.id }).whereRaw("assigned_agents @> ?::jsonb", [JSON.stringify(agent.id)]).first();
|
|
8807
9131
|
console.log("[EXULU] Prompt template found:", prompt.name);
|
|
8808
9132
|
return {
|
|
8809
9133
|
content: [
|
|
@@ -9886,7 +10210,7 @@ var ExuluEval = class {
|
|
|
9886
10210
|
};
|
|
9887
10211
|
|
|
9888
10212
|
// src/templates/evals/index.ts
|
|
9889
|
-
import { z as
|
|
10213
|
+
import { z as z4 } from "zod";
|
|
9890
10214
|
var llmAsJudgeEval = () => {
|
|
9891
10215
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
9892
10216
|
return new ExuluEval({
|
|
@@ -9919,21 +10243,28 @@ var llmAsJudgeEval = () => {
|
|
|
9919
10243
|
}
|
|
9920
10244
|
prompt = prompt.replace("{actual_output}", lastMessage);
|
|
9921
10245
|
prompt = prompt.replace("{expected_output}", testCase.expected_output);
|
|
9922
|
-
if (!agent.
|
|
10246
|
+
if (!agent.model) {
|
|
9923
10247
|
throw new Error(
|
|
9924
|
-
`
|
|
10248
|
+
`Agent ${agent.name} has no model configured (required for llm-as-judge eval).`
|
|
9925
10249
|
);
|
|
9926
10250
|
}
|
|
9927
|
-
const
|
|
10251
|
+
const resolved = await resolveModel({
|
|
10252
|
+
modelId: agent.model,
|
|
10253
|
+
providers: exuluApp.get().providers,
|
|
10254
|
+
agent: { id: agent.id },
|
|
10255
|
+
rbacBypass: true
|
|
10256
|
+
});
|
|
10257
|
+
const providerapikey = resolved.apiKey;
|
|
9928
10258
|
console.log("[EXULU] prompt", prompt);
|
|
9929
10259
|
const response = await provider.generateSync({
|
|
9930
10260
|
agent,
|
|
9931
10261
|
contexts: [],
|
|
9932
10262
|
rerankers: [],
|
|
9933
10263
|
prompt,
|
|
9934
|
-
outputSchema:
|
|
9935
|
-
score:
|
|
10264
|
+
outputSchema: z4.object({
|
|
10265
|
+
score: z4.number().min(0).max(100).describe("The score between 0 and 100.")
|
|
9936
10266
|
}),
|
|
10267
|
+
languageModel: resolved.languageModel,
|
|
9937
10268
|
providerapikey
|
|
9938
10269
|
});
|
|
9939
10270
|
console.log("[EXULU] response", response);
|
|
@@ -10160,12 +10491,12 @@ Usage:
|
|
|
10160
10491
|
- If no todos exist yet, an empty list will be returned`;
|
|
10161
10492
|
|
|
10162
10493
|
// src/templates/tools/todo/todo.ts
|
|
10163
|
-
import
|
|
10164
|
-
var TodoSchema =
|
|
10165
|
-
content:
|
|
10166
|
-
status:
|
|
10167
|
-
priority:
|
|
10168
|
-
id:
|
|
10494
|
+
import z5 from "zod";
|
|
10495
|
+
var TodoSchema = z5.object({
|
|
10496
|
+
content: z5.string().describe("Brief description of the task"),
|
|
10497
|
+
status: z5.string().describe("Current status of the task: pending, in_progress, completed, cancelled"),
|
|
10498
|
+
priority: z5.string().describe("Priority level of the task: high, medium, low"),
|
|
10499
|
+
id: z5.string().describe("Unique identifier for the todo item")
|
|
10169
10500
|
});
|
|
10170
10501
|
var TodoWriteTool = new ExuluTool({
|
|
10171
10502
|
id: "todo_write",
|
|
@@ -10181,8 +10512,8 @@ var TodoWriteTool = new ExuluTool({
|
|
|
10181
10512
|
default: todowrite_default
|
|
10182
10513
|
}
|
|
10183
10514
|
],
|
|
10184
|
-
inputSchema:
|
|
10185
|
-
todos:
|
|
10515
|
+
inputSchema: z5.object({
|
|
10516
|
+
todos: z5.array(TodoSchema).describe("The updated todo list")
|
|
10186
10517
|
}),
|
|
10187
10518
|
execute: async (inputs) => {
|
|
10188
10519
|
const { sessionID, todos, user } = inputs;
|
|
@@ -10217,7 +10548,7 @@ var TodoReadTool = new ExuluTool({
|
|
|
10217
10548
|
id: "todo_read",
|
|
10218
10549
|
name: "Todo Read",
|
|
10219
10550
|
description: "Use this tool to read your todo list",
|
|
10220
|
-
inputSchema:
|
|
10551
|
+
inputSchema: z5.object({}),
|
|
10221
10552
|
type: "function",
|
|
10222
10553
|
category: "todo",
|
|
10223
10554
|
config: [
|
|
@@ -10259,7 +10590,7 @@ var todoTools = [TodoWriteTool, TodoReadTool];
|
|
|
10259
10590
|
var questionread_default = 'Use this tool to read questions you\'ve asked and check if they\'ve been answered by the user. This tool helps you track the status of questions and retrieve the user\'s selected answers.\n\n## When to Use This Tool\n\nUse this tool proactively in these situations:\n- After asking a question to check if the user has responded\n- To retrieve the user\'s answer before proceeding with implementation\n- To review all questions and answers in the current session\n- When you need to reference a previous answer\n\n## How It Works\n\n- This tool takes no parameters (leave the input blank or empty)\n- Returns an array of all questions in the session\n- Each question includes:\n - `id`: Unique identifier for the question\n - `question`: The question text\n - `answerOptions`: Array of answer options with their IDs and text\n - `status`: Either "pending" (not answered) or "answered"\n - `selectedAnswerId`: The ID of the chosen answer (only present if answered)\n\n## Usage Pattern\n\nTypically you\'ll:\n1. Use Question Ask to pose a question\n2. Wait for the user to respond\n3. Use Question Read to check the answer\n4. Find the selected answer by matching the `selectedAnswerId` with an option in `answerOptions`\n5. Proceed with implementation based on the user\'s choice\n\n## Example Response\n\n```json\n[\n {\n "id": "question123",\n "question": "Which authentication method would you like to implement?",\n "answerOptions": [\n { "id": "ans1", "text": "JWT tokens" },\n { "id": "ans2", "text": "OAuth 2.0" },\n { "id": "ans3", "text": "Session-based auth" },\n { "id": "ans4", "text": "None of the above..." }\n ],\n "status": "answered",\n "selectedAnswerId": "ans1"\n }\n]\n```\n\nIn this example, the user selected "JWT tokens" (id: ans1).\n\n## Important Notes\n\n- If no questions exist in the session, an empty array will be returned\n- Questions remain in the session even after being answered for reference\n- Use the `selectedAnswerId` to find which answer option the user chose by matching it against the `id` field in `answerOptions`\n';
|
|
10260
10591
|
|
|
10261
10592
|
// src/templates/tools/question/question.ts
|
|
10262
|
-
import
|
|
10593
|
+
import z7 from "zod";
|
|
10263
10594
|
|
|
10264
10595
|
// src/templates/tools/question/questionask.txt
|
|
10265
10596
|
var questionask_default = `Use this tool to ask the user a question with multiple choice answer options during your session. This helps you gather user input, clarify requirements, and make informed decisions based on user preferences.
|
|
@@ -10346,18 +10677,18 @@ After asking a question, use the Question Read tool to check if the user has ans
|
|
|
10346
10677
|
`;
|
|
10347
10678
|
|
|
10348
10679
|
// src/templates/tools/question/question-ask.ts
|
|
10349
|
-
import
|
|
10680
|
+
import z6 from "zod";
|
|
10350
10681
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10351
|
-
var AnswerOptionSchema =
|
|
10352
|
-
id:
|
|
10353
|
-
text:
|
|
10682
|
+
var AnswerOptionSchema = z6.object({
|
|
10683
|
+
id: z6.string().describe("Unique identifier for the answer option"),
|
|
10684
|
+
text: z6.string().describe("The text of the answer option")
|
|
10354
10685
|
});
|
|
10355
|
-
var _QuestionSchema =
|
|
10356
|
-
id:
|
|
10357
|
-
question:
|
|
10358
|
-
answerOptions:
|
|
10359
|
-
selectedAnswerId:
|
|
10360
|
-
status:
|
|
10686
|
+
var _QuestionSchema = z6.object({
|
|
10687
|
+
id: z6.string().describe("Unique identifier for the question"),
|
|
10688
|
+
question: z6.string().describe("The question to ask the user"),
|
|
10689
|
+
answerOptions: z6.array(AnswerOptionSchema).describe("Array of possible answer options"),
|
|
10690
|
+
selectedAnswerId: z6.string().optional().describe("The ID of the answer option selected by the user"),
|
|
10691
|
+
status: z6.enum(["pending", "answered"]).describe("Status of the question: pending or answered")
|
|
10361
10692
|
});
|
|
10362
10693
|
var QuestionAskTool = new ExuluTool({
|
|
10363
10694
|
id: "question_ask",
|
|
@@ -10374,9 +10705,9 @@ var QuestionAskTool = new ExuluTool({
|
|
|
10374
10705
|
default: questionask_default
|
|
10375
10706
|
}
|
|
10376
10707
|
],
|
|
10377
|
-
inputSchema:
|
|
10378
|
-
question:
|
|
10379
|
-
answerOptions:
|
|
10708
|
+
inputSchema: z6.object({
|
|
10709
|
+
question: z6.string().describe("The question to ask the user"),
|
|
10710
|
+
answerOptions: z6.array(z6.string()).describe("Array of possible answer options (strings)")
|
|
10380
10711
|
}),
|
|
10381
10712
|
execute: async (inputs) => {
|
|
10382
10713
|
const { sessionID, question, answerOptions, user } = inputs;
|
|
@@ -10449,7 +10780,7 @@ var QuestionReadTool = new ExuluTool({
|
|
|
10449
10780
|
name: "Question Read",
|
|
10450
10781
|
needsApproval: false,
|
|
10451
10782
|
description: "Use this tool to read questions and their answers",
|
|
10452
|
-
inputSchema:
|
|
10783
|
+
inputSchema: z7.object({}),
|
|
10453
10784
|
type: "function",
|
|
10454
10785
|
category: "question",
|
|
10455
10786
|
config: [
|
|
@@ -10479,15 +10810,15 @@ async function getQuestions(sessionID) {
|
|
|
10479
10810
|
var questionTools = [QuestionAskTool, QuestionReadTool];
|
|
10480
10811
|
|
|
10481
10812
|
// src/templates/tools/perplexity.ts
|
|
10482
|
-
import
|
|
10813
|
+
import z8 from "zod";
|
|
10483
10814
|
import Perplexity from "@perplexity-ai/perplexity_ai";
|
|
10484
10815
|
var internetSearchTool = new ExuluTool({
|
|
10485
10816
|
id: "internet_search",
|
|
10486
10817
|
name: "Internet Search",
|
|
10487
10818
|
description: "Search the internet for information.",
|
|
10488
|
-
inputSchema:
|
|
10489
|
-
query:
|
|
10490
|
-
search_recency_filter:
|
|
10819
|
+
inputSchema: z8.object({
|
|
10820
|
+
query: z8.string().describe("The query to the tool."),
|
|
10821
|
+
search_recency_filter: z8.enum(["day", "week", "month", "year"]).optional().describe("The recency filter for the search, can be day, week, month or year.")
|
|
10491
10822
|
}),
|
|
10492
10823
|
category: "internet_search",
|
|
10493
10824
|
type: "web_search",
|
|
@@ -10565,121 +10896,531 @@ var internetSearchTool = new ExuluTool({
|
|
|
10565
10896
|
} catch (error) {
|
|
10566
10897
|
if (error instanceof Perplexity.RateLimitError && attempt < maxRetries - 1) {
|
|
10567
10898
|
const delay = Math.pow(2, attempt) * 1e3 + Math.random() * 1e3;
|
|
10568
|
-
await new Promise((
|
|
10899
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
10569
10900
|
continue;
|
|
10570
10901
|
}
|
|
10571
10902
|
throw error;
|
|
10572
10903
|
}
|
|
10573
10904
|
}
|
|
10574
10905
|
return {
|
|
10575
|
-
result: "Max retries exceeded for perplexity research for query."
|
|
10906
|
+
result: "Max retries exceeded for perplexity research for query."
|
|
10907
|
+
};
|
|
10908
|
+
}
|
|
10909
|
+
});
|
|
10910
|
+
var perplexityTools = [internetSearchTool];
|
|
10911
|
+
|
|
10912
|
+
// src/templates/tools/email.ts
|
|
10913
|
+
import * as nodemailer from "nodemailer";
|
|
10914
|
+
import { z as z9 } from "zod";
|
|
10915
|
+
var transporter = null;
|
|
10916
|
+
function getTransporter(config) {
|
|
10917
|
+
if (!transporter) {
|
|
10918
|
+
transporter = nodemailer.createTransport(config);
|
|
10919
|
+
}
|
|
10920
|
+
return transporter;
|
|
10921
|
+
}
|
|
10922
|
+
async function sendEmail(recipient, subject, html, text, config) {
|
|
10923
|
+
const transport = getTransporter(config);
|
|
10924
|
+
html = html.trim();
|
|
10925
|
+
text = text.trim();
|
|
10926
|
+
await transport.sendMail({
|
|
10927
|
+
from: config.from,
|
|
10928
|
+
to: recipient,
|
|
10929
|
+
subject,
|
|
10930
|
+
text,
|
|
10931
|
+
html
|
|
10932
|
+
});
|
|
10933
|
+
}
|
|
10934
|
+
var emailTool = new ExuluTool({
|
|
10935
|
+
id: "email",
|
|
10936
|
+
name: "Email",
|
|
10937
|
+
description: "Send an email.",
|
|
10938
|
+
inputSchema: z9.object({
|
|
10939
|
+
recipient: z9.string().describe("The recipient of the email."),
|
|
10940
|
+
subject: z9.string().describe("The subject of the email."),
|
|
10941
|
+
html: z9.string().describe("The HTML body of the email."),
|
|
10942
|
+
text: z9.string().describe("The text body of the email.")
|
|
10943
|
+
}),
|
|
10944
|
+
type: "function",
|
|
10945
|
+
config: [{
|
|
10946
|
+
name: "smtp_host",
|
|
10947
|
+
description: "The SMTP host to send the email from.",
|
|
10948
|
+
type: "variable",
|
|
10949
|
+
default: void 0
|
|
10950
|
+
}, {
|
|
10951
|
+
name: "smtp_port",
|
|
10952
|
+
description: "The SMTP port to send the email from.",
|
|
10953
|
+
type: "variable",
|
|
10954
|
+
default: void 0
|
|
10955
|
+
}, {
|
|
10956
|
+
name: "smtp_user",
|
|
10957
|
+
description: "The SMTP user to send the email from.",
|
|
10958
|
+
type: "variable",
|
|
10959
|
+
default: void 0
|
|
10960
|
+
}, {
|
|
10961
|
+
name: "smtp_password",
|
|
10962
|
+
description: "The SMTP password to send the email from.",
|
|
10963
|
+
type: "variable",
|
|
10964
|
+
default: void 0
|
|
10965
|
+
}, {
|
|
10966
|
+
name: "smtp_from",
|
|
10967
|
+
description: "The SMTP from address to send the email from.",
|
|
10968
|
+
type: "variable",
|
|
10969
|
+
default: void 0
|
|
10970
|
+
}, {
|
|
10971
|
+
name: "allowed_recipient_domains",
|
|
10972
|
+
description: "A comma seperated list of allowed recipient domains to send emails to.",
|
|
10973
|
+
type: "string",
|
|
10974
|
+
default: void 0
|
|
10975
|
+
}],
|
|
10976
|
+
execute: async ({ recipient, subject, html, text, toolVariablesConfig }) => {
|
|
10977
|
+
const EMAIL_CONFIG = {
|
|
10978
|
+
host: toolVariablesConfig.smtp_host || process.env.SMTP_HOST || "",
|
|
10979
|
+
port: parseInt(toolVariablesConfig.smtp_port || process.env.SMTP_PORT || "587", 10),
|
|
10980
|
+
secure: toolVariablesConfig.smtp_secure === "true" || process.env.SMTP_SECURE === "true",
|
|
10981
|
+
// true for 465, false for other ports
|
|
10982
|
+
auth: {
|
|
10983
|
+
user: toolVariablesConfig.smtp_user || process.env.SMTP_USER || "",
|
|
10984
|
+
pass: toolVariablesConfig.smtp_password || process.env.SMTP_PASSWORD || ""
|
|
10985
|
+
},
|
|
10986
|
+
from: toolVariablesConfig.smtp_from || process.env.SMTP_FROM || "",
|
|
10987
|
+
// Allow self-signed certificates if SMTP_REJECT_UNAUTHORIZED is set to 'false'
|
|
10988
|
+
tls: {
|
|
10989
|
+
rejectUnauthorized: false
|
|
10990
|
+
}
|
|
10991
|
+
};
|
|
10992
|
+
if (toolVariablesConfig.allowed_recipient_domains) {
|
|
10993
|
+
const allowedRecipientDomains = toolVariablesConfig.allowed_recipient_domains.split(",");
|
|
10994
|
+
if (!allowedRecipientDomains.some((domain) => recipient.endsWith(`@${domain}`))) {
|
|
10995
|
+
return {
|
|
10996
|
+
result: "Recipient domain not allowed to send emails to."
|
|
10997
|
+
};
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
await sendEmail(recipient, subject, html, text, EMAIL_CONFIG);
|
|
11001
|
+
return {
|
|
11002
|
+
result: "Email sent successfully to " + recipient + " with subject " + subject + "."
|
|
11003
|
+
};
|
|
11004
|
+
}
|
|
11005
|
+
});
|
|
11006
|
+
|
|
11007
|
+
// src/templates/tools/transcribe.ts
|
|
11008
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
11009
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
11010
|
+
import { dirname, join as join2 } from "path";
|
|
11011
|
+
import { z as z10 } from "zod";
|
|
11012
|
+
var SANDBOX_ROOT = "/tmp/exulu-sessions";
|
|
11013
|
+
var parseSandboxPath = (input) => {
|
|
11014
|
+
const stripped = input.startsWith("file://") ? input.slice("file://".length) : input;
|
|
11015
|
+
if (!stripped.startsWith(`${SANDBOX_ROOT}/`)) return null;
|
|
11016
|
+
const tail = stripped.slice(SANDBOX_ROOT.length + 1);
|
|
11017
|
+
const slash = tail.indexOf("/");
|
|
11018
|
+
if (slash < 1) return null;
|
|
11019
|
+
const sessionId = tail.slice(0, slash);
|
|
11020
|
+
const relPath = tail.slice(slash + 1);
|
|
11021
|
+
if (!relPath) return null;
|
|
11022
|
+
return { sessionId, relPath };
|
|
11023
|
+
};
|
|
11024
|
+
var audioMimetypeFromExtension = (filename) => {
|
|
11025
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
11026
|
+
switch (ext) {
|
|
11027
|
+
case "mp3":
|
|
11028
|
+
return "audio/mpeg";
|
|
11029
|
+
case "m4a":
|
|
11030
|
+
case "mp4":
|
|
11031
|
+
return "audio/mp4";
|
|
11032
|
+
case "wav":
|
|
11033
|
+
return "audio/wav";
|
|
11034
|
+
case "ogg":
|
|
11035
|
+
case "oga":
|
|
11036
|
+
return "audio/ogg";
|
|
11037
|
+
case "flac":
|
|
11038
|
+
return "audio/flac";
|
|
11039
|
+
case "webm":
|
|
11040
|
+
return "audio/webm";
|
|
11041
|
+
case "aac":
|
|
11042
|
+
return "audio/aac";
|
|
11043
|
+
case "mpga":
|
|
11044
|
+
case "mpeg":
|
|
11045
|
+
return "audio/mpeg";
|
|
11046
|
+
default:
|
|
11047
|
+
throw new Error(
|
|
11048
|
+
`Unable to infer an audio mimetype from filename "${filename}". Supported extensions: mp3, m4a, mp4, wav, ogg, flac, webm, aac, mpga.`
|
|
11049
|
+
);
|
|
11050
|
+
}
|
|
11051
|
+
};
|
|
11052
|
+
var transcribeTool = new ExuluTool({
|
|
11053
|
+
id: "transcribe_audio",
|
|
11054
|
+
name: "Transcribe Audio",
|
|
11055
|
+
description: "Transcribe an audio file (mp3, wav, m4a, etc.) from a URL to text using the configured speech-to-text model. The transcript is stored as a .txt file on S3 and the URL is returned; use this for clips that may be too long to inline in the conversation.",
|
|
11056
|
+
inputSchema: z10.object({
|
|
11057
|
+
audio_url: z10.string().describe(
|
|
11058
|
+
"Location of the audio file to transcribe. Accepts a publicly fetchable URL (https URL or presigned S3 URL), or a sandbox path such as '/tmp/exulu-sessions/<sessionId>/<file>' or 'file:///tmp/exulu-sessions/<sessionId>/<file>' \u2014 sandbox paths are resolved to their persisted S3 copy."
|
|
11059
|
+
),
|
|
11060
|
+
language: z10.string().optional().describe(
|
|
11061
|
+
"ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect."
|
|
11062
|
+
)
|
|
11063
|
+
}),
|
|
11064
|
+
type: "function",
|
|
11065
|
+
config: [{
|
|
11066
|
+
name: "default_language",
|
|
11067
|
+
description: "ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect.",
|
|
11068
|
+
type: "string",
|
|
11069
|
+
default: void 0
|
|
11070
|
+
}],
|
|
11071
|
+
execute: async ({ audio_url, language, user, exuluConfig, sessionID }) => {
|
|
11072
|
+
if (!language && exuluConfig?.default_language) {
|
|
11073
|
+
language = exuluConfig?.default_language;
|
|
11074
|
+
} else {
|
|
11075
|
+
language = "en";
|
|
11076
|
+
}
|
|
11077
|
+
language = exuluConfig?.default_language;
|
|
11078
|
+
console.log("[EXULU] Exulu config", exuluConfig);
|
|
11079
|
+
if (!isLiteLLMEnabled()) {
|
|
11080
|
+
console.error("[EXULU] Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true').");
|
|
11081
|
+
throw new Error(
|
|
11082
|
+
"Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true')."
|
|
11083
|
+
);
|
|
11084
|
+
}
|
|
11085
|
+
if (!process.env.TRANSCRIPTION_MODEL) {
|
|
11086
|
+
console.error("[EXULU] TRANSCRIPTION_MODEL env var is not set.");
|
|
11087
|
+
throw new Error("TRANSCRIPTION_MODEL env var is not set.");
|
|
11088
|
+
}
|
|
11089
|
+
if (!exuluConfig?.fileUploads) {
|
|
11090
|
+
console.error("[EXULU] File uploads are not configured; the transcribe tool requires S3 to store transcripts.");
|
|
11091
|
+
throw new Error(
|
|
11092
|
+
"File uploads are not configured; the transcribe tool requires S3 to store transcripts."
|
|
11093
|
+
);
|
|
11094
|
+
}
|
|
11095
|
+
const sandboxPath = parseSandboxPath(audio_url);
|
|
11096
|
+
let buffer;
|
|
11097
|
+
let mimetype;
|
|
11098
|
+
let originalname;
|
|
11099
|
+
if (sandboxPath) {
|
|
11100
|
+
if (!user?.id) {
|
|
11101
|
+
throw new Error(
|
|
11102
|
+
"Sandbox audio paths require an authenticated user; got no user on the tool call."
|
|
11103
|
+
);
|
|
11104
|
+
}
|
|
11105
|
+
if (sessionID && sandboxPath.sessionId !== sessionID) {
|
|
11106
|
+
throw new Error(
|
|
11107
|
+
`Refusing to transcribe an audio file from a different session's sandbox (path session=${sandboxPath.sessionId}, current session=${sessionID}).`
|
|
11108
|
+
);
|
|
11109
|
+
}
|
|
11110
|
+
const rawKey = `user_${user.id}/sessions/${sandboxPath.sessionId}/${sandboxPath.relPath}`;
|
|
11111
|
+
console.log("[EXULU] Transcribing audio from sandbox path", {
|
|
11112
|
+
rawKey
|
|
11113
|
+
});
|
|
11114
|
+
const matches = await listS3ObjectsByPrefix(rawKey, exuluConfig);
|
|
11115
|
+
const found = matches.find((m) => m.key.endsWith(rawKey));
|
|
11116
|
+
if (!found) {
|
|
11117
|
+
console.error("[EXULU] Sandbox audio file not found in S3 storage at", {
|
|
11118
|
+
rawKey,
|
|
11119
|
+
matches
|
|
11120
|
+
});
|
|
11121
|
+
throw new Error(
|
|
11122
|
+
`Sandbox audio file not found in S3 storage at "${rawKey}". The file may not have been persisted yet \u2014 try again after the sandbox flushes it.`
|
|
11123
|
+
);
|
|
11124
|
+
}
|
|
11125
|
+
buffer = await getS3ObjectBytes(found.key, exuluConfig);
|
|
11126
|
+
originalname = decodeURIComponent(
|
|
11127
|
+
sandboxPath.relPath.split("/").pop() || "audio"
|
|
11128
|
+
);
|
|
11129
|
+
mimetype = audioMimetypeFromExtension(originalname);
|
|
11130
|
+
} else {
|
|
11131
|
+
console.log("[EXULU] Fetching audio from URL", {
|
|
11132
|
+
audio_url
|
|
11133
|
+
});
|
|
11134
|
+
const upstream = await fetch(audio_url);
|
|
11135
|
+
if (!upstream.ok) {
|
|
11136
|
+
console.error("[EXULU] Failed to fetch audio from", {
|
|
11137
|
+
audio_url,
|
|
11138
|
+
upstream
|
|
11139
|
+
});
|
|
11140
|
+
throw new Error(
|
|
11141
|
+
`Failed to fetch audio from ${audio_url}: ${upstream.status} ${upstream.statusText}`
|
|
11142
|
+
);
|
|
11143
|
+
}
|
|
11144
|
+
mimetype = upstream.headers.get("content-type") || "audio/mpeg";
|
|
11145
|
+
if (!mimetype.startsWith("audio/")) {
|
|
11146
|
+
throw new Error(
|
|
11147
|
+
`URL did not return an audio file (content-type: ${mimetype}).`
|
|
11148
|
+
);
|
|
11149
|
+
}
|
|
11150
|
+
buffer = Buffer.from(await upstream.arrayBuffer());
|
|
11151
|
+
originalname = "audio";
|
|
11152
|
+
try {
|
|
11153
|
+
const pathname = new URL(audio_url).pathname;
|
|
11154
|
+
const last = pathname.split("/").pop();
|
|
11155
|
+
if (last) originalname = decodeURIComponent(last);
|
|
11156
|
+
} catch {
|
|
11157
|
+
}
|
|
11158
|
+
}
|
|
11159
|
+
const { text } = await transcribeAudio({
|
|
11160
|
+
file: { buffer, originalname, mimetype },
|
|
11161
|
+
language
|
|
11162
|
+
});
|
|
11163
|
+
const transcriptBuffer = Buffer.from(text, "utf-8");
|
|
11164
|
+
const transcriptFilename = `${randomUUID5()}.txt`;
|
|
11165
|
+
const transcriptKey = sessionID ? `sessions/${sessionID}/transcripts/${transcriptFilename}` : `transcripts/${transcriptFilename}`;
|
|
11166
|
+
console.log("[EXULU] Uploading transcript to S3", {
|
|
11167
|
+
transcriptFilename,
|
|
11168
|
+
transcriptKey
|
|
11169
|
+
});
|
|
11170
|
+
const url = await uploadFile(
|
|
11171
|
+
transcriptBuffer,
|
|
11172
|
+
transcriptKey,
|
|
11173
|
+
exuluConfig,
|
|
11174
|
+
{ contentType: "text/plain" },
|
|
11175
|
+
user?.id
|
|
11176
|
+
);
|
|
11177
|
+
console.log("[EXULU] Uploaded transcript to S3", {
|
|
11178
|
+
url
|
|
11179
|
+
});
|
|
11180
|
+
let sandboxLocalPath;
|
|
11181
|
+
if (sessionID) {
|
|
11182
|
+
sandboxLocalPath = join2(
|
|
11183
|
+
SANDBOX_ROOT,
|
|
11184
|
+
sessionID,
|
|
11185
|
+
"transcripts",
|
|
11186
|
+
transcriptFilename
|
|
11187
|
+
);
|
|
11188
|
+
console.log("[EXULU] Mirroring transcript into session sandbox", {
|
|
11189
|
+
sandboxLocalPath
|
|
11190
|
+
});
|
|
11191
|
+
try {
|
|
11192
|
+
await mkdir2(dirname(sandboxLocalPath), { recursive: true });
|
|
11193
|
+
await writeFile2(sandboxLocalPath, transcriptBuffer);
|
|
11194
|
+
} catch (err) {
|
|
11195
|
+
console.error(
|
|
11196
|
+
`[EXULU] Failed to write transcript to sandbox dir ${sandboxLocalPath}; S3 copy is unaffected.`,
|
|
11197
|
+
err
|
|
11198
|
+
);
|
|
11199
|
+
sandboxLocalPath = void 0;
|
|
11200
|
+
}
|
|
11201
|
+
}
|
|
11202
|
+
console.log("[EXULU] Transcribed audio successfully", {
|
|
11203
|
+
text,
|
|
11204
|
+
url,
|
|
11205
|
+
sandboxLocalPath,
|
|
11206
|
+
length: text.length
|
|
11207
|
+
});
|
|
11208
|
+
return {
|
|
11209
|
+
result: sandboxLocalPath ? `Transcript stored at: ${url} (also available in the session sandbox at ${sandboxLocalPath}, ${text.length} characters).` : `${url}`
|
|
11210
|
+
};
|
|
11211
|
+
}
|
|
11212
|
+
});
|
|
11213
|
+
|
|
11214
|
+
// src/validators/postgres-name.ts
|
|
11215
|
+
var isValidPostgresName = (id) => {
|
|
11216
|
+
const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
11217
|
+
const isValid = regex.test(id);
|
|
11218
|
+
const length = id.length;
|
|
11219
|
+
return isValid && length <= 80 && length > 2;
|
|
11220
|
+
};
|
|
11221
|
+
|
|
11222
|
+
// src/utils/python-setup.ts
|
|
11223
|
+
import { exec as exec2 } from "child_process";
|
|
11224
|
+
import { promisify as promisify2 } from "util";
|
|
11225
|
+
import { resolve, join as join3, dirname as dirname2 } from "path";
|
|
11226
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
11227
|
+
import { fileURLToPath } from "url";
|
|
11228
|
+
var execAsync2 = promisify2(exec2);
|
|
11229
|
+
function getPackageRoot() {
|
|
11230
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
11231
|
+
let currentDir = dirname2(currentFile);
|
|
11232
|
+
let attempts = 0;
|
|
11233
|
+
const maxAttempts = 10;
|
|
11234
|
+
while (attempts < maxAttempts) {
|
|
11235
|
+
const packageJsonPath = join3(currentDir, "package.json");
|
|
11236
|
+
if (existsSync2(packageJsonPath)) {
|
|
11237
|
+
try {
|
|
11238
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
11239
|
+
if (packageJson.name === "@exulu/backend") {
|
|
11240
|
+
return currentDir;
|
|
11241
|
+
}
|
|
11242
|
+
} catch {
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
const parentDir = resolve(currentDir, "..");
|
|
11246
|
+
if (parentDir === currentDir) {
|
|
11247
|
+
break;
|
|
11248
|
+
}
|
|
11249
|
+
currentDir = parentDir;
|
|
11250
|
+
attempts++;
|
|
11251
|
+
}
|
|
11252
|
+
const fallback = resolve(dirname2(fileURLToPath(import.meta.url)), "../..");
|
|
11253
|
+
return fallback;
|
|
11254
|
+
}
|
|
11255
|
+
function getSetupScriptPath(packageRoot) {
|
|
11256
|
+
return resolve(packageRoot, "ee/python/setup.sh");
|
|
11257
|
+
}
|
|
11258
|
+
function getVenvPath(packageRoot) {
|
|
11259
|
+
return resolve(packageRoot, "ee/python/.venv");
|
|
11260
|
+
}
|
|
11261
|
+
function isPythonEnvironmentSetup(packageRoot) {
|
|
11262
|
+
const root = packageRoot ?? getPackageRoot();
|
|
11263
|
+
const venvPath = getVenvPath(root);
|
|
11264
|
+
const pythonPath = join3(venvPath, "bin", "python");
|
|
11265
|
+
return existsSync2(venvPath) && existsSync2(pythonPath);
|
|
11266
|
+
}
|
|
11267
|
+
async function setupPythonEnvironment(options = {}) {
|
|
11268
|
+
const {
|
|
11269
|
+
packageRoot = getPackageRoot(),
|
|
11270
|
+
force = false,
|
|
11271
|
+
verbose = false,
|
|
11272
|
+
timeout = 6e5
|
|
11273
|
+
// 10 minutes
|
|
11274
|
+
} = options;
|
|
11275
|
+
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
11276
|
+
if (verbose) {
|
|
11277
|
+
console.log("\u2713 Python environment already set up");
|
|
11278
|
+
}
|
|
11279
|
+
return {
|
|
11280
|
+
success: true,
|
|
11281
|
+
message: "Python environment already exists",
|
|
11282
|
+
alreadyExists: true
|
|
11283
|
+
};
|
|
11284
|
+
}
|
|
11285
|
+
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
11286
|
+
if (!existsSync2(setupScriptPath)) {
|
|
11287
|
+
return {
|
|
11288
|
+
success: false,
|
|
11289
|
+
message: `Setup script not found at: ${setupScriptPath}`,
|
|
11290
|
+
alreadyExists: false
|
|
11291
|
+
};
|
|
11292
|
+
}
|
|
11293
|
+
try {
|
|
11294
|
+
if (verbose) {
|
|
11295
|
+
console.log("Setting up Python environment...");
|
|
11296
|
+
}
|
|
11297
|
+
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
11298
|
+
cwd: packageRoot,
|
|
11299
|
+
timeout,
|
|
11300
|
+
env: {
|
|
11301
|
+
...process.env,
|
|
11302
|
+
// Ensure script can write to the directory
|
|
11303
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
11304
|
+
},
|
|
11305
|
+
maxBuffer: 10 * 1024 * 1024
|
|
11306
|
+
// 10MB buffer
|
|
11307
|
+
});
|
|
11308
|
+
const output = stdout + stderr;
|
|
11309
|
+
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
11310
|
+
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
11311
|
+
if (verbose) {
|
|
11312
|
+
console.log(output);
|
|
11313
|
+
}
|
|
11314
|
+
return {
|
|
11315
|
+
success: true,
|
|
11316
|
+
message: "Python environment set up successfully",
|
|
11317
|
+
alreadyExists: false,
|
|
11318
|
+
pythonVersion,
|
|
11319
|
+
output
|
|
11320
|
+
};
|
|
11321
|
+
} catch (error) {
|
|
11322
|
+
const errorOutput = error.stdout + error.stderr;
|
|
11323
|
+
return {
|
|
11324
|
+
success: false,
|
|
11325
|
+
message: `Setup failed: ${error.message}`,
|
|
11326
|
+
alreadyExists: false,
|
|
11327
|
+
output: errorOutput
|
|
11328
|
+
};
|
|
11329
|
+
}
|
|
11330
|
+
}
|
|
11331
|
+
function getPythonSetupInstructions() {
|
|
11332
|
+
return `
|
|
11333
|
+
Python environment not set up. Please run one of the following commands:
|
|
11334
|
+
|
|
11335
|
+
Option 1 (Automatic):
|
|
11336
|
+
import { setupPythonEnvironment } from '@exulu/backend';
|
|
11337
|
+
await setupPythonEnvironment();
|
|
11338
|
+
|
|
11339
|
+
Option 2 (Manual - for package consumers):
|
|
11340
|
+
npx @exulu/backend setup-python
|
|
11341
|
+
|
|
11342
|
+
Option 3 (Manual - for contributors):
|
|
11343
|
+
npm run python:setup
|
|
11344
|
+
|
|
11345
|
+
These commands will automatically create a Python virtual environment (.venv)
|
|
11346
|
+
in the @exulu/backend package and install all required dependencies.
|
|
11347
|
+
|
|
11348
|
+
Requirements:
|
|
11349
|
+
- Python 3.10 or higher must be installed
|
|
11350
|
+
- pip must be available
|
|
11351
|
+
- venv module must be available (for creating virtual environments)
|
|
11352
|
+
|
|
11353
|
+
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
11354
|
+
- macOS: brew install python@3.12
|
|
11355
|
+
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
11356
|
+
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
11357
|
+
- Windows: Download from https://www.python.org/downloads/
|
|
11358
|
+
|
|
11359
|
+
Note: In Docker containers, ensure you install all three components:
|
|
11360
|
+
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
11361
|
+
Alpine: apk add python3 py3-pip python3-dev
|
|
11362
|
+
`.trim();
|
|
11363
|
+
}
|
|
11364
|
+
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
11365
|
+
const root = packageRoot ?? getPackageRoot();
|
|
11366
|
+
const venvPath = getVenvPath(root);
|
|
11367
|
+
const pythonPath = join3(venvPath, "bin", "python");
|
|
11368
|
+
if (!existsSync2(venvPath)) {
|
|
11369
|
+
return {
|
|
11370
|
+
valid: false,
|
|
11371
|
+
message: getPythonSetupInstructions()
|
|
10576
11372
|
};
|
|
10577
11373
|
}
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
import * as nodemailer from "nodemailer";
|
|
10583
|
-
import { z as z8 } from "zod";
|
|
10584
|
-
var transporter = null;
|
|
10585
|
-
function getTransporter(config) {
|
|
10586
|
-
if (!transporter) {
|
|
10587
|
-
transporter = nodemailer.createTransport(config);
|
|
10588
|
-
}
|
|
10589
|
-
return transporter;
|
|
10590
|
-
}
|
|
10591
|
-
async function sendEmail(recipient, subject, html, text, config) {
|
|
10592
|
-
const transport = getTransporter(config);
|
|
10593
|
-
html = html.trim();
|
|
10594
|
-
text = text.trim();
|
|
10595
|
-
await transport.sendMail({
|
|
10596
|
-
from: config.from,
|
|
10597
|
-
to: recipient,
|
|
10598
|
-
subject,
|
|
10599
|
-
text,
|
|
10600
|
-
html
|
|
10601
|
-
});
|
|
10602
|
-
}
|
|
10603
|
-
var emailTool = new ExuluTool({
|
|
10604
|
-
id: "email",
|
|
10605
|
-
name: "Email",
|
|
10606
|
-
description: "Send an email.",
|
|
10607
|
-
inputSchema: z8.object({
|
|
10608
|
-
recipient: z8.string().describe("The recipient of the email."),
|
|
10609
|
-
subject: z8.string().describe("The subject of the email."),
|
|
10610
|
-
html: z8.string().describe("The HTML body of the email."),
|
|
10611
|
-
text: z8.string().describe("The text body of the email.")
|
|
10612
|
-
}),
|
|
10613
|
-
type: "function",
|
|
10614
|
-
config: [{
|
|
10615
|
-
name: "smtp_host",
|
|
10616
|
-
description: "The SMTP host to send the email from.",
|
|
10617
|
-
type: "variable",
|
|
10618
|
-
default: void 0
|
|
10619
|
-
}, {
|
|
10620
|
-
name: "smtp_port",
|
|
10621
|
-
description: "The SMTP port to send the email from.",
|
|
10622
|
-
type: "variable",
|
|
10623
|
-
default: void 0
|
|
10624
|
-
}, {
|
|
10625
|
-
name: "smtp_user",
|
|
10626
|
-
description: "The SMTP user to send the email from.",
|
|
10627
|
-
type: "variable",
|
|
10628
|
-
default: void 0
|
|
10629
|
-
}, {
|
|
10630
|
-
name: "smtp_password",
|
|
10631
|
-
description: "The SMTP password to send the email from.",
|
|
10632
|
-
type: "variable",
|
|
10633
|
-
default: void 0
|
|
10634
|
-
}, {
|
|
10635
|
-
name: "smtp_from",
|
|
10636
|
-
description: "The SMTP from address to send the email from.",
|
|
10637
|
-
type: "variable",
|
|
10638
|
-
default: void 0
|
|
10639
|
-
}, {
|
|
10640
|
-
name: "allowed_recipient_domains",
|
|
10641
|
-
description: "A comma seperated list of allowed recipient domains to send emails to.",
|
|
10642
|
-
type: "string",
|
|
10643
|
-
default: void 0
|
|
10644
|
-
}],
|
|
10645
|
-
execute: async ({ recipient, subject, html, text, toolVariablesConfig }) => {
|
|
10646
|
-
const EMAIL_CONFIG = {
|
|
10647
|
-
host: toolVariablesConfig.smtp_host || process.env.SMTP_HOST || "",
|
|
10648
|
-
port: parseInt(toolVariablesConfig.smtp_port || process.env.SMTP_PORT || "587", 10),
|
|
10649
|
-
secure: toolVariablesConfig.smtp_secure === "true" || process.env.SMTP_SECURE === "true",
|
|
10650
|
-
// true for 465, false for other ports
|
|
10651
|
-
auth: {
|
|
10652
|
-
user: toolVariablesConfig.smtp_user || process.env.SMTP_USER || "",
|
|
10653
|
-
pass: toolVariablesConfig.smtp_password || process.env.SMTP_PASSWORD || ""
|
|
10654
|
-
},
|
|
10655
|
-
from: toolVariablesConfig.smtp_from || process.env.SMTP_FROM || "",
|
|
10656
|
-
// Allow self-signed certificates if SMTP_REJECT_UNAUTHORIZED is set to 'false'
|
|
10657
|
-
tls: {
|
|
10658
|
-
rejectUnauthorized: false
|
|
10659
|
-
}
|
|
11374
|
+
if (!existsSync2(pythonPath)) {
|
|
11375
|
+
return {
|
|
11376
|
+
valid: false,
|
|
11377
|
+
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
10660
11378
|
};
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
result: "Recipient domain not allowed to send emails to."
|
|
10666
|
-
};
|
|
10667
|
-
}
|
|
10668
|
-
}
|
|
10669
|
-
await sendEmail(recipient, subject, html, text, EMAIL_CONFIG);
|
|
11379
|
+
}
|
|
11380
|
+
try {
|
|
11381
|
+
await execAsync2(`"${pythonPath}" --version`, { cwd: root });
|
|
11382
|
+
} catch {
|
|
10670
11383
|
return {
|
|
10671
|
-
|
|
11384
|
+
valid: false,
|
|
11385
|
+
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
10672
11386
|
};
|
|
10673
11387
|
}
|
|
10674
|
-
|
|
11388
|
+
if (checkPackages) {
|
|
11389
|
+
const criticalPackages = ["docling", "transformers"];
|
|
11390
|
+
const missingPackages = [];
|
|
11391
|
+
for (const pkg of criticalPackages) {
|
|
11392
|
+
try {
|
|
11393
|
+
await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
11394
|
+
cwd: root,
|
|
11395
|
+
timeout: 1e4
|
|
11396
|
+
// 10 second timeout per import check
|
|
11397
|
+
});
|
|
11398
|
+
} catch {
|
|
11399
|
+
missingPackages.push(pkg);
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
if (missingPackages.length > 0) {
|
|
11403
|
+
return {
|
|
11404
|
+
valid: false,
|
|
11405
|
+
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
10675
11406
|
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
11407
|
+
This usually happens when:
|
|
11408
|
+
1. The .venv folder was copied but dependencies were not installed
|
|
11409
|
+
2. The package was installed via npm but setup script was not run
|
|
11410
|
+
|
|
11411
|
+
Please run:
|
|
11412
|
+
await setupPythonEnvironment({ force: true })
|
|
11413
|
+
|
|
11414
|
+
Or manually run the setup script:
|
|
11415
|
+
bash ` + getSetupScriptPath(root)
|
|
11416
|
+
};
|
|
11417
|
+
}
|
|
11418
|
+
}
|
|
11419
|
+
return {
|
|
11420
|
+
valid: true,
|
|
11421
|
+
message: "Python environment is valid"
|
|
11422
|
+
};
|
|
11423
|
+
}
|
|
10683
11424
|
|
|
10684
11425
|
// src/exulu/app/index.ts
|
|
10685
11426
|
var isDev = process.env.NODE_ENV !== "production";
|
|
@@ -10773,12 +11514,17 @@ var ExuluApp = class {
|
|
|
10773
11514
|
...providers ?? []
|
|
10774
11515
|
];
|
|
10775
11516
|
this._config = config;
|
|
11517
|
+
const transcriptionTools = [];
|
|
11518
|
+
if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
|
|
11519
|
+
transcriptionTools.push(transcribeTool);
|
|
11520
|
+
}
|
|
10776
11521
|
this._tools = [
|
|
10777
11522
|
...tools ?? [],
|
|
10778
11523
|
...todoTools,
|
|
10779
11524
|
...questionTools,
|
|
10780
11525
|
...perplexityTools,
|
|
10781
|
-
emailTool
|
|
11526
|
+
emailTool,
|
|
11527
|
+
...transcriptionTools
|
|
10782
11528
|
// Because agents are stored in the database, we add those as tools
|
|
10783
11529
|
// at request time, not during ExuluApp initialization. We add them
|
|
10784
11530
|
// in the grahql tools resolver.
|
|
@@ -10795,9 +11541,9 @@ var ExuluApp = class {
|
|
|
10795
11541
|
id: provider.id ?? "",
|
|
10796
11542
|
type: "agent"
|
|
10797
11543
|
})),
|
|
10798
|
-
...this._tools.map((
|
|
10799
|
-
name:
|
|
10800
|
-
id:
|
|
11544
|
+
...this._tools.map((tool2) => ({
|
|
11545
|
+
name: tool2.name ?? "",
|
|
11546
|
+
id: tool2.id ?? "",
|
|
10801
11547
|
type: "tool"
|
|
10802
11548
|
})),
|
|
10803
11549
|
...this._rerankers.map((reranker) => ({
|
|
@@ -10839,6 +11585,21 @@ var ExuluApp = class {
|
|
|
10839
11585
|
await reportSystemDependencies({
|
|
10840
11586
|
requireSystemDependencies: config.requireSystemDependencies !== false
|
|
10841
11587
|
});
|
|
11588
|
+
if (process.env.TRANSCRIPTION_MODEL && !isLiteLLMEnabled()) {
|
|
11589
|
+
console.warn(
|
|
11590
|
+
"[EXULU] TRANSCRIPTION_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /transcribe endpoint will return 503 until LiteLLM is enabled."
|
|
11591
|
+
);
|
|
11592
|
+
}
|
|
11593
|
+
if (process.env.TTS_MODEL && !isLiteLLMEnabled()) {
|
|
11594
|
+
console.warn(
|
|
11595
|
+
"[EXULU] TTS_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /speech endpoint will return 503 until LiteLLM is enabled."
|
|
11596
|
+
);
|
|
11597
|
+
}
|
|
11598
|
+
if (process.env.TTS_MODEL && !process.env.TTS_VOICE) {
|
|
11599
|
+
console.warn(
|
|
11600
|
+
"[EXULU] TTS_MODEL is set but TTS_VOICE is not. LiteLLM's router requires a voice for /v1/audio/speech; the /speech endpoint will return 503 until TTS_VOICE is set (e.g. 'alloy' for OpenAI tts-1)."
|
|
11601
|
+
);
|
|
11602
|
+
}
|
|
10842
11603
|
console.log("[EXULU] App initialized.");
|
|
10843
11604
|
exuluApp.set(this);
|
|
10844
11605
|
return this;
|
|
@@ -10850,6 +11611,18 @@ var ExuluApp = class {
|
|
|
10850
11611
|
await this.server.express.init();
|
|
10851
11612
|
console.log("[EXULU] Express app initialized.");
|
|
10852
11613
|
}
|
|
11614
|
+
if (isLiteLLMEnabled()) {
|
|
11615
|
+
const packageRoot = getPackageRoot();
|
|
11616
|
+
setLiteLLMPackageRoot(packageRoot);
|
|
11617
|
+
try {
|
|
11618
|
+
await startLiteLLMSupervisor();
|
|
11619
|
+
} catch (err) {
|
|
11620
|
+
console.error(
|
|
11621
|
+
"[EXULU] LiteLLM supervisor failed to start:",
|
|
11622
|
+
err.message
|
|
11623
|
+
);
|
|
11624
|
+
}
|
|
11625
|
+
}
|
|
10853
11626
|
return this._expressApp;
|
|
10854
11627
|
}
|
|
10855
11628
|
};
|
|
@@ -12062,7 +12835,7 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
|
|
|
12062
12835
|
};
|
|
12063
12836
|
|
|
12064
12837
|
// src/exulu/embedder.ts
|
|
12065
|
-
import
|
|
12838
|
+
import CryptoJS6 from "crypto-js";
|
|
12066
12839
|
var ExuluEmbedder = class {
|
|
12067
12840
|
id;
|
|
12068
12841
|
name;
|
|
@@ -12133,8 +12906,8 @@ var ExuluEmbedder = class {
|
|
|
12133
12906
|
);
|
|
12134
12907
|
}
|
|
12135
12908
|
try {
|
|
12136
|
-
const bytes =
|
|
12137
|
-
const decrypted = bytes.toString(
|
|
12909
|
+
const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
12910
|
+
const decrypted = bytes.toString(CryptoJS6.enc.Utf8);
|
|
12138
12911
|
if (!decrypted) {
|
|
12139
12912
|
throw new Error("Decryption returned empty string - invalid key or corrupted data");
|
|
12140
12913
|
}
|
|
@@ -12610,7 +13383,7 @@ var SentenceChunker = class _SentenceChunker extends BaseChunker {
|
|
|
12610
13383
|
}
|
|
12611
13384
|
};
|
|
12612
13385
|
|
|
12613
|
-
// src/postgres/init-db.ts
|
|
13386
|
+
// src/postgres/init-exulu-db.ts
|
|
12614
13387
|
var {
|
|
12615
13388
|
agentsSchema: agentsSchema2,
|
|
12616
13389
|
feedbackSchema: feedbackSchema2,
|
|
@@ -12620,6 +13393,7 @@ var {
|
|
|
12620
13393
|
agentSessionsSchema: agentSessionsSchema2,
|
|
12621
13394
|
platformConfigurationsSchema: platformConfigurationsSchema2,
|
|
12622
13395
|
agentMessagesSchema: agentMessagesSchema2,
|
|
13396
|
+
modelsSchema: modelsSchema2,
|
|
12623
13397
|
rolesSchema: rolesSchema2,
|
|
12624
13398
|
usersSchema: usersSchema2,
|
|
12625
13399
|
skillsSchema: skillsSchema2,
|
|
@@ -12659,6 +13433,7 @@ var up = async function(knex) {
|
|
|
12659
13433
|
const schemas = [
|
|
12660
13434
|
agentSessionsSchema2(),
|
|
12661
13435
|
agentMessagesSchema2(),
|
|
13436
|
+
modelsSchema2(),
|
|
12662
13437
|
rolesSchema2(),
|
|
12663
13438
|
testCasesSchema2(),
|
|
12664
13439
|
evalSetsSchema2(),
|
|
@@ -12702,6 +13477,47 @@ var up = async function(knex) {
|
|
|
12702
13477
|
console.log(`[EXULU] Creating ${schema.name.plural} table.`, schema.fields);
|
|
12703
13478
|
await createTable(schema);
|
|
12704
13479
|
}
|
|
13480
|
+
const hasOldProviderCol = await knex.schema.hasColumn("agents", "provider");
|
|
13481
|
+
const hasOldKeyCol = await knex.schema.hasColumn("agents", "providerapikey");
|
|
13482
|
+
if (hasOldProviderCol || hasOldKeyCol) {
|
|
13483
|
+
console.log("[EXULU] Migrating agents.provider/providerapikey -> models table.");
|
|
13484
|
+
await knex.transaction(async (trx) => {
|
|
13485
|
+
const pairs = await trx("agents").distinct("provider", "providerapikey").whereNotNull("provider");
|
|
13486
|
+
const pairToModelId = /* @__PURE__ */ new Map();
|
|
13487
|
+
for (const { provider, providerapikey } of pairs) {
|
|
13488
|
+
const inserted = await trx("models").insert({
|
|
13489
|
+
name: `${provider}${providerapikey ? ` (${providerapikey})` : ""}`,
|
|
13490
|
+
provider,
|
|
13491
|
+
authvariable: providerapikey,
|
|
13492
|
+
active: true,
|
|
13493
|
+
rights_mode: "public",
|
|
13494
|
+
created_by: 1
|
|
13495
|
+
}).returning("id");
|
|
13496
|
+
const id = inserted[0]?.id;
|
|
13497
|
+
if (!id) {
|
|
13498
|
+
throw new Error("[EXULU] Migration: failed to insert models row");
|
|
13499
|
+
}
|
|
13500
|
+
pairToModelId.set(`${provider}::${providerapikey ?? ""}`, id);
|
|
13501
|
+
}
|
|
13502
|
+
for (const [key2, modelId] of pairToModelId) {
|
|
13503
|
+
const [provider, providerapikey] = key2.split("::");
|
|
13504
|
+
const where = {
|
|
13505
|
+
provider,
|
|
13506
|
+
providerapikey: providerapikey ? providerapikey : null
|
|
13507
|
+
};
|
|
13508
|
+
await trx("agents").where(where).update({ model: modelId });
|
|
13509
|
+
}
|
|
13510
|
+
if (hasOldProviderCol) {
|
|
13511
|
+
await trx.schema.alterTable("agents", (t) => t.dropColumn("provider"));
|
|
13512
|
+
}
|
|
13513
|
+
if (hasOldKeyCol) {
|
|
13514
|
+
await trx.schema.alterTable("agents", (t) => t.dropColumn("providerapikey"));
|
|
13515
|
+
}
|
|
13516
|
+
console.log(
|
|
13517
|
+
`[EXULU] Migrated ${pairToModelId.size} unique provider+key pairs into models.`
|
|
13518
|
+
);
|
|
13519
|
+
});
|
|
13520
|
+
}
|
|
12705
13521
|
if (!await knex.schema.hasTable("verification_token")) {
|
|
12706
13522
|
console.log("[EXULU] Creating verification_token table.");
|
|
12707
13523
|
await knex.schema.createTable("verification_token", (table) => {
|
|
@@ -12799,39 +13615,310 @@ var execute = async ({ contexts }) => {
|
|
|
12799
13615
|
} else {
|
|
12800
13616
|
adminRoleId = existingAdminRole.id;
|
|
12801
13617
|
}
|
|
12802
|
-
if (!existingDefaultRole) {
|
|
12803
|
-
console.log("[EXULU] Creating default role.");
|
|
12804
|
-
await db.from("roles").insert({
|
|
12805
|
-
name: "default",
|
|
12806
|
-
agents: "write",
|
|
12807
|
-
api: "read",
|
|
12808
|
-
workflows: "read",
|
|
12809
|
-
variables: "read",
|
|
12810
|
-
users: "read",
|
|
12811
|
-
evals: "read"
|
|
12812
|
-
}).returning("id");
|
|
13618
|
+
if (!existingDefaultRole) {
|
|
13619
|
+
console.log("[EXULU] Creating default role.");
|
|
13620
|
+
await db.from("roles").insert({
|
|
13621
|
+
name: "default",
|
|
13622
|
+
agents: "write",
|
|
13623
|
+
api: "read",
|
|
13624
|
+
workflows: "read",
|
|
13625
|
+
variables: "read",
|
|
13626
|
+
users: "read",
|
|
13627
|
+
evals: "read"
|
|
13628
|
+
}).returning("id");
|
|
13629
|
+
}
|
|
13630
|
+
const existingUser = await db.from("users").where({ email: "admin@exulu.com" }).first();
|
|
13631
|
+
if (!existingUser) {
|
|
13632
|
+
const password = await encryptString("admin");
|
|
13633
|
+
console.log("[EXULU] Creating default admin user.");
|
|
13634
|
+
await db.from("users").insert({
|
|
13635
|
+
name: "exulu",
|
|
13636
|
+
email: "admin@exulu.com",
|
|
13637
|
+
super_admin: true,
|
|
13638
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
13639
|
+
emailVerified: /* @__PURE__ */ new Date(),
|
|
13640
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
13641
|
+
password,
|
|
13642
|
+
type: "user",
|
|
13643
|
+
role: adminRoleId
|
|
13644
|
+
});
|
|
13645
|
+
}
|
|
13646
|
+
const { key: key2 } = await generateApiKey("exulu", "api@exulu.com");
|
|
13647
|
+
console.log("[EXULU] Database initialized.");
|
|
13648
|
+
console.log("[EXULU] Default api key: ", `${key2}`);
|
|
13649
|
+
console.log("[EXULU] Default password if using password auth: ", `admin`);
|
|
13650
|
+
console.log("[EXULU] Default email if using password auth: ", `admin@exulu.com`);
|
|
13651
|
+
return;
|
|
13652
|
+
};
|
|
13653
|
+
|
|
13654
|
+
// src/exulu/litellm/db-init.ts
|
|
13655
|
+
import { existsSync as existsSync4 } from "fs";
|
|
13656
|
+
import { resolve as resolve2 } from "path";
|
|
13657
|
+
import { spawnSync } from "child_process";
|
|
13658
|
+
import { Client } from "pg";
|
|
13659
|
+
|
|
13660
|
+
// src/exulu/litellm/db-setup-check.ts
|
|
13661
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
13662
|
+
var readLiteLLMDatabaseUrl = (configPath) => {
|
|
13663
|
+
if (!existsSync3(configPath)) return void 0;
|
|
13664
|
+
const text = readFileSync2(configPath, "utf8");
|
|
13665
|
+
const match = text.match(
|
|
13666
|
+
/^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
|
|
13667
|
+
);
|
|
13668
|
+
if (!match) return void 0;
|
|
13669
|
+
const value = match[1]?.trim();
|
|
13670
|
+
if (!value) return void 0;
|
|
13671
|
+
if (value.startsWith("os.environ/")) {
|
|
13672
|
+
const envName = value.slice("os.environ/".length).trim();
|
|
13673
|
+
return process.env[envName];
|
|
13674
|
+
}
|
|
13675
|
+
return value;
|
|
13676
|
+
};
|
|
13677
|
+
var parsePostgresUrl = (url) => {
|
|
13678
|
+
try {
|
|
13679
|
+
const u = new URL(url);
|
|
13680
|
+
if (u.protocol !== "postgres:" && u.protocol !== "postgresql:") return void 0;
|
|
13681
|
+
return {
|
|
13682
|
+
host: u.hostname,
|
|
13683
|
+
port: u.port ? parseInt(u.port, 10) : 5432,
|
|
13684
|
+
database: u.pathname.replace(/^\//, "")
|
|
13685
|
+
};
|
|
13686
|
+
} catch {
|
|
13687
|
+
return void 0;
|
|
13688
|
+
}
|
|
13689
|
+
};
|
|
13690
|
+
var getExuluPostgresTarget = () => {
|
|
13691
|
+
const host = process.env.POSTGRES_DB_HOST;
|
|
13692
|
+
const database = process.env.POSTGRES_DB_NAME ?? "exulu";
|
|
13693
|
+
if (!host) return void 0;
|
|
13694
|
+
return {
|
|
13695
|
+
host,
|
|
13696
|
+
port: parseInt(process.env.POSTGRES_DB_PORT ?? "5432", 10),
|
|
13697
|
+
database
|
|
13698
|
+
};
|
|
13699
|
+
};
|
|
13700
|
+
var isSameDatabase = (a, b) => a.host === b.host && a.port === b.port && a.database === b.database;
|
|
13701
|
+
var checkLiteLLMDatabaseSafety = (configPath) => {
|
|
13702
|
+
const litellmUrl = readLiteLLMDatabaseUrl(configPath);
|
|
13703
|
+
if (!litellmUrl) return { ok: true, reason: "no-litellm-db-mode" };
|
|
13704
|
+
const litellmTarget = parsePostgresUrl(litellmUrl);
|
|
13705
|
+
if (!litellmTarget) return { ok: false, reason: "unparseable-url", rawUrl: litellmUrl };
|
|
13706
|
+
const exuluTarget = getExuluPostgresTarget();
|
|
13707
|
+
if (exuluTarget && isSameDatabase(litellmTarget, exuluTarget)) {
|
|
13708
|
+
return { ok: false, reason: "shared-with-exulu", litellmTarget, exuluTarget };
|
|
13709
|
+
}
|
|
13710
|
+
return { ok: true, reason: "isolated", litellmTarget };
|
|
13711
|
+
};
|
|
13712
|
+
|
|
13713
|
+
// src/exulu/litellm/db-init.ts
|
|
13714
|
+
var WARNING_BANNER = "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550";
|
|
13715
|
+
var warn = (lines) => {
|
|
13716
|
+
console.warn(`
|
|
13717
|
+
${WARNING_BANNER}`);
|
|
13718
|
+
console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
|
|
13719
|
+
for (const line of lines) console.warn(` ${line}`);
|
|
13720
|
+
console.warn(`${WARNING_BANNER}
|
|
13721
|
+
`);
|
|
13722
|
+
};
|
|
13723
|
+
var log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
13724
|
+
var initLiteLLMDatabase = async (packageRoot) => {
|
|
13725
|
+
const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(packageRoot, "./config.litellm.yaml");
|
|
13726
|
+
const safety = checkLiteLLMDatabaseSafety(configPath);
|
|
13727
|
+
if (safety.ok && safety.reason === "no-litellm-db-mode") return;
|
|
13728
|
+
if (!safety.ok && safety.reason === "unparseable-url") {
|
|
13729
|
+
warn([
|
|
13730
|
+
`LiteLLM's database_url is not a valid postgres URL:`,
|
|
13731
|
+
` ${safety.rawUrl}`,
|
|
13732
|
+
`Expected postgres://user:pass@host:port/database. Skipping setup.`
|
|
13733
|
+
]);
|
|
13734
|
+
return;
|
|
13735
|
+
}
|
|
13736
|
+
if (!safety.ok && safety.reason === "shared-with-exulu") {
|
|
13737
|
+
const { exuluTarget } = safety;
|
|
13738
|
+
warn([
|
|
13739
|
+
`LiteLLM's database_url points to the SAME database Exulu uses:`,
|
|
13740
|
+
` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
|
|
13741
|
+
``,
|
|
13742
|
+
`If LiteLLM's schema sync were to run against this database, it`,
|
|
13743
|
+
`would DROP every table that is not in LiteLLM's Prisma schema,`,
|
|
13744
|
+
`destroying all of Exulu's data. Setup has been SKIPPED.`,
|
|
13745
|
+
``,
|
|
13746
|
+
`Fix: create a dedicated Postgres database for LiteLLM (e.g.`,
|
|
13747
|
+
`\`createdb litellm\`) and change database_url in ${configPath}`,
|
|
13748
|
+
`to point at it. Then restart Exulu.`
|
|
13749
|
+
]);
|
|
13750
|
+
return;
|
|
13751
|
+
}
|
|
13752
|
+
const litellmUrl = readLiteLLMDatabaseUrl(configPath);
|
|
13753
|
+
if (!litellmUrl) {
|
|
13754
|
+
return;
|
|
13755
|
+
}
|
|
13756
|
+
const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
|
|
13757
|
+
log(
|
|
13758
|
+
`LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
|
|
13759
|
+
);
|
|
13760
|
+
const ensureDatabaseExists = async () => {
|
|
13761
|
+
const probe = new Client({ connectionString: litellmUrl });
|
|
13762
|
+
try {
|
|
13763
|
+
await probe.connect();
|
|
13764
|
+
await probe.end();
|
|
13765
|
+
return true;
|
|
13766
|
+
} catch (err) {
|
|
13767
|
+
const code = err?.code;
|
|
13768
|
+
if (code !== "3D000") {
|
|
13769
|
+
warn([
|
|
13770
|
+
`Could not connect to LiteLLM's target database:`,
|
|
13771
|
+
` ${err instanceof Error ? err.message : String(err)}`,
|
|
13772
|
+
``,
|
|
13773
|
+
`Skipping LiteLLM database setup. LiteLLM features that depend on`,
|
|
13774
|
+
`the database will fail at runtime until this is resolved.`
|
|
13775
|
+
]);
|
|
13776
|
+
return false;
|
|
13777
|
+
}
|
|
13778
|
+
const url = new URL(litellmUrl);
|
|
13779
|
+
const targetDbName = url.pathname.replace(/^\//, "");
|
|
13780
|
+
if (!targetDbName) {
|
|
13781
|
+
warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
|
|
13782
|
+
return false;
|
|
13783
|
+
}
|
|
13784
|
+
url.pathname = "/postgres";
|
|
13785
|
+
log(`Target database "${targetDbName}" does not exist; creating it\u2026`);
|
|
13786
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
|
|
13787
|
+
warn([
|
|
13788
|
+
`Refusing to auto-create database "${targetDbName}" \u2014 name`,
|
|
13789
|
+
`contains characters that would require quoting. Create it`,
|
|
13790
|
+
`manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
|
|
13791
|
+
]);
|
|
13792
|
+
return false;
|
|
13793
|
+
}
|
|
13794
|
+
const admin = new Client({ connectionString: url.toString() });
|
|
13795
|
+
try {
|
|
13796
|
+
await admin.connect();
|
|
13797
|
+
await admin.query(`CREATE DATABASE "${targetDbName}"`);
|
|
13798
|
+
log(`\u2713 Created database "${targetDbName}".`);
|
|
13799
|
+
return true;
|
|
13800
|
+
} catch (createErr) {
|
|
13801
|
+
warn([
|
|
13802
|
+
`Failed to auto-create database "${targetDbName}":`,
|
|
13803
|
+
` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
|
|
13804
|
+
``,
|
|
13805
|
+
`The connecting user likely lacks CREATEDB privilege. Create the`,
|
|
13806
|
+
`database manually:`,
|
|
13807
|
+
` createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`,
|
|
13808
|
+
`Then restart Exulu.`
|
|
13809
|
+
]);
|
|
13810
|
+
return false;
|
|
13811
|
+
} finally {
|
|
13812
|
+
try {
|
|
13813
|
+
await admin.end();
|
|
13814
|
+
} catch {
|
|
13815
|
+
}
|
|
13816
|
+
}
|
|
13817
|
+
}
|
|
13818
|
+
};
|
|
13819
|
+
if (!await ensureDatabaseExists()) return;
|
|
13820
|
+
log("Checking that the target database is safe to push into\u2026");
|
|
13821
|
+
const client2 = new Client({ connectionString: litellmUrl });
|
|
13822
|
+
let foreignTables = [];
|
|
13823
|
+
try {
|
|
13824
|
+
await client2.connect();
|
|
13825
|
+
const res = await client2.query(
|
|
13826
|
+
`SELECT table_name
|
|
13827
|
+
FROM information_schema.tables
|
|
13828
|
+
WHERE table_schema = 'public'
|
|
13829
|
+
AND table_type = 'BASE TABLE'
|
|
13830
|
+
AND table_name NOT LIKE 'LiteLLM%'
|
|
13831
|
+
AND table_name <> '_prisma_migrations'
|
|
13832
|
+
ORDER BY table_name;`
|
|
13833
|
+
);
|
|
13834
|
+
foreignTables = res.rows.map((r) => r.table_name);
|
|
13835
|
+
} catch (err) {
|
|
13836
|
+
warn([
|
|
13837
|
+
`Could not query LiteLLM's target database to verify it is safe`,
|
|
13838
|
+
`to push into:`,
|
|
13839
|
+
` ${err instanceof Error ? err.message : String(err)}`,
|
|
13840
|
+
``,
|
|
13841
|
+
`Skipping LiteLLM database setup. LiteLLM features that depend on the`,
|
|
13842
|
+
`database will fail at runtime until this is resolved.`
|
|
13843
|
+
]);
|
|
13844
|
+
return;
|
|
13845
|
+
} finally {
|
|
13846
|
+
try {
|
|
13847
|
+
await client2.end();
|
|
13848
|
+
} catch {
|
|
13849
|
+
}
|
|
13850
|
+
}
|
|
13851
|
+
if (foreignTables.length > 0) {
|
|
13852
|
+
warn([
|
|
13853
|
+
`LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
|
|
13854
|
+
`part of LiteLLM's schema:`,
|
|
13855
|
+
...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
|
|
13856
|
+
...foreignTables.length > 10 ? [` \u2026 and ${foreignTables.length - 10} more`] : [],
|
|
13857
|
+
``,
|
|
13858
|
+
`Refusing to run prisma db push to avoid data loss. Move LiteLLM to a`,
|
|
13859
|
+
`dedicated database, or remove these tables first if they are obsolete.`
|
|
13860
|
+
]);
|
|
13861
|
+
return;
|
|
13862
|
+
}
|
|
13863
|
+
const venvBin = resolve2(packageRoot, "ee/python/.venv/bin");
|
|
13864
|
+
const prismaCli = resolve2(venvBin, "prisma");
|
|
13865
|
+
const litellmProxyDir = resolve2(
|
|
13866
|
+
packageRoot,
|
|
13867
|
+
"ee/python/.venv/lib/python3.12/site-packages/litellm/proxy"
|
|
13868
|
+
);
|
|
13869
|
+
const schemaPath = resolve2(litellmProxyDir, "schema.prisma");
|
|
13870
|
+
if (!existsSync4(prismaCli)) {
|
|
13871
|
+
warn([
|
|
13872
|
+
`Prisma CLI not found at ${prismaCli}.`,
|
|
13873
|
+
`Run \`npm run python:setup\` to create the venv and install prisma.`,
|
|
13874
|
+
`Skipping LiteLLM database setup.`
|
|
13875
|
+
]);
|
|
13876
|
+
return;
|
|
12813
13877
|
}
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
email: "admin@exulu.com",
|
|
12821
|
-
super_admin: true,
|
|
12822
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
12823
|
-
emailVerified: /* @__PURE__ */ new Date(),
|
|
12824
|
-
updatedAt: /* @__PURE__ */ new Date(),
|
|
12825
|
-
password,
|
|
12826
|
-
type: "user",
|
|
12827
|
-
role: adminRoleId
|
|
12828
|
-
});
|
|
13878
|
+
if (!existsSync4(schemaPath)) {
|
|
13879
|
+
warn([
|
|
13880
|
+
`LiteLLM Prisma schema not found at ${schemaPath}.`,
|
|
13881
|
+
`Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
|
|
13882
|
+
]);
|
|
13883
|
+
return;
|
|
12829
13884
|
}
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
13885
|
+
log("Running `prisma db push` against LiteLLM's schema\u2026");
|
|
13886
|
+
const result = spawnSync(prismaCli, ["db", "push", "--skip-generate"], {
|
|
13887
|
+
cwd: litellmProxyDir,
|
|
13888
|
+
env: {
|
|
13889
|
+
...process.env,
|
|
13890
|
+
DATABASE_URL: litellmUrl,
|
|
13891
|
+
PATH: `${venvBin}:${process.env.PATH ?? ""}`
|
|
13892
|
+
},
|
|
13893
|
+
// Capture rather than inherit so the prisma noise doesn't bury other
|
|
13894
|
+
// Exulu boot logs; we'll print stdout/stderr only on failure.
|
|
13895
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13896
|
+
encoding: "utf8"
|
|
13897
|
+
});
|
|
13898
|
+
if (result.error) {
|
|
13899
|
+
warn([
|
|
13900
|
+
`Failed to launch prisma: ${result.error.message}`,
|
|
13901
|
+
`Skipping LiteLLM database setup.`
|
|
13902
|
+
]);
|
|
13903
|
+
return;
|
|
13904
|
+
}
|
|
13905
|
+
if (result.status !== 0) {
|
|
13906
|
+
warn([
|
|
13907
|
+
`prisma db push exited with status ${result.status}.`,
|
|
13908
|
+
`stdout:`,
|
|
13909
|
+
...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
|
|
13910
|
+
`stderr:`,
|
|
13911
|
+
...(result.stderr || "(empty)").split("\n").map((l) => ` ${l}`)
|
|
13912
|
+
]);
|
|
13913
|
+
return;
|
|
13914
|
+
}
|
|
13915
|
+
log("\u2713 LiteLLM database ready.");
|
|
13916
|
+
};
|
|
13917
|
+
|
|
13918
|
+
// src/postgres/init-litellm-db.ts
|
|
13919
|
+
var initLitellmDb = async () => {
|
|
13920
|
+
await initLiteLLMDatabase(getPackageRoot());
|
|
13921
|
+
console.log("[EXULU] LiteLLM database initialized.");
|
|
12835
13922
|
return;
|
|
12836
13923
|
};
|
|
12837
13924
|
|
|
@@ -12878,7 +13965,7 @@ var create = ({
|
|
|
12878
13965
|
};
|
|
12879
13966
|
|
|
12880
13967
|
// src/index.ts
|
|
12881
|
-
import
|
|
13968
|
+
import CryptoJS7 from "crypto-js";
|
|
12882
13969
|
|
|
12883
13970
|
// ee/chunking/markdown.ts
|
|
12884
13971
|
var extractPageTag = (text) => {
|
|
@@ -13358,216 +14445,13 @@ var MarkdownChunker = class {
|
|
|
13358
14445
|
}
|
|
13359
14446
|
};
|
|
13360
14447
|
|
|
13361
|
-
// src/utils/python-setup.ts
|
|
13362
|
-
import { exec as exec2 } from "child_process";
|
|
13363
|
-
import { promisify as promisify2 } from "util";
|
|
13364
|
-
import { resolve, join as join2, dirname } from "path";
|
|
13365
|
-
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
13366
|
-
import { fileURLToPath } from "url";
|
|
13367
|
-
var execAsync2 = promisify2(exec2);
|
|
13368
|
-
function getPackageRoot() {
|
|
13369
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
13370
|
-
let currentDir = dirname(currentFile);
|
|
13371
|
-
let attempts = 0;
|
|
13372
|
-
const maxAttempts = 10;
|
|
13373
|
-
while (attempts < maxAttempts) {
|
|
13374
|
-
const packageJsonPath = join2(currentDir, "package.json");
|
|
13375
|
-
if (existsSync2(packageJsonPath)) {
|
|
13376
|
-
try {
|
|
13377
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
13378
|
-
if (packageJson.name === "@exulu/backend") {
|
|
13379
|
-
return currentDir;
|
|
13380
|
-
}
|
|
13381
|
-
} catch {
|
|
13382
|
-
}
|
|
13383
|
-
}
|
|
13384
|
-
const parentDir = resolve(currentDir, "..");
|
|
13385
|
-
if (parentDir === currentDir) {
|
|
13386
|
-
break;
|
|
13387
|
-
}
|
|
13388
|
-
currentDir = parentDir;
|
|
13389
|
-
attempts++;
|
|
13390
|
-
}
|
|
13391
|
-
const fallback = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13392
|
-
return fallback;
|
|
13393
|
-
}
|
|
13394
|
-
function getSetupScriptPath(packageRoot) {
|
|
13395
|
-
return resolve(packageRoot, "ee/python/setup.sh");
|
|
13396
|
-
}
|
|
13397
|
-
function getVenvPath(packageRoot) {
|
|
13398
|
-
return resolve(packageRoot, "ee/python/.venv");
|
|
13399
|
-
}
|
|
13400
|
-
function isPythonEnvironmentSetup(packageRoot) {
|
|
13401
|
-
const root = packageRoot ?? getPackageRoot();
|
|
13402
|
-
const venvPath = getVenvPath(root);
|
|
13403
|
-
const pythonPath = join2(venvPath, "bin", "python");
|
|
13404
|
-
return existsSync2(venvPath) && existsSync2(pythonPath);
|
|
13405
|
-
}
|
|
13406
|
-
async function setupPythonEnvironment(options = {}) {
|
|
13407
|
-
const {
|
|
13408
|
-
packageRoot = getPackageRoot(),
|
|
13409
|
-
force = false,
|
|
13410
|
-
verbose = false,
|
|
13411
|
-
timeout = 6e5
|
|
13412
|
-
// 10 minutes
|
|
13413
|
-
} = options;
|
|
13414
|
-
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
13415
|
-
if (verbose) {
|
|
13416
|
-
console.log("\u2713 Python environment already set up");
|
|
13417
|
-
}
|
|
13418
|
-
return {
|
|
13419
|
-
success: true,
|
|
13420
|
-
message: "Python environment already exists",
|
|
13421
|
-
alreadyExists: true
|
|
13422
|
-
};
|
|
13423
|
-
}
|
|
13424
|
-
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
13425
|
-
if (!existsSync2(setupScriptPath)) {
|
|
13426
|
-
return {
|
|
13427
|
-
success: false,
|
|
13428
|
-
message: `Setup script not found at: ${setupScriptPath}`,
|
|
13429
|
-
alreadyExists: false
|
|
13430
|
-
};
|
|
13431
|
-
}
|
|
13432
|
-
try {
|
|
13433
|
-
if (verbose) {
|
|
13434
|
-
console.log("Setting up Python environment...");
|
|
13435
|
-
}
|
|
13436
|
-
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
13437
|
-
cwd: packageRoot,
|
|
13438
|
-
timeout,
|
|
13439
|
-
env: {
|
|
13440
|
-
...process.env,
|
|
13441
|
-
// Ensure script can write to the directory
|
|
13442
|
-
PYTHONDONTWRITEBYTECODE: "1"
|
|
13443
|
-
},
|
|
13444
|
-
maxBuffer: 10 * 1024 * 1024
|
|
13445
|
-
// 10MB buffer
|
|
13446
|
-
});
|
|
13447
|
-
const output = stdout + stderr;
|
|
13448
|
-
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
13449
|
-
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
13450
|
-
if (verbose) {
|
|
13451
|
-
console.log(output);
|
|
13452
|
-
}
|
|
13453
|
-
return {
|
|
13454
|
-
success: true,
|
|
13455
|
-
message: "Python environment set up successfully",
|
|
13456
|
-
alreadyExists: false,
|
|
13457
|
-
pythonVersion,
|
|
13458
|
-
output
|
|
13459
|
-
};
|
|
13460
|
-
} catch (error) {
|
|
13461
|
-
const errorOutput = error.stdout + error.stderr;
|
|
13462
|
-
return {
|
|
13463
|
-
success: false,
|
|
13464
|
-
message: `Setup failed: ${error.message}`,
|
|
13465
|
-
alreadyExists: false,
|
|
13466
|
-
output: errorOutput
|
|
13467
|
-
};
|
|
13468
|
-
}
|
|
13469
|
-
}
|
|
13470
|
-
function getPythonSetupInstructions() {
|
|
13471
|
-
return `
|
|
13472
|
-
Python environment not set up. Please run one of the following commands:
|
|
13473
|
-
|
|
13474
|
-
Option 1 (Automatic):
|
|
13475
|
-
import { setupPythonEnvironment } from '@exulu/backend';
|
|
13476
|
-
await setupPythonEnvironment();
|
|
13477
|
-
|
|
13478
|
-
Option 2 (Manual - for package consumers):
|
|
13479
|
-
npx @exulu/backend setup-python
|
|
13480
|
-
|
|
13481
|
-
Option 3 (Manual - for contributors):
|
|
13482
|
-
npm run python:setup
|
|
13483
|
-
|
|
13484
|
-
These commands will automatically create a Python virtual environment (.venv)
|
|
13485
|
-
in the @exulu/backend package and install all required dependencies.
|
|
13486
|
-
|
|
13487
|
-
Requirements:
|
|
13488
|
-
- Python 3.10 or higher must be installed
|
|
13489
|
-
- pip must be available
|
|
13490
|
-
- venv module must be available (for creating virtual environments)
|
|
13491
|
-
|
|
13492
|
-
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
13493
|
-
- macOS: brew install python@3.12
|
|
13494
|
-
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
13495
|
-
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
13496
|
-
- Windows: Download from https://www.python.org/downloads/
|
|
13497
|
-
|
|
13498
|
-
Note: In Docker containers, ensure you install all three components:
|
|
13499
|
-
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
13500
|
-
Alpine: apk add python3 py3-pip python3-dev
|
|
13501
|
-
`.trim();
|
|
13502
|
-
}
|
|
13503
|
-
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
13504
|
-
const root = packageRoot ?? getPackageRoot();
|
|
13505
|
-
const venvPath = getVenvPath(root);
|
|
13506
|
-
const pythonPath = join2(venvPath, "bin", "python");
|
|
13507
|
-
if (!existsSync2(venvPath)) {
|
|
13508
|
-
return {
|
|
13509
|
-
valid: false,
|
|
13510
|
-
message: getPythonSetupInstructions()
|
|
13511
|
-
};
|
|
13512
|
-
}
|
|
13513
|
-
if (!existsSync2(pythonPath)) {
|
|
13514
|
-
return {
|
|
13515
|
-
valid: false,
|
|
13516
|
-
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
13517
|
-
};
|
|
13518
|
-
}
|
|
13519
|
-
try {
|
|
13520
|
-
await execAsync2(`"${pythonPath}" --version`, { cwd: root });
|
|
13521
|
-
} catch {
|
|
13522
|
-
return {
|
|
13523
|
-
valid: false,
|
|
13524
|
-
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
13525
|
-
};
|
|
13526
|
-
}
|
|
13527
|
-
if (checkPackages) {
|
|
13528
|
-
const criticalPackages = ["docling", "transformers"];
|
|
13529
|
-
const missingPackages = [];
|
|
13530
|
-
for (const pkg of criticalPackages) {
|
|
13531
|
-
try {
|
|
13532
|
-
await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
13533
|
-
cwd: root,
|
|
13534
|
-
timeout: 1e4
|
|
13535
|
-
// 10 second timeout per import check
|
|
13536
|
-
});
|
|
13537
|
-
} catch {
|
|
13538
|
-
missingPackages.push(pkg);
|
|
13539
|
-
}
|
|
13540
|
-
}
|
|
13541
|
-
if (missingPackages.length > 0) {
|
|
13542
|
-
return {
|
|
13543
|
-
valid: false,
|
|
13544
|
-
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
13545
|
-
|
|
13546
|
-
This usually happens when:
|
|
13547
|
-
1. The .venv folder was copied but dependencies were not installed
|
|
13548
|
-
2. The package was installed via npm but setup script was not run
|
|
13549
|
-
|
|
13550
|
-
Please run:
|
|
13551
|
-
await setupPythonEnvironment({ force: true })
|
|
13552
|
-
|
|
13553
|
-
Or manually run the setup script:
|
|
13554
|
-
bash ` + getSetupScriptPath(root)
|
|
13555
|
-
};
|
|
13556
|
-
}
|
|
13557
|
-
}
|
|
13558
|
-
return {
|
|
13559
|
-
valid: true,
|
|
13560
|
-
message: "Python environment is valid"
|
|
13561
|
-
};
|
|
13562
|
-
}
|
|
13563
|
-
|
|
13564
14448
|
// ee/python/documents/processing/doc_processor.ts
|
|
13565
|
-
import * as
|
|
14449
|
+
import * as fs4 from "fs";
|
|
13566
14450
|
import * as path from "path";
|
|
13567
|
-
import { generateText as
|
|
13568
|
-
import { z as
|
|
14451
|
+
import { generateText as generateText5, Output as Output2 } from "ai";
|
|
14452
|
+
import { z as z11 } from "zod";
|
|
13569
14453
|
import pLimit from "p-limit";
|
|
13570
|
-
import { randomUUID as
|
|
14454
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
13571
14455
|
import * as mammoth from "mammoth";
|
|
13572
14456
|
import TurndownService from "turndown";
|
|
13573
14457
|
import WordExtractor from "word-extractor";
|
|
@@ -13576,34 +14460,34 @@ import { parseOfficeAsync as parseOfficeAsync2 } from "officeparser";
|
|
|
13576
14460
|
// src/utils/python-executor.ts
|
|
13577
14461
|
import { exec as exec3 } from "child_process";
|
|
13578
14462
|
import { promisify as promisify3 } from "util";
|
|
13579
|
-
import { resolve as
|
|
13580
|
-
import { existsSync as
|
|
14463
|
+
import { resolve as resolve3, join as join4, dirname as dirname3 } from "path";
|
|
14464
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
13581
14465
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13582
14466
|
var execAsync3 = promisify3(exec3);
|
|
13583
14467
|
function getPackageRoot2() {
|
|
13584
14468
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
13585
|
-
let currentDir =
|
|
14469
|
+
let currentDir = dirname3(currentFile);
|
|
13586
14470
|
let attempts = 0;
|
|
13587
14471
|
const maxAttempts = 10;
|
|
13588
14472
|
while (attempts < maxAttempts) {
|
|
13589
|
-
const packageJsonPath =
|
|
13590
|
-
if (
|
|
14473
|
+
const packageJsonPath = join4(currentDir, "package.json");
|
|
14474
|
+
if (existsSync5(packageJsonPath)) {
|
|
13591
14475
|
try {
|
|
13592
|
-
const packageJson = JSON.parse(
|
|
14476
|
+
const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
13593
14477
|
if (packageJson.name === "@exulu/backend") {
|
|
13594
14478
|
return currentDir;
|
|
13595
14479
|
}
|
|
13596
14480
|
} catch {
|
|
13597
14481
|
}
|
|
13598
14482
|
}
|
|
13599
|
-
const parentDir =
|
|
14483
|
+
const parentDir = resolve3(currentDir, "..");
|
|
13600
14484
|
if (parentDir === currentDir) {
|
|
13601
14485
|
break;
|
|
13602
14486
|
}
|
|
13603
14487
|
currentDir = parentDir;
|
|
13604
14488
|
attempts++;
|
|
13605
14489
|
}
|
|
13606
|
-
return
|
|
14490
|
+
return resolve3(dirname3(fileURLToPath2(import.meta.url)), "../..");
|
|
13607
14491
|
}
|
|
13608
14492
|
var PythonEnvironmentError = class extends Error {
|
|
13609
14493
|
constructor(message) {
|
|
@@ -13624,11 +14508,11 @@ var PythonExecutionError = class extends Error {
|
|
|
13624
14508
|
}
|
|
13625
14509
|
};
|
|
13626
14510
|
function getVenvPath2(packageRoot) {
|
|
13627
|
-
return
|
|
14511
|
+
return resolve3(packageRoot, "ee/python/.venv");
|
|
13628
14512
|
}
|
|
13629
14513
|
function getPythonExecutable(packageRoot) {
|
|
13630
14514
|
const venvPath = getVenvPath2(packageRoot);
|
|
13631
|
-
return
|
|
14515
|
+
return join4(venvPath, "bin", "python");
|
|
13632
14516
|
}
|
|
13633
14517
|
async function validatePythonEnvironmentForExecution(packageRoot) {
|
|
13634
14518
|
const validation = await validatePythonEnvironment(packageRoot);
|
|
@@ -13650,8 +14534,8 @@ async function executePythonScript(config) {
|
|
|
13650
14534
|
if (validateEnvironment) {
|
|
13651
14535
|
await validatePythonEnvironmentForExecution(packageRoot);
|
|
13652
14536
|
}
|
|
13653
|
-
const resolvedScriptPath =
|
|
13654
|
-
if (!
|
|
14537
|
+
const resolvedScriptPath = resolve3(packageRoot, scriptPath);
|
|
14538
|
+
if (!existsSync5(resolvedScriptPath)) {
|
|
13655
14539
|
throw new PythonExecutionError(
|
|
13656
14540
|
`Python script not found: ${resolvedScriptPath}`,
|
|
13657
14541
|
"",
|
|
@@ -13746,9 +14630,9 @@ async function processWord(file) {
|
|
|
13746
14630
|
}
|
|
13747
14631
|
async function processImage(buffer, paths, config, verbose = false) {
|
|
13748
14632
|
try {
|
|
13749
|
-
await
|
|
14633
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
13750
14634
|
const imagePath = path.join(paths.images, "1.png");
|
|
13751
|
-
await
|
|
14635
|
+
await fs4.promises.writeFile(imagePath, buffer);
|
|
13752
14636
|
console.log(`[EXULU] Image saved to: ${imagePath}`);
|
|
13753
14637
|
let json = [{
|
|
13754
14638
|
page: 1,
|
|
@@ -13765,7 +14649,7 @@ async function processImage(buffer, paths, config, verbose = false) {
|
|
|
13765
14649
|
verbose,
|
|
13766
14650
|
config.vlm.concurrency
|
|
13767
14651
|
);
|
|
13768
|
-
await
|
|
14652
|
+
await fs4.promises.writeFile(
|
|
13769
14653
|
paths.json,
|
|
13770
14654
|
JSON.stringify(json, null, 2),
|
|
13771
14655
|
"utf-8"
|
|
@@ -13776,14 +14660,14 @@ async function processImage(buffer, paths, config, verbose = false) {
|
|
|
13776
14660
|
} else {
|
|
13777
14661
|
console.log("[EXULU] No VLM configured, image saved without content extraction");
|
|
13778
14662
|
console.log("[EXULU] Note: Enable VLM in config to extract text/content from images");
|
|
13779
|
-
await
|
|
14663
|
+
await fs4.promises.writeFile(
|
|
13780
14664
|
paths.json,
|
|
13781
14665
|
JSON.stringify(json, null, 2),
|
|
13782
14666
|
"utf-8"
|
|
13783
14667
|
);
|
|
13784
14668
|
}
|
|
13785
14669
|
const markdown = json.map((p) => p.vlm_corrected_text ?? p.content).join("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
|
|
13786
|
-
await
|
|
14670
|
+
await fs4.promises.writeFile(paths.markdown, markdown, "utf-8");
|
|
13787
14671
|
return {
|
|
13788
14672
|
markdown,
|
|
13789
14673
|
json
|
|
@@ -13836,7 +14720,7 @@ function reconstructHeadings(correctedText, headingsHierarchy) {
|
|
|
13836
14720
|
return result;
|
|
13837
14721
|
}
|
|
13838
14722
|
async function validatePageWithVLM(page, imagePath, model) {
|
|
13839
|
-
const imageBuffer = await
|
|
14723
|
+
const imageBuffer = await fs4.promises.readFile(imagePath);
|
|
13840
14724
|
const imageBase64 = imageBuffer.toString("base64");
|
|
13841
14725
|
const mimeType = "image/png";
|
|
13842
14726
|
const prompt = `You are a document validation assistant. Your task is to analyze a page image and correct the output of an OCR/parsing pipeline. The content may include tables, technical diagrams, schematics, and structured text.
|
|
@@ -13914,18 +14798,18 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
13914
14798
|
|
|
13915
14799
|
### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
|
|
13916
14800
|
`;
|
|
13917
|
-
const result = await
|
|
14801
|
+
const result = await generateText5({
|
|
13918
14802
|
model,
|
|
13919
14803
|
output: Output2.object({
|
|
13920
|
-
schema:
|
|
13921
|
-
needs_correction:
|
|
13922
|
-
corrected_text:
|
|
13923
|
-
current_page_table:
|
|
13924
|
-
headers:
|
|
13925
|
-
is_continuation:
|
|
14804
|
+
schema: z11.object({
|
|
14805
|
+
needs_correction: z11.boolean(),
|
|
14806
|
+
corrected_text: z11.string().nullable(),
|
|
14807
|
+
current_page_table: z11.object({
|
|
14808
|
+
headers: z11.array(z11.string()),
|
|
14809
|
+
is_continuation: z11.boolean()
|
|
13926
14810
|
}).nullable(),
|
|
13927
|
-
confidence:
|
|
13928
|
-
reasoning:
|
|
14811
|
+
confidence: z11.enum(["high", "medium", "low"]),
|
|
14812
|
+
reasoning: z11.string()
|
|
13929
14813
|
})
|
|
13930
14814
|
}),
|
|
13931
14815
|
messages: [
|
|
@@ -14005,7 +14889,7 @@ async function validateWithVLM(document, model, verbose = false, concurrency = 1
|
|
|
14005
14889
|
let correctedCount = 0;
|
|
14006
14890
|
const validationTasks = document.map(
|
|
14007
14891
|
(page) => limit(async () => {
|
|
14008
|
-
await new Promise((
|
|
14892
|
+
await new Promise((resolve4) => setImmediate(resolve4));
|
|
14009
14893
|
const imagePath = page.image;
|
|
14010
14894
|
if (!imagePath) {
|
|
14011
14895
|
console.warn(`[EXULU] Page ${page.page}: No image found, skipping validation`);
|
|
@@ -14179,7 +15063,7 @@ ${setupResult.output || ""}`);
|
|
|
14179
15063
|
if (!result.success) {
|
|
14180
15064
|
throw new Error(`Document processing failed: ${result.stderr}`);
|
|
14181
15065
|
}
|
|
14182
|
-
const jsonContent = await
|
|
15066
|
+
const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
|
|
14183
15067
|
json = JSON.parse(jsonContent);
|
|
14184
15068
|
} else if (config?.processor.name === "officeparser") {
|
|
14185
15069
|
const text = await parseOfficeAsync2(buffer, {
|
|
@@ -14196,7 +15080,7 @@ ${setupResult.output || ""}`);
|
|
|
14196
15080
|
if (!MISTRAL_API_KEY) {
|
|
14197
15081
|
throw new Error('[EXULU] MISTRAL_API_KEY is not set, please set it in the environment variable via process.env or via an Exulu variable named "MISTRAL_API_KEY".');
|
|
14198
15082
|
}
|
|
14199
|
-
await new Promise((
|
|
15083
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 4e3) + 1e3));
|
|
14200
15084
|
const base64Pdf = buffer.toString("base64");
|
|
14201
15085
|
const client2 = new Mistral({ apiKey: MISTRAL_API_KEY });
|
|
14202
15086
|
const ocrResponse = await withRetry(async () => {
|
|
@@ -14212,9 +15096,9 @@ ${setupResult.output || ""}`);
|
|
|
14212
15096
|
}, 10);
|
|
14213
15097
|
const parser = new LiteParse();
|
|
14214
15098
|
const screenshots = await parser.screenshot(paths.source, void 0);
|
|
14215
|
-
await
|
|
15099
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
14216
15100
|
for (const screenshot of screenshots) {
|
|
14217
|
-
await
|
|
15101
|
+
await fs4.promises.writeFile(
|
|
14218
15102
|
path.join(
|
|
14219
15103
|
paths.images,
|
|
14220
15104
|
`${screenshot.pageNum}.png`
|
|
@@ -14229,15 +15113,15 @@ ${setupResult.output || ""}`);
|
|
|
14229
15113
|
image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
|
|
14230
15114
|
headings: []
|
|
14231
15115
|
}));
|
|
14232
|
-
|
|
15116
|
+
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
14233
15117
|
} else if (config?.processor.name === "liteparse") {
|
|
14234
15118
|
const parser = new LiteParse();
|
|
14235
15119
|
const result = await parser.parse(paths.source);
|
|
14236
15120
|
const screenshots = await parser.screenshot(paths.source, void 0);
|
|
14237
15121
|
console.log(`[EXULU] Liteparse screenshots: ${JSON.stringify(screenshots)}`);
|
|
14238
|
-
await
|
|
15122
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
14239
15123
|
for (const screenshot of screenshots) {
|
|
14240
|
-
await
|
|
15124
|
+
await fs4.promises.writeFile(path.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
|
|
14241
15125
|
screenshot.imagePath = path.join(paths.images, `${screenshot.pageNum}.png`);
|
|
14242
15126
|
}
|
|
14243
15127
|
json = result.pages.map((page) => ({
|
|
@@ -14245,7 +15129,7 @@ ${setupResult.output || ""}`);
|
|
|
14245
15129
|
content: page.text,
|
|
14246
15130
|
image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
|
|
14247
15131
|
}));
|
|
14248
|
-
|
|
15132
|
+
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
14249
15133
|
}
|
|
14250
15134
|
console.log(`[EXULU]
|
|
14251
15135
|
\u2713 Document processing completed successfully`);
|
|
@@ -14276,13 +15160,13 @@ ${setupResult.output || ""}`);
|
|
|
14276
15160
|
console.log(`[EXULU] Corrected: ${page.vlm_corrected_text.substring(0, 150)}...`);
|
|
14277
15161
|
});
|
|
14278
15162
|
}
|
|
14279
|
-
await
|
|
15163
|
+
await fs4.promises.writeFile(
|
|
14280
15164
|
paths.json,
|
|
14281
15165
|
JSON.stringify(json, null, 2),
|
|
14282
15166
|
"utf-8"
|
|
14283
15167
|
);
|
|
14284
15168
|
}
|
|
14285
|
-
const markdownStream =
|
|
15169
|
+
const markdownStream = fs4.createWriteStream(paths.markdown, { encoding: "utf-8" });
|
|
14286
15170
|
for (let i = 0; i < json.length; i++) {
|
|
14287
15171
|
const p = json[i];
|
|
14288
15172
|
if (!p) continue;
|
|
@@ -14292,13 +15176,13 @@ ${setupResult.output || ""}`);
|
|
|
14292
15176
|
markdownStream.write("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
|
|
14293
15177
|
}
|
|
14294
15178
|
}
|
|
14295
|
-
await new Promise((
|
|
14296
|
-
markdownStream.end(() =>
|
|
15179
|
+
await new Promise((resolve4, reject) => {
|
|
15180
|
+
markdownStream.end(() => resolve4());
|
|
14297
15181
|
markdownStream.on("error", reject);
|
|
14298
15182
|
});
|
|
14299
15183
|
console.log(`[EXULU] Validated output saved to: ${paths.json}`);
|
|
14300
15184
|
console.log(`[EXULU] Validated markdown saved to: ${paths.markdown}`);
|
|
14301
|
-
const markdown = await
|
|
15185
|
+
const markdown = await fs4.promises.readFile(paths.markdown, "utf-8");
|
|
14302
15186
|
const processedJson = json.map((e) => {
|
|
14303
15187
|
const finalContent = e.vlm_corrected_text ?? e.content;
|
|
14304
15188
|
return {
|
|
@@ -14325,11 +15209,11 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
14325
15209
|
if (!fileType) {
|
|
14326
15210
|
throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
|
|
14327
15211
|
}
|
|
14328
|
-
const UUID =
|
|
15212
|
+
const UUID = randomUUID6();
|
|
14329
15213
|
let buffer;
|
|
14330
15214
|
if (Buffer.isBuffer(file)) {
|
|
14331
15215
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
14332
|
-
await
|
|
15216
|
+
await fs4.promises.writeFile(filePath, file);
|
|
14333
15217
|
buffer = file;
|
|
14334
15218
|
} else {
|
|
14335
15219
|
filePath = filePath.trim();
|
|
@@ -14337,11 +15221,11 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
14337
15221
|
const response = await fetch(filePath);
|
|
14338
15222
|
const array = await response.arrayBuffer();
|
|
14339
15223
|
const tempFilePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
14340
|
-
await
|
|
15224
|
+
await fs4.promises.writeFile(tempFilePath, Buffer.from(array));
|
|
14341
15225
|
buffer = Buffer.from(array);
|
|
14342
15226
|
filePath = tempFilePath;
|
|
14343
15227
|
} else {
|
|
14344
|
-
buffer = await
|
|
15228
|
+
buffer = await fs4.promises.readFile(file);
|
|
14345
15229
|
}
|
|
14346
15230
|
}
|
|
14347
15231
|
return { filePath, fileType, buffer };
|
|
@@ -14355,13 +15239,13 @@ async function documentProcessor({
|
|
|
14355
15239
|
if (!license["advanced-document-processing"]) {
|
|
14356
15240
|
throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
|
|
14357
15241
|
}
|
|
14358
|
-
const uuid =
|
|
15242
|
+
const uuid = randomUUID6();
|
|
14359
15243
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
14360
15244
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
14361
15245
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|
|
14362
|
-
await
|
|
15246
|
+
await fs4.promises.mkdir(tempDir, { recursive: true });
|
|
14363
15247
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
14364
|
-
await
|
|
15248
|
+
await fs4.promises.writeFile(path.join(tempDir, "created_at.txt"), timestamp);
|
|
14365
15249
|
try {
|
|
14366
15250
|
const {
|
|
14367
15251
|
filePath,
|
|
@@ -14402,7 +15286,7 @@ async function documentProcessor({
|
|
|
14402
15286
|
if (config?.debugging?.deleteTempFiles !== false) {
|
|
14403
15287
|
for (const file2 of localFilesAndFoldersToDelete) {
|
|
14404
15288
|
try {
|
|
14405
|
-
await
|
|
15289
|
+
await fs4.promises.rm(file2, { recursive: true });
|
|
14406
15290
|
console.log(`[EXULU] Deleted file or folder: ${file2}`);
|
|
14407
15291
|
} catch (error) {
|
|
14408
15292
|
console.error(`[EXULU] Error deleting file or folder: ${file2}`, error);
|
|
@@ -14463,8 +15347,8 @@ var ExuluVariables = {
|
|
|
14463
15347
|
throw new Error(`Variable ${name} not found.`);
|
|
14464
15348
|
}
|
|
14465
15349
|
if (variable.encrypted) {
|
|
14466
|
-
const bytes =
|
|
14467
|
-
variable.value = bytes.toString(
|
|
15350
|
+
const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
15351
|
+
variable.value = bytes.toString(CryptoJS7.enc.Utf8);
|
|
14468
15352
|
}
|
|
14469
15353
|
return variable.value;
|
|
14470
15354
|
}
|
|
@@ -14489,11 +15373,17 @@ var ExuluOtel = {
|
|
|
14489
15373
|
}
|
|
14490
15374
|
};
|
|
14491
15375
|
var ExuluDatabase = {
|
|
14492
|
-
init: async ({ contexts }) => {
|
|
15376
|
+
init: async ({ contexts, litellm }) => {
|
|
14493
15377
|
await execute({ contexts });
|
|
15378
|
+
if (litellm !== false) {
|
|
15379
|
+
await initLitellmDb();
|
|
15380
|
+
}
|
|
14494
15381
|
},
|
|
14495
|
-
update: async ({ contexts }) => {
|
|
15382
|
+
update: async ({ contexts, litellm }) => {
|
|
14496
15383
|
await execute({ contexts });
|
|
15384
|
+
if (litellm !== false) {
|
|
15385
|
+
await initLitellmDb();
|
|
15386
|
+
}
|
|
14497
15387
|
},
|
|
14498
15388
|
api: {
|
|
14499
15389
|
key: {
|