@exulu/backend 1.58.0 → 1.59.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-U36VJDZ7.js} +644 -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-ZEECMX43.js} +1 -1
- package/dist/index.cjs +2606 -1236
- package/dist/index.d.cts +13 -14
- package/dist/index.d.ts +13 -14
- package/dist/index.js +1812 -1134
- 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-U36VJDZ7.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,324 @@ 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/validators/postgres-name.ts
|
|
11008
|
+
var isValidPostgresName = (id) => {
|
|
11009
|
+
const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
11010
|
+
const isValid = regex.test(id);
|
|
11011
|
+
const length = id.length;
|
|
11012
|
+
return isValid && length <= 80 && length > 2;
|
|
11013
|
+
};
|
|
11014
|
+
|
|
11015
|
+
// src/utils/python-setup.ts
|
|
11016
|
+
import { exec as exec2 } from "child_process";
|
|
11017
|
+
import { promisify as promisify2 } from "util";
|
|
11018
|
+
import { resolve, join as join2, dirname } from "path";
|
|
11019
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
11020
|
+
import { fileURLToPath } from "url";
|
|
11021
|
+
var execAsync2 = promisify2(exec2);
|
|
11022
|
+
function getPackageRoot() {
|
|
11023
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
11024
|
+
let currentDir = dirname(currentFile);
|
|
11025
|
+
let attempts = 0;
|
|
11026
|
+
const maxAttempts = 10;
|
|
11027
|
+
while (attempts < maxAttempts) {
|
|
11028
|
+
const packageJsonPath = join2(currentDir, "package.json");
|
|
11029
|
+
if (existsSync2(packageJsonPath)) {
|
|
11030
|
+
try {
|
|
11031
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
11032
|
+
if (packageJson.name === "@exulu/backend") {
|
|
11033
|
+
return currentDir;
|
|
11034
|
+
}
|
|
11035
|
+
} catch {
|
|
11036
|
+
}
|
|
11037
|
+
}
|
|
11038
|
+
const parentDir = resolve(currentDir, "..");
|
|
11039
|
+
if (parentDir === currentDir) {
|
|
11040
|
+
break;
|
|
11041
|
+
}
|
|
11042
|
+
currentDir = parentDir;
|
|
11043
|
+
attempts++;
|
|
11044
|
+
}
|
|
11045
|
+
const fallback = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
11046
|
+
return fallback;
|
|
11047
|
+
}
|
|
11048
|
+
function getSetupScriptPath(packageRoot) {
|
|
11049
|
+
return resolve(packageRoot, "ee/python/setup.sh");
|
|
11050
|
+
}
|
|
11051
|
+
function getVenvPath(packageRoot) {
|
|
11052
|
+
return resolve(packageRoot, "ee/python/.venv");
|
|
11053
|
+
}
|
|
11054
|
+
function isPythonEnvironmentSetup(packageRoot) {
|
|
11055
|
+
const root = packageRoot ?? getPackageRoot();
|
|
11056
|
+
const venvPath = getVenvPath(root);
|
|
11057
|
+
const pythonPath = join2(venvPath, "bin", "python");
|
|
11058
|
+
return existsSync2(venvPath) && existsSync2(pythonPath);
|
|
11059
|
+
}
|
|
11060
|
+
async function setupPythonEnvironment(options = {}) {
|
|
11061
|
+
const {
|
|
11062
|
+
packageRoot = getPackageRoot(),
|
|
11063
|
+
force = false,
|
|
11064
|
+
verbose = false,
|
|
11065
|
+
timeout = 6e5
|
|
11066
|
+
// 10 minutes
|
|
11067
|
+
} = options;
|
|
11068
|
+
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
11069
|
+
if (verbose) {
|
|
11070
|
+
console.log("\u2713 Python environment already set up");
|
|
11071
|
+
}
|
|
11072
|
+
return {
|
|
11073
|
+
success: true,
|
|
11074
|
+
message: "Python environment already exists",
|
|
11075
|
+
alreadyExists: true
|
|
11076
|
+
};
|
|
11077
|
+
}
|
|
11078
|
+
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
11079
|
+
if (!existsSync2(setupScriptPath)) {
|
|
11080
|
+
return {
|
|
11081
|
+
success: false,
|
|
11082
|
+
message: `Setup script not found at: ${setupScriptPath}`,
|
|
11083
|
+
alreadyExists: false
|
|
11084
|
+
};
|
|
11085
|
+
}
|
|
11086
|
+
try {
|
|
11087
|
+
if (verbose) {
|
|
11088
|
+
console.log("Setting up Python environment...");
|
|
11089
|
+
}
|
|
11090
|
+
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
11091
|
+
cwd: packageRoot,
|
|
11092
|
+
timeout,
|
|
11093
|
+
env: {
|
|
11094
|
+
...process.env,
|
|
11095
|
+
// Ensure script can write to the directory
|
|
11096
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
11097
|
+
},
|
|
11098
|
+
maxBuffer: 10 * 1024 * 1024
|
|
11099
|
+
// 10MB buffer
|
|
11100
|
+
});
|
|
11101
|
+
const output = stdout + stderr;
|
|
11102
|
+
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
11103
|
+
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
11104
|
+
if (verbose) {
|
|
11105
|
+
console.log(output);
|
|
11106
|
+
}
|
|
11107
|
+
return {
|
|
11108
|
+
success: true,
|
|
11109
|
+
message: "Python environment set up successfully",
|
|
11110
|
+
alreadyExists: false,
|
|
11111
|
+
pythonVersion,
|
|
11112
|
+
output
|
|
11113
|
+
};
|
|
11114
|
+
} catch (error) {
|
|
11115
|
+
const errorOutput = error.stdout + error.stderr;
|
|
11116
|
+
return {
|
|
11117
|
+
success: false,
|
|
11118
|
+
message: `Setup failed: ${error.message}`,
|
|
11119
|
+
alreadyExists: false,
|
|
11120
|
+
output: errorOutput
|
|
11121
|
+
};
|
|
11122
|
+
}
|
|
11123
|
+
}
|
|
11124
|
+
function getPythonSetupInstructions() {
|
|
11125
|
+
return `
|
|
11126
|
+
Python environment not set up. Please run one of the following commands:
|
|
11127
|
+
|
|
11128
|
+
Option 1 (Automatic):
|
|
11129
|
+
import { setupPythonEnvironment } from '@exulu/backend';
|
|
11130
|
+
await setupPythonEnvironment();
|
|
11131
|
+
|
|
11132
|
+
Option 2 (Manual - for package consumers):
|
|
11133
|
+
npx @exulu/backend setup-python
|
|
11134
|
+
|
|
11135
|
+
Option 3 (Manual - for contributors):
|
|
11136
|
+
npm run python:setup
|
|
11137
|
+
|
|
11138
|
+
These commands will automatically create a Python virtual environment (.venv)
|
|
11139
|
+
in the @exulu/backend package and install all required dependencies.
|
|
11140
|
+
|
|
11141
|
+
Requirements:
|
|
11142
|
+
- Python 3.10 or higher must be installed
|
|
11143
|
+
- pip must be available
|
|
11144
|
+
- venv module must be available (for creating virtual environments)
|
|
11145
|
+
|
|
11146
|
+
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
11147
|
+
- macOS: brew install python@3.12
|
|
11148
|
+
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
11149
|
+
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
11150
|
+
- Windows: Download from https://www.python.org/downloads/
|
|
11151
|
+
|
|
11152
|
+
Note: In Docker containers, ensure you install all three components:
|
|
11153
|
+
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
11154
|
+
Alpine: apk add python3 py3-pip python3-dev
|
|
11155
|
+
`.trim();
|
|
11156
|
+
}
|
|
11157
|
+
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
11158
|
+
const root = packageRoot ?? getPackageRoot();
|
|
11159
|
+
const venvPath = getVenvPath(root);
|
|
11160
|
+
const pythonPath = join2(venvPath, "bin", "python");
|
|
11161
|
+
if (!existsSync2(venvPath)) {
|
|
11162
|
+
return {
|
|
11163
|
+
valid: false,
|
|
11164
|
+
message: getPythonSetupInstructions()
|
|
10576
11165
|
};
|
|
10577
11166
|
}
|
|
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
|
-
}
|
|
11167
|
+
if (!existsSync2(pythonPath)) {
|
|
11168
|
+
return {
|
|
11169
|
+
valid: false,
|
|
11170
|
+
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
10660
11171
|
};
|
|
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);
|
|
11172
|
+
}
|
|
11173
|
+
try {
|
|
11174
|
+
await execAsync2(`"${pythonPath}" --version`, { cwd: root });
|
|
11175
|
+
} catch {
|
|
10670
11176
|
return {
|
|
10671
|
-
|
|
11177
|
+
valid: false,
|
|
11178
|
+
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
10672
11179
|
};
|
|
10673
11180
|
}
|
|
10674
|
-
|
|
11181
|
+
if (checkPackages) {
|
|
11182
|
+
const criticalPackages = ["docling", "transformers"];
|
|
11183
|
+
const missingPackages = [];
|
|
11184
|
+
for (const pkg of criticalPackages) {
|
|
11185
|
+
try {
|
|
11186
|
+
await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
11187
|
+
cwd: root,
|
|
11188
|
+
timeout: 1e4
|
|
11189
|
+
// 10 second timeout per import check
|
|
11190
|
+
});
|
|
11191
|
+
} catch {
|
|
11192
|
+
missingPackages.push(pkg);
|
|
11193
|
+
}
|
|
11194
|
+
}
|
|
11195
|
+
if (missingPackages.length > 0) {
|
|
11196
|
+
return {
|
|
11197
|
+
valid: false,
|
|
11198
|
+
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
10675
11199
|
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
|
|
10681
|
-
|
|
10682
|
-
|
|
11200
|
+
This usually happens when:
|
|
11201
|
+
1. The .venv folder was copied but dependencies were not installed
|
|
11202
|
+
2. The package was installed via npm but setup script was not run
|
|
11203
|
+
|
|
11204
|
+
Please run:
|
|
11205
|
+
await setupPythonEnvironment({ force: true })
|
|
11206
|
+
|
|
11207
|
+
Or manually run the setup script:
|
|
11208
|
+
bash ` + getSetupScriptPath(root)
|
|
11209
|
+
};
|
|
11210
|
+
}
|
|
11211
|
+
}
|
|
11212
|
+
return {
|
|
11213
|
+
valid: true,
|
|
11214
|
+
message: "Python environment is valid"
|
|
11215
|
+
};
|
|
11216
|
+
}
|
|
10683
11217
|
|
|
10684
11218
|
// src/exulu/app/index.ts
|
|
10685
11219
|
var isDev = process.env.NODE_ENV !== "production";
|
|
@@ -10795,9 +11329,9 @@ var ExuluApp = class {
|
|
|
10795
11329
|
id: provider.id ?? "",
|
|
10796
11330
|
type: "agent"
|
|
10797
11331
|
})),
|
|
10798
|
-
...this._tools.map((
|
|
10799
|
-
name:
|
|
10800
|
-
id:
|
|
11332
|
+
...this._tools.map((tool2) => ({
|
|
11333
|
+
name: tool2.name ?? "",
|
|
11334
|
+
id: tool2.id ?? "",
|
|
10801
11335
|
type: "tool"
|
|
10802
11336
|
})),
|
|
10803
11337
|
...this._rerankers.map((reranker) => ({
|
|
@@ -10839,6 +11373,21 @@ var ExuluApp = class {
|
|
|
10839
11373
|
await reportSystemDependencies({
|
|
10840
11374
|
requireSystemDependencies: config.requireSystemDependencies !== false
|
|
10841
11375
|
});
|
|
11376
|
+
if (process.env.TRANSCRIPTION_MODEL && !isLiteLLMEnabled()) {
|
|
11377
|
+
console.warn(
|
|
11378
|
+
"[EXULU] TRANSCRIPTION_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /transcribe endpoint will return 503 until LiteLLM is enabled."
|
|
11379
|
+
);
|
|
11380
|
+
}
|
|
11381
|
+
if (process.env.TTS_MODEL && !isLiteLLMEnabled()) {
|
|
11382
|
+
console.warn(
|
|
11383
|
+
"[EXULU] TTS_MODEL is set but EXULU_USE_LITELLM is not 'true'. The /speech endpoint will return 503 until LiteLLM is enabled."
|
|
11384
|
+
);
|
|
11385
|
+
}
|
|
11386
|
+
if (process.env.TTS_MODEL && !process.env.TTS_VOICE) {
|
|
11387
|
+
console.warn(
|
|
11388
|
+
"[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)."
|
|
11389
|
+
);
|
|
11390
|
+
}
|
|
10842
11391
|
console.log("[EXULU] App initialized.");
|
|
10843
11392
|
exuluApp.set(this);
|
|
10844
11393
|
return this;
|
|
@@ -10850,6 +11399,18 @@ var ExuluApp = class {
|
|
|
10850
11399
|
await this.server.express.init();
|
|
10851
11400
|
console.log("[EXULU] Express app initialized.");
|
|
10852
11401
|
}
|
|
11402
|
+
if (isLiteLLMEnabled()) {
|
|
11403
|
+
const packageRoot = getPackageRoot();
|
|
11404
|
+
setLiteLLMPackageRoot(packageRoot);
|
|
11405
|
+
try {
|
|
11406
|
+
await startLiteLLMSupervisor();
|
|
11407
|
+
} catch (err) {
|
|
11408
|
+
console.error(
|
|
11409
|
+
"[EXULU] LiteLLM supervisor failed to start:",
|
|
11410
|
+
err.message
|
|
11411
|
+
);
|
|
11412
|
+
}
|
|
11413
|
+
}
|
|
10853
11414
|
return this._expressApp;
|
|
10854
11415
|
}
|
|
10855
11416
|
};
|
|
@@ -12062,7 +12623,7 @@ var RecursiveChunker = class _RecursiveChunker extends BaseChunker {
|
|
|
12062
12623
|
};
|
|
12063
12624
|
|
|
12064
12625
|
// src/exulu/embedder.ts
|
|
12065
|
-
import
|
|
12626
|
+
import CryptoJS6 from "crypto-js";
|
|
12066
12627
|
var ExuluEmbedder = class {
|
|
12067
12628
|
id;
|
|
12068
12629
|
name;
|
|
@@ -12133,8 +12694,8 @@ var ExuluEmbedder = class {
|
|
|
12133
12694
|
);
|
|
12134
12695
|
}
|
|
12135
12696
|
try {
|
|
12136
|
-
const bytes =
|
|
12137
|
-
const decrypted = bytes.toString(
|
|
12697
|
+
const bytes = CryptoJS6.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
12698
|
+
const decrypted = bytes.toString(CryptoJS6.enc.Utf8);
|
|
12138
12699
|
if (!decrypted) {
|
|
12139
12700
|
throw new Error("Decryption returned empty string - invalid key or corrupted data");
|
|
12140
12701
|
}
|
|
@@ -12610,7 +13171,7 @@ var SentenceChunker = class _SentenceChunker extends BaseChunker {
|
|
|
12610
13171
|
}
|
|
12611
13172
|
};
|
|
12612
13173
|
|
|
12613
|
-
// src/postgres/init-db.ts
|
|
13174
|
+
// src/postgres/init-exulu-db.ts
|
|
12614
13175
|
var {
|
|
12615
13176
|
agentsSchema: agentsSchema2,
|
|
12616
13177
|
feedbackSchema: feedbackSchema2,
|
|
@@ -12620,6 +13181,7 @@ var {
|
|
|
12620
13181
|
agentSessionsSchema: agentSessionsSchema2,
|
|
12621
13182
|
platformConfigurationsSchema: platformConfigurationsSchema2,
|
|
12622
13183
|
agentMessagesSchema: agentMessagesSchema2,
|
|
13184
|
+
modelsSchema: modelsSchema2,
|
|
12623
13185
|
rolesSchema: rolesSchema2,
|
|
12624
13186
|
usersSchema: usersSchema2,
|
|
12625
13187
|
skillsSchema: skillsSchema2,
|
|
@@ -12659,6 +13221,7 @@ var up = async function(knex) {
|
|
|
12659
13221
|
const schemas = [
|
|
12660
13222
|
agentSessionsSchema2(),
|
|
12661
13223
|
agentMessagesSchema2(),
|
|
13224
|
+
modelsSchema2(),
|
|
12662
13225
|
rolesSchema2(),
|
|
12663
13226
|
testCasesSchema2(),
|
|
12664
13227
|
evalSetsSchema2(),
|
|
@@ -12702,6 +13265,47 @@ var up = async function(knex) {
|
|
|
12702
13265
|
console.log(`[EXULU] Creating ${schema.name.plural} table.`, schema.fields);
|
|
12703
13266
|
await createTable(schema);
|
|
12704
13267
|
}
|
|
13268
|
+
const hasOldProviderCol = await knex.schema.hasColumn("agents", "provider");
|
|
13269
|
+
const hasOldKeyCol = await knex.schema.hasColumn("agents", "providerapikey");
|
|
13270
|
+
if (hasOldProviderCol || hasOldKeyCol) {
|
|
13271
|
+
console.log("[EXULU] Migrating agents.provider/providerapikey -> models table.");
|
|
13272
|
+
await knex.transaction(async (trx) => {
|
|
13273
|
+
const pairs = await trx("agents").distinct("provider", "providerapikey").whereNotNull("provider");
|
|
13274
|
+
const pairToModelId = /* @__PURE__ */ new Map();
|
|
13275
|
+
for (const { provider, providerapikey } of pairs) {
|
|
13276
|
+
const inserted = await trx("models").insert({
|
|
13277
|
+
name: `${provider}${providerapikey ? ` (${providerapikey})` : ""}`,
|
|
13278
|
+
provider,
|
|
13279
|
+
authvariable: providerapikey,
|
|
13280
|
+
active: true,
|
|
13281
|
+
rights_mode: "public",
|
|
13282
|
+
created_by: 1
|
|
13283
|
+
}).returning("id");
|
|
13284
|
+
const id = inserted[0]?.id;
|
|
13285
|
+
if (!id) {
|
|
13286
|
+
throw new Error("[EXULU] Migration: failed to insert models row");
|
|
13287
|
+
}
|
|
13288
|
+
pairToModelId.set(`${provider}::${providerapikey ?? ""}`, id);
|
|
13289
|
+
}
|
|
13290
|
+
for (const [key2, modelId] of pairToModelId) {
|
|
13291
|
+
const [provider, providerapikey] = key2.split("::");
|
|
13292
|
+
const where = {
|
|
13293
|
+
provider,
|
|
13294
|
+
providerapikey: providerapikey ? providerapikey : null
|
|
13295
|
+
};
|
|
13296
|
+
await trx("agents").where(where).update({ model: modelId });
|
|
13297
|
+
}
|
|
13298
|
+
if (hasOldProviderCol) {
|
|
13299
|
+
await trx.schema.alterTable("agents", (t) => t.dropColumn("provider"));
|
|
13300
|
+
}
|
|
13301
|
+
if (hasOldKeyCol) {
|
|
13302
|
+
await trx.schema.alterTable("agents", (t) => t.dropColumn("providerapikey"));
|
|
13303
|
+
}
|
|
13304
|
+
console.log(
|
|
13305
|
+
`[EXULU] Migrated ${pairToModelId.size} unique provider+key pairs into models.`
|
|
13306
|
+
);
|
|
13307
|
+
});
|
|
13308
|
+
}
|
|
12705
13309
|
if (!await knex.schema.hasTable("verification_token")) {
|
|
12706
13310
|
console.log("[EXULU] Creating verification_token table.");
|
|
12707
13311
|
await knex.schema.createTable("verification_token", (table) => {
|
|
@@ -12799,39 +13403,310 @@ var execute = async ({ contexts }) => {
|
|
|
12799
13403
|
} else {
|
|
12800
13404
|
adminRoleId = existingAdminRole.id;
|
|
12801
13405
|
}
|
|
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");
|
|
13406
|
+
if (!existingDefaultRole) {
|
|
13407
|
+
console.log("[EXULU] Creating default role.");
|
|
13408
|
+
await db.from("roles").insert({
|
|
13409
|
+
name: "default",
|
|
13410
|
+
agents: "write",
|
|
13411
|
+
api: "read",
|
|
13412
|
+
workflows: "read",
|
|
13413
|
+
variables: "read",
|
|
13414
|
+
users: "read",
|
|
13415
|
+
evals: "read"
|
|
13416
|
+
}).returning("id");
|
|
13417
|
+
}
|
|
13418
|
+
const existingUser = await db.from("users").where({ email: "admin@exulu.com" }).first();
|
|
13419
|
+
if (!existingUser) {
|
|
13420
|
+
const password = await encryptString("admin");
|
|
13421
|
+
console.log("[EXULU] Creating default admin user.");
|
|
13422
|
+
await db.from("users").insert({
|
|
13423
|
+
name: "exulu",
|
|
13424
|
+
email: "admin@exulu.com",
|
|
13425
|
+
super_admin: true,
|
|
13426
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
13427
|
+
emailVerified: /* @__PURE__ */ new Date(),
|
|
13428
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
13429
|
+
password,
|
|
13430
|
+
type: "user",
|
|
13431
|
+
role: adminRoleId
|
|
13432
|
+
});
|
|
13433
|
+
}
|
|
13434
|
+
const { key: key2 } = await generateApiKey("exulu", "api@exulu.com");
|
|
13435
|
+
console.log("[EXULU] Database initialized.");
|
|
13436
|
+
console.log("[EXULU] Default api key: ", `${key2}`);
|
|
13437
|
+
console.log("[EXULU] Default password if using password auth: ", `admin`);
|
|
13438
|
+
console.log("[EXULU] Default email if using password auth: ", `admin@exulu.com`);
|
|
13439
|
+
return;
|
|
13440
|
+
};
|
|
13441
|
+
|
|
13442
|
+
// src/exulu/litellm/db-init.ts
|
|
13443
|
+
import { existsSync as existsSync4 } from "fs";
|
|
13444
|
+
import { resolve as resolve2 } from "path";
|
|
13445
|
+
import { spawnSync } from "child_process";
|
|
13446
|
+
import { Client } from "pg";
|
|
13447
|
+
|
|
13448
|
+
// src/exulu/litellm/db-setup-check.ts
|
|
13449
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
13450
|
+
var readLiteLLMDatabaseUrl = (configPath) => {
|
|
13451
|
+
if (!existsSync3(configPath)) return void 0;
|
|
13452
|
+
const text = readFileSync2(configPath, "utf8");
|
|
13453
|
+
const match = text.match(
|
|
13454
|
+
/^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
|
|
13455
|
+
);
|
|
13456
|
+
if (!match) return void 0;
|
|
13457
|
+
const value = match[1]?.trim();
|
|
13458
|
+
if (!value) return void 0;
|
|
13459
|
+
if (value.startsWith("os.environ/")) {
|
|
13460
|
+
const envName = value.slice("os.environ/".length).trim();
|
|
13461
|
+
return process.env[envName];
|
|
13462
|
+
}
|
|
13463
|
+
return value;
|
|
13464
|
+
};
|
|
13465
|
+
var parsePostgresUrl = (url) => {
|
|
13466
|
+
try {
|
|
13467
|
+
const u = new URL(url);
|
|
13468
|
+
if (u.protocol !== "postgres:" && u.protocol !== "postgresql:") return void 0;
|
|
13469
|
+
return {
|
|
13470
|
+
host: u.hostname,
|
|
13471
|
+
port: u.port ? parseInt(u.port, 10) : 5432,
|
|
13472
|
+
database: u.pathname.replace(/^\//, "")
|
|
13473
|
+
};
|
|
13474
|
+
} catch {
|
|
13475
|
+
return void 0;
|
|
13476
|
+
}
|
|
13477
|
+
};
|
|
13478
|
+
var getExuluPostgresTarget = () => {
|
|
13479
|
+
const host = process.env.POSTGRES_DB_HOST;
|
|
13480
|
+
const database = process.env.POSTGRES_DB_NAME ?? "exulu";
|
|
13481
|
+
if (!host) return void 0;
|
|
13482
|
+
return {
|
|
13483
|
+
host,
|
|
13484
|
+
port: parseInt(process.env.POSTGRES_DB_PORT ?? "5432", 10),
|
|
13485
|
+
database
|
|
13486
|
+
};
|
|
13487
|
+
};
|
|
13488
|
+
var isSameDatabase = (a, b) => a.host === b.host && a.port === b.port && a.database === b.database;
|
|
13489
|
+
var checkLiteLLMDatabaseSafety = (configPath) => {
|
|
13490
|
+
const litellmUrl = readLiteLLMDatabaseUrl(configPath);
|
|
13491
|
+
if (!litellmUrl) return { ok: true, reason: "no-litellm-db-mode" };
|
|
13492
|
+
const litellmTarget = parsePostgresUrl(litellmUrl);
|
|
13493
|
+
if (!litellmTarget) return { ok: false, reason: "unparseable-url", rawUrl: litellmUrl };
|
|
13494
|
+
const exuluTarget = getExuluPostgresTarget();
|
|
13495
|
+
if (exuluTarget && isSameDatabase(litellmTarget, exuluTarget)) {
|
|
13496
|
+
return { ok: false, reason: "shared-with-exulu", litellmTarget, exuluTarget };
|
|
13497
|
+
}
|
|
13498
|
+
return { ok: true, reason: "isolated", litellmTarget };
|
|
13499
|
+
};
|
|
13500
|
+
|
|
13501
|
+
// src/exulu/litellm/db-init.ts
|
|
13502
|
+
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";
|
|
13503
|
+
var warn = (lines) => {
|
|
13504
|
+
console.warn(`
|
|
13505
|
+
${WARNING_BANNER}`);
|
|
13506
|
+
console.warn("\u26A0 [EXULU-LITELLM] CONFIGURATION WARNING");
|
|
13507
|
+
for (const line of lines) console.warn(` ${line}`);
|
|
13508
|
+
console.warn(`${WARNING_BANNER}
|
|
13509
|
+
`);
|
|
13510
|
+
};
|
|
13511
|
+
var log = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
13512
|
+
var initLiteLLMDatabase = async (packageRoot) => {
|
|
13513
|
+
const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(packageRoot, "./config.litellm.yaml");
|
|
13514
|
+
const safety = checkLiteLLMDatabaseSafety(configPath);
|
|
13515
|
+
if (safety.ok && safety.reason === "no-litellm-db-mode") return;
|
|
13516
|
+
if (!safety.ok && safety.reason === "unparseable-url") {
|
|
13517
|
+
warn([
|
|
13518
|
+
`LiteLLM's database_url is not a valid postgres URL:`,
|
|
13519
|
+
` ${safety.rawUrl}`,
|
|
13520
|
+
`Expected postgres://user:pass@host:port/database. Skipping setup.`
|
|
13521
|
+
]);
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
13524
|
+
if (!safety.ok && safety.reason === "shared-with-exulu") {
|
|
13525
|
+
const { exuluTarget } = safety;
|
|
13526
|
+
warn([
|
|
13527
|
+
`LiteLLM's database_url points to the SAME database Exulu uses:`,
|
|
13528
|
+
` ${exuluTarget.host}:${exuluTarget.port}/${exuluTarget.database}`,
|
|
13529
|
+
``,
|
|
13530
|
+
`If LiteLLM's schema sync were to run against this database, it`,
|
|
13531
|
+
`would DROP every table that is not in LiteLLM's Prisma schema,`,
|
|
13532
|
+
`destroying all of Exulu's data. Setup has been SKIPPED.`,
|
|
13533
|
+
``,
|
|
13534
|
+
`Fix: create a dedicated Postgres database for LiteLLM (e.g.`,
|
|
13535
|
+
`\`createdb litellm\`) and change database_url in ${configPath}`,
|
|
13536
|
+
`to point at it. Then restart Exulu.`
|
|
13537
|
+
]);
|
|
13538
|
+
return;
|
|
13539
|
+
}
|
|
13540
|
+
const litellmUrl = readLiteLLMDatabaseUrl(configPath);
|
|
13541
|
+
if (!litellmUrl) {
|
|
13542
|
+
return;
|
|
13543
|
+
}
|
|
13544
|
+
const target = "litellmTarget" in safety ? safety.litellmTarget : void 0;
|
|
13545
|
+
log(
|
|
13546
|
+
`LiteLLM database mode detected (${target?.host}:${target?.port}/${target?.database}).`
|
|
13547
|
+
);
|
|
13548
|
+
const ensureDatabaseExists = async () => {
|
|
13549
|
+
const probe = new Client({ connectionString: litellmUrl });
|
|
13550
|
+
try {
|
|
13551
|
+
await probe.connect();
|
|
13552
|
+
await probe.end();
|
|
13553
|
+
return true;
|
|
13554
|
+
} catch (err) {
|
|
13555
|
+
const code = err?.code;
|
|
13556
|
+
if (code !== "3D000") {
|
|
13557
|
+
warn([
|
|
13558
|
+
`Could not connect to LiteLLM's target database:`,
|
|
13559
|
+
` ${err instanceof Error ? err.message : String(err)}`,
|
|
13560
|
+
``,
|
|
13561
|
+
`Skipping LiteLLM database setup. LiteLLM features that depend on`,
|
|
13562
|
+
`the database will fail at runtime until this is resolved.`
|
|
13563
|
+
]);
|
|
13564
|
+
return false;
|
|
13565
|
+
}
|
|
13566
|
+
const url = new URL(litellmUrl);
|
|
13567
|
+
const targetDbName = url.pathname.replace(/^\//, "");
|
|
13568
|
+
if (!targetDbName) {
|
|
13569
|
+
warn([`LiteLLM database_url has no database name; cannot auto-create.`]);
|
|
13570
|
+
return false;
|
|
13571
|
+
}
|
|
13572
|
+
url.pathname = "/postgres";
|
|
13573
|
+
log(`Target database "${targetDbName}" does not exist; creating it\u2026`);
|
|
13574
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(targetDbName)) {
|
|
13575
|
+
warn([
|
|
13576
|
+
`Refusing to auto-create database "${targetDbName}" \u2014 name`,
|
|
13577
|
+
`contains characters that would require quoting. Create it`,
|
|
13578
|
+
`manually: createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`
|
|
13579
|
+
]);
|
|
13580
|
+
return false;
|
|
13581
|
+
}
|
|
13582
|
+
const admin = new Client({ connectionString: url.toString() });
|
|
13583
|
+
try {
|
|
13584
|
+
await admin.connect();
|
|
13585
|
+
await admin.query(`CREATE DATABASE "${targetDbName}"`);
|
|
13586
|
+
log(`\u2713 Created database "${targetDbName}".`);
|
|
13587
|
+
return true;
|
|
13588
|
+
} catch (createErr) {
|
|
13589
|
+
warn([
|
|
13590
|
+
`Failed to auto-create database "${targetDbName}":`,
|
|
13591
|
+
` ${createErr instanceof Error ? createErr.message : String(createErr)}`,
|
|
13592
|
+
``,
|
|
13593
|
+
`The connecting user likely lacks CREATEDB privilege. Create the`,
|
|
13594
|
+
`database manually:`,
|
|
13595
|
+
` createdb -h ${url.hostname} -U ${url.username} ${targetDbName}`,
|
|
13596
|
+
`Then restart Exulu.`
|
|
13597
|
+
]);
|
|
13598
|
+
return false;
|
|
13599
|
+
} finally {
|
|
13600
|
+
try {
|
|
13601
|
+
await admin.end();
|
|
13602
|
+
} catch {
|
|
13603
|
+
}
|
|
13604
|
+
}
|
|
13605
|
+
}
|
|
13606
|
+
};
|
|
13607
|
+
if (!await ensureDatabaseExists()) return;
|
|
13608
|
+
log("Checking that the target database is safe to push into\u2026");
|
|
13609
|
+
const client2 = new Client({ connectionString: litellmUrl });
|
|
13610
|
+
let foreignTables = [];
|
|
13611
|
+
try {
|
|
13612
|
+
await client2.connect();
|
|
13613
|
+
const res = await client2.query(
|
|
13614
|
+
`SELECT table_name
|
|
13615
|
+
FROM information_schema.tables
|
|
13616
|
+
WHERE table_schema = 'public'
|
|
13617
|
+
AND table_type = 'BASE TABLE'
|
|
13618
|
+
AND table_name NOT LIKE 'LiteLLM%'
|
|
13619
|
+
AND table_name <> '_prisma_migrations'
|
|
13620
|
+
ORDER BY table_name;`
|
|
13621
|
+
);
|
|
13622
|
+
foreignTables = res.rows.map((r) => r.table_name);
|
|
13623
|
+
} catch (err) {
|
|
13624
|
+
warn([
|
|
13625
|
+
`Could not query LiteLLM's target database to verify it is safe`,
|
|
13626
|
+
`to push into:`,
|
|
13627
|
+
` ${err instanceof Error ? err.message : String(err)}`,
|
|
13628
|
+
``,
|
|
13629
|
+
`Skipping LiteLLM database setup. LiteLLM features that depend on the`,
|
|
13630
|
+
`database will fail at runtime until this is resolved.`
|
|
13631
|
+
]);
|
|
13632
|
+
return;
|
|
13633
|
+
} finally {
|
|
13634
|
+
try {
|
|
13635
|
+
await client2.end();
|
|
13636
|
+
} catch {
|
|
13637
|
+
}
|
|
13638
|
+
}
|
|
13639
|
+
if (foreignTables.length > 0) {
|
|
13640
|
+
warn([
|
|
13641
|
+
`LiteLLM's target database contains ${foreignTables.length} table(s) that are NOT`,
|
|
13642
|
+
`part of LiteLLM's schema:`,
|
|
13643
|
+
...foreignTables.slice(0, 10).map((t) => ` - ${t}`),
|
|
13644
|
+
...foreignTables.length > 10 ? [` \u2026 and ${foreignTables.length - 10} more`] : [],
|
|
13645
|
+
``,
|
|
13646
|
+
`Refusing to run prisma db push to avoid data loss. Move LiteLLM to a`,
|
|
13647
|
+
`dedicated database, or remove these tables first if they are obsolete.`
|
|
13648
|
+
]);
|
|
13649
|
+
return;
|
|
13650
|
+
}
|
|
13651
|
+
const venvBin = resolve2(packageRoot, "ee/python/.venv/bin");
|
|
13652
|
+
const prismaCli = resolve2(venvBin, "prisma");
|
|
13653
|
+
const litellmProxyDir = resolve2(
|
|
13654
|
+
packageRoot,
|
|
13655
|
+
"ee/python/.venv/lib/python3.12/site-packages/litellm/proxy"
|
|
13656
|
+
);
|
|
13657
|
+
const schemaPath = resolve2(litellmProxyDir, "schema.prisma");
|
|
13658
|
+
if (!existsSync4(prismaCli)) {
|
|
13659
|
+
warn([
|
|
13660
|
+
`Prisma CLI not found at ${prismaCli}.`,
|
|
13661
|
+
`Run \`npm run python:setup\` to create the venv and install prisma.`,
|
|
13662
|
+
`Skipping LiteLLM database setup.`
|
|
13663
|
+
]);
|
|
13664
|
+
return;
|
|
12813
13665
|
}
|
|
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
|
-
});
|
|
13666
|
+
if (!existsSync4(schemaPath)) {
|
|
13667
|
+
warn([
|
|
13668
|
+
`LiteLLM Prisma schema not found at ${schemaPath}.`,
|
|
13669
|
+
`Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
|
|
13670
|
+
]);
|
|
13671
|
+
return;
|
|
12829
13672
|
}
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
13673
|
+
log("Running `prisma db push` against LiteLLM's schema\u2026");
|
|
13674
|
+
const result = spawnSync(prismaCli, ["db", "push", "--skip-generate"], {
|
|
13675
|
+
cwd: litellmProxyDir,
|
|
13676
|
+
env: {
|
|
13677
|
+
...process.env,
|
|
13678
|
+
DATABASE_URL: litellmUrl,
|
|
13679
|
+
PATH: `${venvBin}:${process.env.PATH ?? ""}`
|
|
13680
|
+
},
|
|
13681
|
+
// Capture rather than inherit so the prisma noise doesn't bury other
|
|
13682
|
+
// Exulu boot logs; we'll print stdout/stderr only on failure.
|
|
13683
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
13684
|
+
encoding: "utf8"
|
|
13685
|
+
});
|
|
13686
|
+
if (result.error) {
|
|
13687
|
+
warn([
|
|
13688
|
+
`Failed to launch prisma: ${result.error.message}`,
|
|
13689
|
+
`Skipping LiteLLM database setup.`
|
|
13690
|
+
]);
|
|
13691
|
+
return;
|
|
13692
|
+
}
|
|
13693
|
+
if (result.status !== 0) {
|
|
13694
|
+
warn([
|
|
13695
|
+
`prisma db push exited with status ${result.status}.`,
|
|
13696
|
+
`stdout:`,
|
|
13697
|
+
...(result.stdout || "(empty)").split("\n").map((l) => ` ${l}`),
|
|
13698
|
+
`stderr:`,
|
|
13699
|
+
...(result.stderr || "(empty)").split("\n").map((l) => ` ${l}`)
|
|
13700
|
+
]);
|
|
13701
|
+
return;
|
|
13702
|
+
}
|
|
13703
|
+
log("\u2713 LiteLLM database ready.");
|
|
13704
|
+
};
|
|
13705
|
+
|
|
13706
|
+
// src/postgres/init-litellm-db.ts
|
|
13707
|
+
var initLitellmDb = async () => {
|
|
13708
|
+
await initLiteLLMDatabase(getPackageRoot());
|
|
13709
|
+
console.log("[EXULU] LiteLLM database initialized.");
|
|
12835
13710
|
return;
|
|
12836
13711
|
};
|
|
12837
13712
|
|
|
@@ -12878,7 +13753,7 @@ var create = ({
|
|
|
12878
13753
|
};
|
|
12879
13754
|
|
|
12880
13755
|
// src/index.ts
|
|
12881
|
-
import
|
|
13756
|
+
import CryptoJS7 from "crypto-js";
|
|
12882
13757
|
|
|
12883
13758
|
// ee/chunking/markdown.ts
|
|
12884
13759
|
var extractPageTag = (text) => {
|
|
@@ -13358,214 +14233,11 @@ var MarkdownChunker = class {
|
|
|
13358
14233
|
}
|
|
13359
14234
|
};
|
|
13360
14235
|
|
|
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
14236
|
// ee/python/documents/processing/doc_processor.ts
|
|
13565
|
-
import * as
|
|
14237
|
+
import * as fs4 from "fs";
|
|
13566
14238
|
import * as path from "path";
|
|
13567
|
-
import { generateText as
|
|
13568
|
-
import { z as
|
|
14239
|
+
import { generateText as generateText5, Output as Output2 } from "ai";
|
|
14240
|
+
import { z as z10 } from "zod";
|
|
13569
14241
|
import pLimit from "p-limit";
|
|
13570
14242
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
13571
14243
|
import * as mammoth from "mammoth";
|
|
@@ -13576,8 +14248,8 @@ import { parseOfficeAsync as parseOfficeAsync2 } from "officeparser";
|
|
|
13576
14248
|
// src/utils/python-executor.ts
|
|
13577
14249
|
import { exec as exec3 } from "child_process";
|
|
13578
14250
|
import { promisify as promisify3 } from "util";
|
|
13579
|
-
import { resolve as
|
|
13580
|
-
import { existsSync as
|
|
14251
|
+
import { resolve as resolve3, join as join3, dirname as dirname2 } from "path";
|
|
14252
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
13581
14253
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13582
14254
|
var execAsync3 = promisify3(exec3);
|
|
13583
14255
|
function getPackageRoot2() {
|
|
@@ -13587,23 +14259,23 @@ function getPackageRoot2() {
|
|
|
13587
14259
|
const maxAttempts = 10;
|
|
13588
14260
|
while (attempts < maxAttempts) {
|
|
13589
14261
|
const packageJsonPath = join3(currentDir, "package.json");
|
|
13590
|
-
if (
|
|
14262
|
+
if (existsSync5(packageJsonPath)) {
|
|
13591
14263
|
try {
|
|
13592
|
-
const packageJson = JSON.parse(
|
|
14264
|
+
const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
13593
14265
|
if (packageJson.name === "@exulu/backend") {
|
|
13594
14266
|
return currentDir;
|
|
13595
14267
|
}
|
|
13596
14268
|
} catch {
|
|
13597
14269
|
}
|
|
13598
14270
|
}
|
|
13599
|
-
const parentDir =
|
|
14271
|
+
const parentDir = resolve3(currentDir, "..");
|
|
13600
14272
|
if (parentDir === currentDir) {
|
|
13601
14273
|
break;
|
|
13602
14274
|
}
|
|
13603
14275
|
currentDir = parentDir;
|
|
13604
14276
|
attempts++;
|
|
13605
14277
|
}
|
|
13606
|
-
return
|
|
14278
|
+
return resolve3(dirname2(fileURLToPath2(import.meta.url)), "../..");
|
|
13607
14279
|
}
|
|
13608
14280
|
var PythonEnvironmentError = class extends Error {
|
|
13609
14281
|
constructor(message) {
|
|
@@ -13624,7 +14296,7 @@ var PythonExecutionError = class extends Error {
|
|
|
13624
14296
|
}
|
|
13625
14297
|
};
|
|
13626
14298
|
function getVenvPath2(packageRoot) {
|
|
13627
|
-
return
|
|
14299
|
+
return resolve3(packageRoot, "ee/python/.venv");
|
|
13628
14300
|
}
|
|
13629
14301
|
function getPythonExecutable(packageRoot) {
|
|
13630
14302
|
const venvPath = getVenvPath2(packageRoot);
|
|
@@ -13650,8 +14322,8 @@ async function executePythonScript(config) {
|
|
|
13650
14322
|
if (validateEnvironment) {
|
|
13651
14323
|
await validatePythonEnvironmentForExecution(packageRoot);
|
|
13652
14324
|
}
|
|
13653
|
-
const resolvedScriptPath =
|
|
13654
|
-
if (!
|
|
14325
|
+
const resolvedScriptPath = resolve3(packageRoot, scriptPath);
|
|
14326
|
+
if (!existsSync5(resolvedScriptPath)) {
|
|
13655
14327
|
throw new PythonExecutionError(
|
|
13656
14328
|
`Python script not found: ${resolvedScriptPath}`,
|
|
13657
14329
|
"",
|
|
@@ -13746,9 +14418,9 @@ async function processWord(file) {
|
|
|
13746
14418
|
}
|
|
13747
14419
|
async function processImage(buffer, paths, config, verbose = false) {
|
|
13748
14420
|
try {
|
|
13749
|
-
await
|
|
14421
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
13750
14422
|
const imagePath = path.join(paths.images, "1.png");
|
|
13751
|
-
await
|
|
14423
|
+
await fs4.promises.writeFile(imagePath, buffer);
|
|
13752
14424
|
console.log(`[EXULU] Image saved to: ${imagePath}`);
|
|
13753
14425
|
let json = [{
|
|
13754
14426
|
page: 1,
|
|
@@ -13765,7 +14437,7 @@ async function processImage(buffer, paths, config, verbose = false) {
|
|
|
13765
14437
|
verbose,
|
|
13766
14438
|
config.vlm.concurrency
|
|
13767
14439
|
);
|
|
13768
|
-
await
|
|
14440
|
+
await fs4.promises.writeFile(
|
|
13769
14441
|
paths.json,
|
|
13770
14442
|
JSON.stringify(json, null, 2),
|
|
13771
14443
|
"utf-8"
|
|
@@ -13776,14 +14448,14 @@ async function processImage(buffer, paths, config, verbose = false) {
|
|
|
13776
14448
|
} else {
|
|
13777
14449
|
console.log("[EXULU] No VLM configured, image saved without content extraction");
|
|
13778
14450
|
console.log("[EXULU] Note: Enable VLM in config to extract text/content from images");
|
|
13779
|
-
await
|
|
14451
|
+
await fs4.promises.writeFile(
|
|
13780
14452
|
paths.json,
|
|
13781
14453
|
JSON.stringify(json, null, 2),
|
|
13782
14454
|
"utf-8"
|
|
13783
14455
|
);
|
|
13784
14456
|
}
|
|
13785
14457
|
const markdown = json.map((p) => p.vlm_corrected_text ?? p.content).join("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
|
|
13786
|
-
await
|
|
14458
|
+
await fs4.promises.writeFile(paths.markdown, markdown, "utf-8");
|
|
13787
14459
|
return {
|
|
13788
14460
|
markdown,
|
|
13789
14461
|
json
|
|
@@ -13836,7 +14508,7 @@ function reconstructHeadings(correctedText, headingsHierarchy) {
|
|
|
13836
14508
|
return result;
|
|
13837
14509
|
}
|
|
13838
14510
|
async function validatePageWithVLM(page, imagePath, model) {
|
|
13839
|
-
const imageBuffer = await
|
|
14511
|
+
const imageBuffer = await fs4.promises.readFile(imagePath);
|
|
13840
14512
|
const imageBase64 = imageBuffer.toString("base64");
|
|
13841
14513
|
const mimeType = "image/png";
|
|
13842
14514
|
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 +14586,18 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
13914
14586
|
|
|
13915
14587
|
### 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
14588
|
`;
|
|
13917
|
-
const result = await
|
|
14589
|
+
const result = await generateText5({
|
|
13918
14590
|
model,
|
|
13919
14591
|
output: Output2.object({
|
|
13920
|
-
schema:
|
|
13921
|
-
needs_correction:
|
|
13922
|
-
corrected_text:
|
|
13923
|
-
current_page_table:
|
|
13924
|
-
headers:
|
|
13925
|
-
is_continuation:
|
|
14592
|
+
schema: z10.object({
|
|
14593
|
+
needs_correction: z10.boolean(),
|
|
14594
|
+
corrected_text: z10.string().nullable(),
|
|
14595
|
+
current_page_table: z10.object({
|
|
14596
|
+
headers: z10.array(z10.string()),
|
|
14597
|
+
is_continuation: z10.boolean()
|
|
13926
14598
|
}).nullable(),
|
|
13927
|
-
confidence:
|
|
13928
|
-
reasoning:
|
|
14599
|
+
confidence: z10.enum(["high", "medium", "low"]),
|
|
14600
|
+
reasoning: z10.string()
|
|
13929
14601
|
})
|
|
13930
14602
|
}),
|
|
13931
14603
|
messages: [
|
|
@@ -14005,7 +14677,7 @@ async function validateWithVLM(document, model, verbose = false, concurrency = 1
|
|
|
14005
14677
|
let correctedCount = 0;
|
|
14006
14678
|
const validationTasks = document.map(
|
|
14007
14679
|
(page) => limit(async () => {
|
|
14008
|
-
await new Promise((
|
|
14680
|
+
await new Promise((resolve4) => setImmediate(resolve4));
|
|
14009
14681
|
const imagePath = page.image;
|
|
14010
14682
|
if (!imagePath) {
|
|
14011
14683
|
console.warn(`[EXULU] Page ${page.page}: No image found, skipping validation`);
|
|
@@ -14179,7 +14851,7 @@ ${setupResult.output || ""}`);
|
|
|
14179
14851
|
if (!result.success) {
|
|
14180
14852
|
throw new Error(`Document processing failed: ${result.stderr}`);
|
|
14181
14853
|
}
|
|
14182
|
-
const jsonContent = await
|
|
14854
|
+
const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
|
|
14183
14855
|
json = JSON.parse(jsonContent);
|
|
14184
14856
|
} else if (config?.processor.name === "officeparser") {
|
|
14185
14857
|
const text = await parseOfficeAsync2(buffer, {
|
|
@@ -14196,7 +14868,7 @@ ${setupResult.output || ""}`);
|
|
|
14196
14868
|
if (!MISTRAL_API_KEY) {
|
|
14197
14869
|
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
14870
|
}
|
|
14199
|
-
await new Promise((
|
|
14871
|
+
await new Promise((resolve4) => setTimeout(resolve4, Math.floor(Math.random() * 4e3) + 1e3));
|
|
14200
14872
|
const base64Pdf = buffer.toString("base64");
|
|
14201
14873
|
const client2 = new Mistral({ apiKey: MISTRAL_API_KEY });
|
|
14202
14874
|
const ocrResponse = await withRetry(async () => {
|
|
@@ -14212,9 +14884,9 @@ ${setupResult.output || ""}`);
|
|
|
14212
14884
|
}, 10);
|
|
14213
14885
|
const parser = new LiteParse();
|
|
14214
14886
|
const screenshots = await parser.screenshot(paths.source, void 0);
|
|
14215
|
-
await
|
|
14887
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
14216
14888
|
for (const screenshot of screenshots) {
|
|
14217
|
-
await
|
|
14889
|
+
await fs4.promises.writeFile(
|
|
14218
14890
|
path.join(
|
|
14219
14891
|
paths.images,
|
|
14220
14892
|
`${screenshot.pageNum}.png`
|
|
@@ -14229,15 +14901,15 @@ ${setupResult.output || ""}`);
|
|
|
14229
14901
|
image: screenshots.find((s) => s.pageNum === page.index + 1)?.imagePath,
|
|
14230
14902
|
headings: []
|
|
14231
14903
|
}));
|
|
14232
|
-
|
|
14904
|
+
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
14233
14905
|
} else if (config?.processor.name === "liteparse") {
|
|
14234
14906
|
const parser = new LiteParse();
|
|
14235
14907
|
const result = await parser.parse(paths.source);
|
|
14236
14908
|
const screenshots = await parser.screenshot(paths.source, void 0);
|
|
14237
14909
|
console.log(`[EXULU] Liteparse screenshots: ${JSON.stringify(screenshots)}`);
|
|
14238
|
-
await
|
|
14910
|
+
await fs4.promises.mkdir(paths.images, { recursive: true });
|
|
14239
14911
|
for (const screenshot of screenshots) {
|
|
14240
|
-
await
|
|
14912
|
+
await fs4.promises.writeFile(path.join(paths.images, `${screenshot.pageNum}.png`), screenshot.imageBuffer);
|
|
14241
14913
|
screenshot.imagePath = path.join(paths.images, `${screenshot.pageNum}.png`);
|
|
14242
14914
|
}
|
|
14243
14915
|
json = result.pages.map((page) => ({
|
|
@@ -14245,7 +14917,7 @@ ${setupResult.output || ""}`);
|
|
|
14245
14917
|
content: page.text,
|
|
14246
14918
|
image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
|
|
14247
14919
|
}));
|
|
14248
|
-
|
|
14920
|
+
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
14249
14921
|
}
|
|
14250
14922
|
console.log(`[EXULU]
|
|
14251
14923
|
\u2713 Document processing completed successfully`);
|
|
@@ -14276,13 +14948,13 @@ ${setupResult.output || ""}`);
|
|
|
14276
14948
|
console.log(`[EXULU] Corrected: ${page.vlm_corrected_text.substring(0, 150)}...`);
|
|
14277
14949
|
});
|
|
14278
14950
|
}
|
|
14279
|
-
await
|
|
14951
|
+
await fs4.promises.writeFile(
|
|
14280
14952
|
paths.json,
|
|
14281
14953
|
JSON.stringify(json, null, 2),
|
|
14282
14954
|
"utf-8"
|
|
14283
14955
|
);
|
|
14284
14956
|
}
|
|
14285
|
-
const markdownStream =
|
|
14957
|
+
const markdownStream = fs4.createWriteStream(paths.markdown, { encoding: "utf-8" });
|
|
14286
14958
|
for (let i = 0; i < json.length; i++) {
|
|
14287
14959
|
const p = json[i];
|
|
14288
14960
|
if (!p) continue;
|
|
@@ -14292,13 +14964,13 @@ ${setupResult.output || ""}`);
|
|
|
14292
14964
|
markdownStream.write("\n\n\n<!-- END_OF_PAGE -->\n\n\n");
|
|
14293
14965
|
}
|
|
14294
14966
|
}
|
|
14295
|
-
await new Promise((
|
|
14296
|
-
markdownStream.end(() =>
|
|
14967
|
+
await new Promise((resolve4, reject) => {
|
|
14968
|
+
markdownStream.end(() => resolve4());
|
|
14297
14969
|
markdownStream.on("error", reject);
|
|
14298
14970
|
});
|
|
14299
14971
|
console.log(`[EXULU] Validated output saved to: ${paths.json}`);
|
|
14300
14972
|
console.log(`[EXULU] Validated markdown saved to: ${paths.markdown}`);
|
|
14301
|
-
const markdown = await
|
|
14973
|
+
const markdown = await fs4.promises.readFile(paths.markdown, "utf-8");
|
|
14302
14974
|
const processedJson = json.map((e) => {
|
|
14303
14975
|
const finalContent = e.vlm_corrected_text ?? e.content;
|
|
14304
14976
|
return {
|
|
@@ -14329,7 +15001,7 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
14329
15001
|
let buffer;
|
|
14330
15002
|
if (Buffer.isBuffer(file)) {
|
|
14331
15003
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
14332
|
-
await
|
|
15004
|
+
await fs4.promises.writeFile(filePath, file);
|
|
14333
15005
|
buffer = file;
|
|
14334
15006
|
} else {
|
|
14335
15007
|
filePath = filePath.trim();
|
|
@@ -14337,11 +15009,11 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
14337
15009
|
const response = await fetch(filePath);
|
|
14338
15010
|
const array = await response.arrayBuffer();
|
|
14339
15011
|
const tempFilePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
14340
|
-
await
|
|
15012
|
+
await fs4.promises.writeFile(tempFilePath, Buffer.from(array));
|
|
14341
15013
|
buffer = Buffer.from(array);
|
|
14342
15014
|
filePath = tempFilePath;
|
|
14343
15015
|
} else {
|
|
14344
|
-
buffer = await
|
|
15016
|
+
buffer = await fs4.promises.readFile(file);
|
|
14345
15017
|
}
|
|
14346
15018
|
}
|
|
14347
15019
|
return { filePath, fileType, buffer };
|
|
@@ -14359,9 +15031,9 @@ async function documentProcessor({
|
|
|
14359
15031
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
14360
15032
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
14361
15033
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|
|
14362
|
-
await
|
|
15034
|
+
await fs4.promises.mkdir(tempDir, { recursive: true });
|
|
14363
15035
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
14364
|
-
await
|
|
15036
|
+
await fs4.promises.writeFile(path.join(tempDir, "created_at.txt"), timestamp);
|
|
14365
15037
|
try {
|
|
14366
15038
|
const {
|
|
14367
15039
|
filePath,
|
|
@@ -14402,7 +15074,7 @@ async function documentProcessor({
|
|
|
14402
15074
|
if (config?.debugging?.deleteTempFiles !== false) {
|
|
14403
15075
|
for (const file2 of localFilesAndFoldersToDelete) {
|
|
14404
15076
|
try {
|
|
14405
|
-
await
|
|
15077
|
+
await fs4.promises.rm(file2, { recursive: true });
|
|
14406
15078
|
console.log(`[EXULU] Deleted file or folder: ${file2}`);
|
|
14407
15079
|
} catch (error) {
|
|
14408
15080
|
console.error(`[EXULU] Error deleting file or folder: ${file2}`, error);
|
|
@@ -14463,8 +15135,8 @@ var ExuluVariables = {
|
|
|
14463
15135
|
throw new Error(`Variable ${name} not found.`);
|
|
14464
15136
|
}
|
|
14465
15137
|
if (variable.encrypted) {
|
|
14466
|
-
const bytes =
|
|
14467
|
-
variable.value = bytes.toString(
|
|
15138
|
+
const bytes = CryptoJS7.AES.decrypt(variable.value, process.env.NEXTAUTH_SECRET);
|
|
15139
|
+
variable.value = bytes.toString(CryptoJS7.enc.Utf8);
|
|
14468
15140
|
}
|
|
14469
15141
|
return variable.value;
|
|
14470
15142
|
}
|
|
@@ -14489,11 +15161,17 @@ var ExuluOtel = {
|
|
|
14489
15161
|
}
|
|
14490
15162
|
};
|
|
14491
15163
|
var ExuluDatabase = {
|
|
14492
|
-
init: async ({ contexts }) => {
|
|
15164
|
+
init: async ({ contexts, litellm }) => {
|
|
14493
15165
|
await execute({ contexts });
|
|
15166
|
+
if (litellm !== false) {
|
|
15167
|
+
await initLitellmDb();
|
|
15168
|
+
}
|
|
14494
15169
|
},
|
|
14495
|
-
update: async ({ contexts }) => {
|
|
15170
|
+
update: async ({ contexts, litellm }) => {
|
|
14496
15171
|
await execute({ contexts });
|
|
15172
|
+
if (litellm !== false) {
|
|
15173
|
+
await initLitellmDb();
|
|
15174
|
+
}
|
|
14497
15175
|
},
|
|
14498
15176
|
api: {
|
|
14499
15177
|
key: {
|