@exulu/backend 1.61.3 → 1.62.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.
@@ -236,7 +236,7 @@ var resolveConfig = (packageRoot) => {
236
236
  const host = process.env.LITELLM_HOST ?? "127.0.0.1";
237
237
  const port = process.env.LITELLM_PORT ?? "4000";
238
238
  const masterKey = process.env.LITELLM_MASTER_KEY;
239
- const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(packageRoot, "./config.litellm.yaml");
239
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(process.cwd(), "./config.litellm.yaml");
240
240
  const venvBin = resolve(packageRoot, "ee/python/.venv/bin");
241
241
  const venvPython = resolve(venvBin, "python");
242
242
  const litellmBin = resolve(venvBin, "litellm");
@@ -263,7 +263,13 @@ var spawnLiteLLM = (cfg) => {
263
263
  `Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
264
264
  );
265
265
  const { DEBUG: _debug, ...rest } = process.env;
266
- const childEnv = { ...rest, DEBUG: "false" };
266
+ const childEnv = {
267
+ ...rest,
268
+ DEBUG: "false"
269
+ };
270
+ if (process.env.EXULU_LITELLM_UI_PATH && !process.env.SERVER_ROOT_PATH) {
271
+ childEnv.SERVER_ROOT_PATH = process.env.EXULU_LITELLM_UI_PATH;
272
+ }
267
273
  const child = spawn(
268
274
  cfg.litellmBin,
269
275
  [
@@ -577,7 +583,17 @@ var getLiteLLMProvider = ({
577
583
  name: "litellm",
578
584
  baseURL: `http://${host}:${port}/v1`,
579
585
  apiKey: masterKey,
580
- fetch: createTaggedFetch(tags)
586
+ fetch: createTaggedFetch(tags),
587
+ // Without this flag the openai-compatible provider strips any
588
+ // responseFormat.schema before sending and warns
589
+ // "JSON response format schema is only supported with structuredOutputs".
590
+ // Models then return free-form JSON that fails Zod parsing in callers
591
+ // using `Output.object({ schema })`. LiteLLM forwards
592
+ // `response_format: { type: "json_schema", ... }` to every upstream it
593
+ // supports — including Vertex Gemini, which translates it into
594
+ // responseSchema/responseMimeType — so enabling this matches the actual
595
+ // proxy contract.
596
+ supportsStructuredOutputs: true
581
597
  });
582
598
  return _litellmProvider;
583
599
  };
@@ -744,7 +760,7 @@ var ExuluTool = class {
744
760
  });
745
761
  providerapikey = resolved.apiKey;
746
762
  }
747
- const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-VRZ45OGI.js");
763
+ const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-2HF7PPYW.js");
748
764
  const tools = await convertExuluToolsToAiSdkTools2(
749
765
  [this],
750
766
  [],
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  convertExuluToolsToAiSdkTools
3
- } from "./chunk-PFKAWGB6.js";
3
+ } from "./chunk-LYNLQWXC.js";
4
4
  export {
5
5
  convertExuluToolsToAiSdkTools
6
6
  };
package/dist/index.cjs CHANGED
@@ -403,7 +403,7 @@ var init_supervisor = __esm({
403
403
  const host = process.env.LITELLM_HOST ?? "127.0.0.1";
404
404
  const port = process.env.LITELLM_PORT ?? "4000";
405
405
  const masterKey = process.env.LITELLM_MASTER_KEY;
406
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path.resolve)(packageRoot, "./config.litellm.yaml");
406
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path.resolve)(process.cwd(), "./config.litellm.yaml");
407
407
  const venvBin = (0, import_node_path.resolve)(packageRoot, "ee/python/.venv/bin");
408
408
  const venvPython = (0, import_node_path.resolve)(venvBin, "python");
409
409
  const litellmBin = (0, import_node_path.resolve)(venvBin, "litellm");
@@ -430,7 +430,13 @@ var init_supervisor = __esm({
430
430
  `Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
431
431
  );
432
432
  const { DEBUG: _debug, ...rest } = process.env;
433
- const childEnv = { ...rest, DEBUG: "false" };
433
+ const childEnv = {
434
+ ...rest,
435
+ DEBUG: "false"
436
+ };
437
+ if (process.env.EXULU_LITELLM_UI_PATH && !process.env.SERVER_ROOT_PATH) {
438
+ childEnv.SERVER_ROOT_PATH = process.env.EXULU_LITELLM_UI_PATH;
439
+ }
434
440
  const child = (0, import_node_child_process.spawn)(
435
441
  cfg.litellmBin,
436
442
  [
@@ -1002,7 +1008,17 @@ var init_resolve_model = __esm({
1002
1008
  name: "litellm",
1003
1009
  baseURL: `http://${host}:${port}/v1`,
1004
1010
  apiKey: masterKey,
1005
- fetch: createTaggedFetch(tags)
1011
+ fetch: createTaggedFetch(tags),
1012
+ // Without this flag the openai-compatible provider strips any
1013
+ // responseFormat.schema before sending and warns
1014
+ // "JSON response format schema is only supported with structuredOutputs".
1015
+ // Models then return free-form JSON that fails Zod parsing in callers
1016
+ // using `Output.object({ schema })`. LiteLLM forwards
1017
+ // `response_format: { type: "json_schema", ... }` to every upstream it
1018
+ // supports — including Vertex Gemini, which translates it into
1019
+ // responseSchema/responseMimeType — so enabling this matches the actual
1020
+ // proxy contract.
1021
+ supportsStructuredOutputs: true
1006
1022
  });
1007
1023
  return _litellmProvider;
1008
1024
  };
@@ -13034,30 +13050,6 @@ type LiteLLMModel {
13034
13050
  pageInfo: PageInfo!
13035
13051
  }
13036
13052
  `;
13037
- typeDefs += `
13038
- agentWorldAgents: [AgentWorldAgent!]!
13039
- `;
13040
- resolvers.Query["agentWorldAgents"] = async (_, _args, context) => {
13041
- const { db: db2 } = context;
13042
- const sessions = await db2("agent_sessions as s").select([
13043
- "s.id as sessionId",
13044
- "s.agent as agentId",
13045
- "s.currenttask as currentTask",
13046
- "s.createdAt as lastActivityAt",
13047
- "s.created_by as userId"
13048
- ]).whereNotNull("s.currenttask").limit(20).orderBy("s.createdAt", "desc");
13049
- return sessions.map((row) => {
13050
- const agent = providers.find((p) => p.id === row.agentId);
13051
- return {
13052
- sessionId: row.sessionId,
13053
- agentId: row.agentId ?? "",
13054
- agentName: agent?.name ?? row.agentId ?? "Agent",
13055
- agentImage: agent?.image ?? null,
13056
- currentTask: row.currentTask,
13057
- lastActivityAt: row.lastActivityAt
13058
- };
13059
- });
13060
- };
13061
13053
  typeDefs += "}\n";
13062
13054
  mutationDefs += "}\n";
13063
13055
  const genericTypes = `
@@ -13272,15 +13264,6 @@ type StatisticsResult {
13272
13264
  group: String!
13273
13265
  count: Int!
13274
13266
  }
13275
-
13276
- type AgentWorldAgent {
13277
- sessionId: ID!
13278
- agentId: ID!
13279
- agentName: String!
13280
- agentImage: String
13281
- currentTask: String
13282
- lastActivityAt: String
13283
- }
13284
13267
  `;
13285
13268
  const fullSDL = typeDefs + mutationDefs + modelDefs + genericTypes;
13286
13269
  const schema = (0, import_schema.makeExecutableSchema)({
@@ -13491,7 +13474,7 @@ var import_utils5 = require("@apollo/utils.keyvaluecache");
13491
13474
  var import_body_parser = __toESM(require("body-parser"), 1);
13492
13475
  var import_crypto_js8 = __toESM(require("crypto-js"), 1);
13493
13476
  var import_openai = __toESM(require("openai"), 1);
13494
- var import_fs4 = __toESM(require("fs"), 1);
13477
+ var import_fs3 = __toESM(require("fs"), 1);
13495
13478
  var import_node_crypto5 = require("crypto");
13496
13479
  var import_api2 = require("@opentelemetry/api");
13497
13480
  init_check_record_access();
@@ -13574,7 +13557,7 @@ async function clearSessionCurrentTask(session) {
13574
13557
  }
13575
13558
 
13576
13559
  // src/exulu/provider.ts
13577
- var import_fs2 = __toESM(require("fs"), 1);
13560
+ var import_fs2 = require("fs");
13578
13561
  var ExuluProvider = class {
13579
13562
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
13580
13563
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -14237,7 +14220,6 @@ ${extractedText}
14237
14220
  // todo make this configurable?
14238
14221
  page: 1
14239
14222
  });
14240
- import_fs2.default.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
14241
14223
  if (result2?.chunks?.length) {
14242
14224
  memoryItems = result2.chunks;
14243
14225
  memoryContext = `
@@ -14888,212 +14870,6 @@ See docs/superpowers/specs/2026-05-31-in-chat-image-generation-design.md for the
14888
14870
 
14889
14871
  // src/exulu/routes.ts
14890
14872
  var import_node_path5 = require("path");
14891
-
14892
- // src/utils/python-setup.ts
14893
- init_cjs_shims();
14894
- var import_child_process = require("child_process");
14895
- var import_util = require("util");
14896
- var import_path = require("path");
14897
- var import_fs3 = require("fs");
14898
- var import_url = require("url");
14899
- var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
14900
- function getPackageRoot() {
14901
- const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
14902
- let currentDir = (0, import_path.dirname)(currentFile);
14903
- let attempts = 0;
14904
- const maxAttempts = 10;
14905
- while (attempts < maxAttempts) {
14906
- const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
14907
- if ((0, import_fs3.existsSync)(packageJsonPath)) {
14908
- try {
14909
- const packageJson = JSON.parse((0, import_fs3.readFileSync)(packageJsonPath, "utf-8"));
14910
- if (packageJson.name === "@exulu/backend") {
14911
- return currentDir;
14912
- }
14913
- } catch {
14914
- }
14915
- }
14916
- const parentDir = (0, import_path.resolve)(currentDir, "..");
14917
- if (parentDir === currentDir) {
14918
- break;
14919
- }
14920
- currentDir = parentDir;
14921
- attempts++;
14922
- }
14923
- const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
14924
- return fallback;
14925
- }
14926
- function getSetupScriptPath(packageRoot) {
14927
- return (0, import_path.resolve)(packageRoot, "ee/python/setup.sh");
14928
- }
14929
- function getVenvPath(packageRoot) {
14930
- return (0, import_path.resolve)(packageRoot, "ee/python/.venv");
14931
- }
14932
- function isPythonEnvironmentSetup(packageRoot) {
14933
- const root = packageRoot ?? getPackageRoot();
14934
- const venvPath = getVenvPath(root);
14935
- const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
14936
- return (0, import_fs3.existsSync)(venvPath) && (0, import_fs3.existsSync)(pythonPath);
14937
- }
14938
- async function setupPythonEnvironment(options = {}) {
14939
- const {
14940
- packageRoot = getPackageRoot(),
14941
- force = false,
14942
- verbose = false,
14943
- timeout = 6e5
14944
- // 10 minutes
14945
- } = options;
14946
- if (!force && isPythonEnvironmentSetup(packageRoot)) {
14947
- if (verbose) {
14948
- console.log("\u2713 Python environment already set up");
14949
- }
14950
- return {
14951
- success: true,
14952
- message: "Python environment already exists",
14953
- alreadyExists: true
14954
- };
14955
- }
14956
- const setupScriptPath = getSetupScriptPath(packageRoot);
14957
- if (!(0, import_fs3.existsSync)(setupScriptPath)) {
14958
- return {
14959
- success: false,
14960
- message: `Setup script not found at: ${setupScriptPath}`,
14961
- alreadyExists: false
14962
- };
14963
- }
14964
- try {
14965
- if (verbose) {
14966
- console.log("Setting up Python environment...");
14967
- }
14968
- const { stdout, stderr } = await execAsync4(`bash "${setupScriptPath}"`, {
14969
- cwd: packageRoot,
14970
- timeout,
14971
- env: {
14972
- ...process.env,
14973
- // Ensure script can write to the directory
14974
- PYTHONDONTWRITEBYTECODE: "1"
14975
- },
14976
- maxBuffer: 10 * 1024 * 1024
14977
- // 10MB buffer
14978
- });
14979
- const output = stdout + stderr;
14980
- const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
14981
- const pythonVersion = versionMatch ? versionMatch[1] : void 0;
14982
- if (verbose) {
14983
- console.log(output);
14984
- }
14985
- return {
14986
- success: true,
14987
- message: "Python environment set up successfully",
14988
- alreadyExists: false,
14989
- pythonVersion,
14990
- output
14991
- };
14992
- } catch (error) {
14993
- const errorOutput = error.stdout + error.stderr;
14994
- return {
14995
- success: false,
14996
- message: `Setup failed: ${error.message}`,
14997
- alreadyExists: false,
14998
- output: errorOutput
14999
- };
15000
- }
15001
- }
15002
- function getPythonSetupInstructions() {
15003
- return `
15004
- Python environment not set up. Please run one of the following commands:
15005
-
15006
- Option 1 (Automatic):
15007
- import { setupPythonEnvironment } from '@exulu/backend';
15008
- await setupPythonEnvironment();
15009
-
15010
- Option 2 (Manual - for package consumers):
15011
- npx @exulu/backend setup-python
15012
-
15013
- Option 3 (Manual - for contributors):
15014
- npm run python:setup
15015
-
15016
- These commands will automatically create a Python virtual environment (.venv)
15017
- in the @exulu/backend package and install all required dependencies.
15018
-
15019
- Requirements:
15020
- - Python 3.10 or higher must be installed
15021
- - pip must be available
15022
- - venv module must be available (for creating virtual environments)
15023
-
15024
- If Python dependencies are not installed, install them first, then run one of the commands above:
15025
- - macOS: brew install python@3.12
15026
- - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
15027
- - Alpine Linux: apk add python3 py3-pip python3-dev
15028
- - Windows: Download from https://www.python.org/downloads/
15029
-
15030
- Note: In Docker containers, ensure you install all three components:
15031
- Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
15032
- Alpine: apk add python3 py3-pip python3-dev
15033
- `.trim();
15034
- }
15035
- async function validatePythonEnvironment(packageRoot, checkPackages = true) {
15036
- const root = packageRoot ?? getPackageRoot();
15037
- const venvPath = getVenvPath(root);
15038
- const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
15039
- if (!(0, import_fs3.existsSync)(venvPath)) {
15040
- return {
15041
- valid: false,
15042
- message: getPythonSetupInstructions()
15043
- };
15044
- }
15045
- if (!(0, import_fs3.existsSync)(pythonPath)) {
15046
- return {
15047
- valid: false,
15048
- message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
15049
- };
15050
- }
15051
- try {
15052
- await execAsync4(`"${pythonPath}" --version`, { cwd: root });
15053
- } catch {
15054
- return {
15055
- valid: false,
15056
- message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
15057
- };
15058
- }
15059
- if (checkPackages) {
15060
- const criticalPackages = ["docling", "transformers"];
15061
- const missingPackages = [];
15062
- for (const pkg of criticalPackages) {
15063
- try {
15064
- await execAsync4(`"${pythonPath}" -c "import ${pkg}"`, {
15065
- cwd: root,
15066
- timeout: 1e4
15067
- // 10 second timeout per import check
15068
- });
15069
- } catch {
15070
- missingPackages.push(pkg);
15071
- }
15072
- }
15073
- if (missingPackages.length > 0) {
15074
- return {
15075
- valid: false,
15076
- message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
15077
-
15078
- This usually happens when:
15079
- 1. The .venv folder was copied but dependencies were not installed
15080
- 2. The package was installed via npm but setup script was not run
15081
-
15082
- Please run:
15083
- await setupPythonEnvironment({ force: true })
15084
-
15085
- Or manually run the setup script:
15086
- bash ` + getSetupScriptPath(root)
15087
- };
15088
- }
15089
- }
15090
- return {
15091
- valid: true,
15092
- message: "Python environment is valid"
15093
- };
15094
- }
15095
-
15096
- // src/exulu/routes.ts
15097
14873
  init_tags();
15098
14874
  var import_multer = __toESM(require("multer"), 1);
15099
14875
 
@@ -15769,7 +15545,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
15769
15545
  var getExuluVersionNumber = async () => {
15770
15546
  try {
15771
15547
  const path3 = process.cwd();
15772
- const packageJson = import_fs4.default.readFileSync(path3 + "/package.json", "utf8");
15548
+ const packageJson = import_fs3.default.readFileSync(path3 + "/package.json", "utf8");
15773
15549
  const packageData = JSON.parse(packageJson);
15774
15550
  const exuluVersion = packageData.dependencies["@exulu/backend"];
15775
15551
  console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
@@ -16689,7 +16465,7 @@ ${customInstructions}` : agent.instructions;
16689
16465
  const imageModelsByName = (() => {
16690
16466
  if (!isLiteLLMEnabled() || !config?.fileUploads) return /* @__PURE__ */ new Map();
16691
16467
  try {
16692
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path5.resolve)(getPackageRoot(), "./config.litellm.yaml");
16468
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path5.resolve)(process.cwd(), "./config.litellm.yaml");
16693
16469
  const models2 = parseImageGenerationModels(configPath);
16694
16470
  return new Map(models2.map((m) => [m.model_name, m]));
16695
16471
  } catch (err) {
@@ -17167,6 +16943,80 @@ ${style.markdown}` : params.prompt;
17167
16943
  }));
17168
16944
  res.status(200).json({ history });
17169
16945
  });
16946
+ const litellmUiPath = "/litellm-admin";
16947
+ if (isLiteLLMEnabled() && litellmUiPath) {
16948
+ console.log("[EXULU] Registering LiteLLM UI at", litellmUiPath);
16949
+ app.use(litellmUiPath, async (req, res) => {
16950
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
16951
+ const port = process.env.LITELLM_PORT ?? "4000";
16952
+ const upstreamUrl = `http://${host}:${port}${litellmUiPath}${req.url}`;
16953
+ const upstreamHeaders = {};
16954
+ for (const [name, value] of Object.entries(req.headers)) {
16955
+ if (value === void 0) continue;
16956
+ const lower = name.toLowerCase();
16957
+ if (lower === "host" || lower === "content-length" || lower === "connection" || lower === "transfer-encoding" || lower === "accept-encoding")
16958
+ continue;
16959
+ upstreamHeaders[name] = Array.isArray(value) ? value.join(", ") : value;
16960
+ }
16961
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
16962
+ let body;
16963
+ if (methodHasBody && req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
16964
+ body = JSON.stringify(req.body);
16965
+ upstreamHeaders["content-type"] = "application/json";
16966
+ }
16967
+ try {
16968
+ const upstream = await fetch(upstreamUrl, {
16969
+ method: req.method,
16970
+ headers: upstreamHeaders,
16971
+ body,
16972
+ // Pass redirects through to the browser so LiteLLM's post-login
16973
+ // redirect lands the user on the right URL (the Location already
16974
+ // includes SERVER_ROOT_PATH).
16975
+ redirect: "manual"
16976
+ });
16977
+ res.status(upstream.status);
16978
+ const upstreamOrigin = `http://${host}:${port}`;
16979
+ upstream.headers.forEach((value, name) => {
16980
+ const lower = name.toLowerCase();
16981
+ if (lower === "content-encoding" || lower === "content-length" || lower === "transfer-encoding" || lower === "connection")
16982
+ return;
16983
+ if (lower === "location") {
16984
+ let loc = value.startsWith(upstreamOrigin) ? value.slice(upstreamOrigin.length) : value;
16985
+ if (loc.startsWith("/") && loc !== litellmUiPath && !loc.startsWith(`${litellmUiPath}/`)) {
16986
+ loc = `${litellmUiPath}${loc}`;
16987
+ }
16988
+ res.setHeader(name, loc);
16989
+ return;
16990
+ }
16991
+ res.setHeader(name, value);
16992
+ });
16993
+ if (!upstream.body) {
16994
+ res.end();
16995
+ return;
16996
+ }
16997
+ const reader = upstream.body.getReader();
16998
+ try {
16999
+ while (true) {
17000
+ const { done, value } = await reader.read();
17001
+ if (done) break;
17002
+ if (value) res.write(value);
17003
+ }
17004
+ } finally {
17005
+ reader.releaseLock();
17006
+ }
17007
+ res.end();
17008
+ } catch (err) {
17009
+ console.error("[EXULU] LiteLLM UI proxy failed", err);
17010
+ if (!res.headersSent) {
17011
+ res.status(502).json({
17012
+ detail: err instanceof Error ? err.message : "LiteLLM UI proxy failed."
17013
+ });
17014
+ } else {
17015
+ res.end();
17016
+ }
17017
+ }
17018
+ });
17019
+ }
17170
17020
  app.use("/litellm/:project", async (req, res) => {
17171
17021
  if (!isLiteLLMEnabled()) {
17172
17022
  res.status(503).json({
@@ -20676,6 +20526,210 @@ init_entitlements();
20676
20526
  init_system_dependencies();
20677
20527
  init_supervisor();
20678
20528
 
20529
+ // src/utils/python-setup.ts
20530
+ init_cjs_shims();
20531
+ var import_child_process = require("child_process");
20532
+ var import_util = require("util");
20533
+ var import_path = require("path");
20534
+ var import_fs4 = require("fs");
20535
+ var import_url = require("url");
20536
+ var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
20537
+ function getPackageRoot() {
20538
+ const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
20539
+ let currentDir = (0, import_path.dirname)(currentFile);
20540
+ let attempts = 0;
20541
+ const maxAttempts = 10;
20542
+ while (attempts < maxAttempts) {
20543
+ const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
20544
+ if ((0, import_fs4.existsSync)(packageJsonPath)) {
20545
+ try {
20546
+ const packageJson = JSON.parse((0, import_fs4.readFileSync)(packageJsonPath, "utf-8"));
20547
+ if (packageJson.name === "@exulu/backend") {
20548
+ return currentDir;
20549
+ }
20550
+ } catch {
20551
+ }
20552
+ }
20553
+ const parentDir = (0, import_path.resolve)(currentDir, "..");
20554
+ if (parentDir === currentDir) {
20555
+ break;
20556
+ }
20557
+ currentDir = parentDir;
20558
+ attempts++;
20559
+ }
20560
+ const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
20561
+ return fallback;
20562
+ }
20563
+ function getSetupScriptPath(packageRoot) {
20564
+ return (0, import_path.resolve)(packageRoot, "ee/python/setup.sh");
20565
+ }
20566
+ function getVenvPath(packageRoot) {
20567
+ return (0, import_path.resolve)(packageRoot, "ee/python/.venv");
20568
+ }
20569
+ function isPythonEnvironmentSetup(packageRoot) {
20570
+ const root = packageRoot ?? getPackageRoot();
20571
+ const venvPath = getVenvPath(root);
20572
+ const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
20573
+ return (0, import_fs4.existsSync)(venvPath) && (0, import_fs4.existsSync)(pythonPath);
20574
+ }
20575
+ async function setupPythonEnvironment(options = {}) {
20576
+ const {
20577
+ packageRoot = getPackageRoot(),
20578
+ force = false,
20579
+ verbose = false,
20580
+ timeout = 6e5
20581
+ // 10 minutes
20582
+ } = options;
20583
+ if (!force && isPythonEnvironmentSetup(packageRoot)) {
20584
+ if (verbose) {
20585
+ console.log("\u2713 Python environment already set up");
20586
+ }
20587
+ return {
20588
+ success: true,
20589
+ message: "Python environment already exists",
20590
+ alreadyExists: true
20591
+ };
20592
+ }
20593
+ const setupScriptPath = getSetupScriptPath(packageRoot);
20594
+ if (!(0, import_fs4.existsSync)(setupScriptPath)) {
20595
+ return {
20596
+ success: false,
20597
+ message: `Setup script not found at: ${setupScriptPath}`,
20598
+ alreadyExists: false
20599
+ };
20600
+ }
20601
+ try {
20602
+ if (verbose) {
20603
+ console.log("Setting up Python environment...");
20604
+ }
20605
+ const { stdout, stderr } = await execAsync4(`bash "${setupScriptPath}"`, {
20606
+ cwd: packageRoot,
20607
+ timeout,
20608
+ env: {
20609
+ ...process.env,
20610
+ // Ensure script can write to the directory
20611
+ PYTHONDONTWRITEBYTECODE: "1"
20612
+ },
20613
+ maxBuffer: 10 * 1024 * 1024
20614
+ // 10MB buffer
20615
+ });
20616
+ const output = stdout + stderr;
20617
+ const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
20618
+ const pythonVersion = versionMatch ? versionMatch[1] : void 0;
20619
+ if (verbose) {
20620
+ console.log(output);
20621
+ }
20622
+ return {
20623
+ success: true,
20624
+ message: "Python environment set up successfully",
20625
+ alreadyExists: false,
20626
+ pythonVersion,
20627
+ output
20628
+ };
20629
+ } catch (error) {
20630
+ const errorOutput = error.stdout + error.stderr;
20631
+ return {
20632
+ success: false,
20633
+ message: `Setup failed: ${error.message}`,
20634
+ alreadyExists: false,
20635
+ output: errorOutput
20636
+ };
20637
+ }
20638
+ }
20639
+ function getPythonSetupInstructions() {
20640
+ return `
20641
+ Python environment not set up. Please run one of the following commands:
20642
+
20643
+ Option 1 (Automatic):
20644
+ import { setupPythonEnvironment } from '@exulu/backend';
20645
+ await setupPythonEnvironment();
20646
+
20647
+ Option 2 (Manual - for package consumers):
20648
+ npx @exulu/backend setup-python
20649
+
20650
+ Option 3 (Manual - for contributors):
20651
+ npm run python:setup
20652
+
20653
+ These commands will automatically create a Python virtual environment (.venv)
20654
+ in the @exulu/backend package and install all required dependencies.
20655
+
20656
+ Requirements:
20657
+ - Python 3.10 or higher must be installed
20658
+ - pip must be available
20659
+ - venv module must be available (for creating virtual environments)
20660
+
20661
+ If Python dependencies are not installed, install them first, then run one of the commands above:
20662
+ - macOS: brew install python@3.12
20663
+ - Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
20664
+ - Alpine Linux: apk add python3 py3-pip python3-dev
20665
+ - Windows: Download from https://www.python.org/downloads/
20666
+
20667
+ Note: In Docker containers, ensure you install all three components:
20668
+ Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
20669
+ Alpine: apk add python3 py3-pip python3-dev
20670
+ `.trim();
20671
+ }
20672
+ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
20673
+ const root = packageRoot ?? getPackageRoot();
20674
+ const venvPath = getVenvPath(root);
20675
+ const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
20676
+ if (!(0, import_fs4.existsSync)(venvPath)) {
20677
+ return {
20678
+ valid: false,
20679
+ message: getPythonSetupInstructions()
20680
+ };
20681
+ }
20682
+ if (!(0, import_fs4.existsSync)(pythonPath)) {
20683
+ return {
20684
+ valid: false,
20685
+ message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
20686
+ };
20687
+ }
20688
+ try {
20689
+ await execAsync4(`"${pythonPath}" --version`, { cwd: root });
20690
+ } catch {
20691
+ return {
20692
+ valid: false,
20693
+ message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
20694
+ };
20695
+ }
20696
+ if (checkPackages) {
20697
+ const criticalPackages = ["docling", "transformers"];
20698
+ const missingPackages = [];
20699
+ for (const pkg of criticalPackages) {
20700
+ try {
20701
+ await execAsync4(`"${pythonPath}" -c "import ${pkg}"`, {
20702
+ cwd: root,
20703
+ timeout: 1e4
20704
+ // 10 second timeout per import check
20705
+ });
20706
+ } catch {
20707
+ missingPackages.push(pkg);
20708
+ }
20709
+ }
20710
+ if (missingPackages.length > 0) {
20711
+ return {
20712
+ valid: false,
20713
+ message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
20714
+
20715
+ This usually happens when:
20716
+ 1. The .venv folder was copied but dependencies were not installed
20717
+ 2. The package was installed via npm but setup script was not run
20718
+
20719
+ Please run:
20720
+ await setupPythonEnvironment({ force: true })
20721
+
20722
+ Or manually run the setup script:
20723
+ bash ` + getSetupScriptPath(root)
20724
+ };
20725
+ }
20726
+ }
20727
+ return {
20728
+ valid: true,
20729
+ message: "Python environment is valid"
20730
+ };
20731
+ }
20732
+
20679
20733
  // src/templates/contexts/index.ts
20680
20734
  init_cjs_shims();
20681
20735
 
@@ -20838,6 +20892,12 @@ var ExuluApp = class {
20838
20892
  ...providers ?? []
20839
20893
  ];
20840
20894
  this._config = config;
20895
+ if (isLiteLLMEnabled()) {
20896
+ const uiPath = "/litellm-admin";
20897
+ if (uiPath) {
20898
+ process.env.EXULU_LITELLM_UI_PATH = uiPath;
20899
+ }
20900
+ }
20841
20901
  const transcriptionTools = [];
20842
20902
  if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
20843
20903
  transcriptionTools.push(transcribeTool);
@@ -20845,7 +20905,7 @@ var ExuluApp = class {
20845
20905
  const imageGenerationTools = [];
20846
20906
  const s3Configured = !!config?.fileUploads && !!config.fileUploads.s3region && !!config.fileUploads.s3key && !!config.fileUploads.s3secret && !!config.fileUploads.s3Bucket;
20847
20907
  if (isLiteLLMEnabled() && s3Configured) {
20848
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path7.resolve)(getPackageRoot(), "./config.litellm.yaml");
20908
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path7.resolve)(process.cwd(), "./config.litellm.yaml");
20849
20909
  const imageModels = parseImageGenerationModels(configPath);
20850
20910
  if (imageModels.length > 0) {
20851
20911
  console.log(
@@ -23119,7 +23179,7 @@ ${WARNING_BANNER}`);
23119
23179
  };
23120
23180
  var log3 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
23121
23181
  var initLiteLLMDatabase = async (packageRoot) => {
23122
- const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path8.resolve)(packageRoot, "./config.litellm.yaml");
23182
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path8.resolve)(process.cwd(), "./config.litellm.yaml");
23123
23183
  const safety = checkLiteLLMDatabaseSafety(configPath);
23124
23184
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
23125
23185
  if (!safety.ok && safety.reason === "unparseable-url") {
package/dist/index.js CHANGED
@@ -53,7 +53,7 @@ import {
53
53
  vectorSearch,
54
54
  waitForLiteLLMReady,
55
55
  withRetry
56
- } from "./chunk-PFKAWGB6.js";
56
+ } from "./chunk-LYNLQWXC.js";
57
57
  import {
58
58
  findLiteLLMModel
59
59
  } from "./chunk-ILAHW4UT.js";
@@ -5182,30 +5182,6 @@ type LiteLLMModel {
5182
5182
  pageInfo: PageInfo!
5183
5183
  }
5184
5184
  `;
5185
- typeDefs += `
5186
- agentWorldAgents: [AgentWorldAgent!]!
5187
- `;
5188
- resolvers.Query["agentWorldAgents"] = async (_, _args, context) => {
5189
- const { db } = context;
5190
- const sessions = await db("agent_sessions as s").select([
5191
- "s.id as sessionId",
5192
- "s.agent as agentId",
5193
- "s.currenttask as currentTask",
5194
- "s.createdAt as lastActivityAt",
5195
- "s.created_by as userId"
5196
- ]).whereNotNull("s.currenttask").limit(20).orderBy("s.createdAt", "desc");
5197
- return sessions.map((row) => {
5198
- const agent = providers.find((p) => p.id === row.agentId);
5199
- return {
5200
- sessionId: row.sessionId,
5201
- agentId: row.agentId ?? "",
5202
- agentName: agent?.name ?? row.agentId ?? "Agent",
5203
- agentImage: agent?.image ?? null,
5204
- currentTask: row.currentTask,
5205
- lastActivityAt: row.lastActivityAt
5206
- };
5207
- });
5208
- };
5209
5185
  typeDefs += "}\n";
5210
5186
  mutationDefs += "}\n";
5211
5187
  const genericTypes = `
@@ -5420,15 +5396,6 @@ type StatisticsResult {
5420
5396
  group: String!
5421
5397
  count: Int!
5422
5398
  }
5423
-
5424
- type AgentWorldAgent {
5425
- sessionId: ID!
5426
- agentId: ID!
5427
- agentName: String!
5428
- agentImage: String
5429
- currentTask: String
5430
- lastActivityAt: String
5431
- }
5432
5399
  `;
5433
5400
  const fullSDL = typeDefs + mutationDefs + modelDefs + genericTypes;
5434
5401
  const schema = makeExecutableSchema({
@@ -5703,7 +5670,7 @@ async function clearSessionCurrentTask(session) {
5703
5670
  }
5704
5671
 
5705
5672
  // src/exulu/provider.ts
5706
- import fs2 from "fs";
5673
+ import "fs";
5707
5674
  var ExuluProvider = class {
5708
5675
  // Must begin with a letter (a-z) or underscore (_). Subsequent characters in a name can be letters, digits (0-9), or
5709
5676
  // underscores and be a max length of 80 characters and at least 5 characters long.
@@ -6366,7 +6333,6 @@ ${extractedText}
6366
6333
  // todo make this configurable?
6367
6334
  page: 1
6368
6335
  });
6369
- fs2.writeFileSync("pre-fetched-relevant-information.json", JSON.stringify(result2, null, 2));
6370
6336
  if (result2?.chunks?.length) {
6371
6337
  memoryItems = result2.chunks;
6372
6338
  memoryContext = `
@@ -8594,7 +8560,7 @@ ${customInstructions}` : agent.instructions;
8594
8560
  const imageModelsByName = (() => {
8595
8561
  if (!isLiteLLMEnabled() || !config?.fileUploads) return /* @__PURE__ */ new Map();
8596
8562
  try {
8597
- const configPath = process.env.LITELLM_CONFIG_PATH ?? resolvePath(getPackageRoot(), "./config.litellm.yaml");
8563
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? resolvePath(process.cwd(), "./config.litellm.yaml");
8598
8564
  const models2 = parseImageGenerationModels(configPath);
8599
8565
  return new Map(models2.map((m) => [m.model_name, m]));
8600
8566
  } catch (err) {
@@ -9072,6 +9038,80 @@ ${style.markdown}` : params.prompt;
9072
9038
  }));
9073
9039
  res.status(200).json({ history });
9074
9040
  });
9041
+ const litellmUiPath = "/litellm-admin";
9042
+ if (isLiteLLMEnabled() && litellmUiPath) {
9043
+ console.log("[EXULU] Registering LiteLLM UI at", litellmUiPath);
9044
+ app.use(litellmUiPath, async (req, res) => {
9045
+ const host = process.env.LITELLM_HOST ?? "127.0.0.1";
9046
+ const port = process.env.LITELLM_PORT ?? "4000";
9047
+ const upstreamUrl = `http://${host}:${port}${litellmUiPath}${req.url}`;
9048
+ const upstreamHeaders = {};
9049
+ for (const [name, value] of Object.entries(req.headers)) {
9050
+ if (value === void 0) continue;
9051
+ const lower = name.toLowerCase();
9052
+ if (lower === "host" || lower === "content-length" || lower === "connection" || lower === "transfer-encoding" || lower === "accept-encoding")
9053
+ continue;
9054
+ upstreamHeaders[name] = Array.isArray(value) ? value.join(", ") : value;
9055
+ }
9056
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
9057
+ let body;
9058
+ if (methodHasBody && req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) {
9059
+ body = JSON.stringify(req.body);
9060
+ upstreamHeaders["content-type"] = "application/json";
9061
+ }
9062
+ try {
9063
+ const upstream = await fetch(upstreamUrl, {
9064
+ method: req.method,
9065
+ headers: upstreamHeaders,
9066
+ body,
9067
+ // Pass redirects through to the browser so LiteLLM's post-login
9068
+ // redirect lands the user on the right URL (the Location already
9069
+ // includes SERVER_ROOT_PATH).
9070
+ redirect: "manual"
9071
+ });
9072
+ res.status(upstream.status);
9073
+ const upstreamOrigin = `http://${host}:${port}`;
9074
+ upstream.headers.forEach((value, name) => {
9075
+ const lower = name.toLowerCase();
9076
+ if (lower === "content-encoding" || lower === "content-length" || lower === "transfer-encoding" || lower === "connection")
9077
+ return;
9078
+ if (lower === "location") {
9079
+ let loc = value.startsWith(upstreamOrigin) ? value.slice(upstreamOrigin.length) : value;
9080
+ if (loc.startsWith("/") && loc !== litellmUiPath && !loc.startsWith(`${litellmUiPath}/`)) {
9081
+ loc = `${litellmUiPath}${loc}`;
9082
+ }
9083
+ res.setHeader(name, loc);
9084
+ return;
9085
+ }
9086
+ res.setHeader(name, value);
9087
+ });
9088
+ if (!upstream.body) {
9089
+ res.end();
9090
+ return;
9091
+ }
9092
+ const reader = upstream.body.getReader();
9093
+ try {
9094
+ while (true) {
9095
+ const { done, value } = await reader.read();
9096
+ if (done) break;
9097
+ if (value) res.write(value);
9098
+ }
9099
+ } finally {
9100
+ reader.releaseLock();
9101
+ }
9102
+ res.end();
9103
+ } catch (err) {
9104
+ console.error("[EXULU] LiteLLM UI proxy failed", err);
9105
+ if (!res.headersSent) {
9106
+ res.status(502).json({
9107
+ detail: err instanceof Error ? err.message : "LiteLLM UI proxy failed."
9108
+ });
9109
+ } else {
9110
+ res.end();
9111
+ }
9112
+ }
9113
+ });
9114
+ }
9075
9115
  app.use("/litellm/:project", async (req, res) => {
9076
9116
  if (!isLiteLLMEnabled()) {
9077
9117
  res.status(503).json({
@@ -12677,6 +12717,12 @@ var ExuluApp = class {
12677
12717
  ...providers ?? []
12678
12718
  ];
12679
12719
  this._config = config;
12720
+ if (isLiteLLMEnabled()) {
12721
+ const uiPath = "/litellm-admin";
12722
+ if (uiPath) {
12723
+ process.env.EXULU_LITELLM_UI_PATH = uiPath;
12724
+ }
12725
+ }
12680
12726
  const transcriptionTools = [];
12681
12727
  if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
12682
12728
  transcriptionTools.push(transcribeTool);
@@ -12684,7 +12730,7 @@ var ExuluApp = class {
12684
12730
  const imageGenerationTools = [];
12685
12731
  const s3Configured = !!config?.fileUploads && !!config.fileUploads.s3region && !!config.fileUploads.s3key && !!config.fileUploads.s3secret && !!config.fileUploads.s3Bucket;
12686
12732
  if (isLiteLLMEnabled() && s3Configured) {
12687
- const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(getPackageRoot(), "./config.litellm.yaml");
12733
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve(process.cwd(), "./config.litellm.yaml");
12688
12734
  const imageModels = parseImageGenerationModels(configPath);
12689
12735
  if (imageModels.length > 0) {
12690
12736
  console.log(
@@ -14919,7 +14965,7 @@ ${WARNING_BANNER}`);
14919
14965
  };
14920
14966
  var log2 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
14921
14967
  var initLiteLLMDatabase = async (packageRoot) => {
14922
- const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(packageRoot, "./config.litellm.yaml");
14968
+ const configPath = process.env.LITELLM_CONFIG_PATH ?? resolve2(process.cwd(), "./config.litellm.yaml");
14923
14969
  const safety = checkLiteLLMDatabaseSafety(configPath);
14924
14970
  if (safety.ok && safety.reason === "no-litellm-db-mode") return;
14925
14971
  if (!safety.ok && safety.reason === "unparseable-url") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "1.61.3",
4
+ "version": "1.62.0",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {