@exulu/backend 3.7.3 → 3.7.4
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/{chunk-T6JVFT7L.js → chunk-27K2CO47.js} +5 -0
- package/dist/{chunk-BNTL6LYY.js → chunk-AWMU6QXB.js} +109 -22
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-UQSLJDXE.js → convert-exulu-tools-to-ai-sdk-tools-XNQ6Q3X6.js} +1 -1
- package/dist/index.cjs +381 -247
- package/dist/index.d.cts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.js +10 -4
- package/dist/python-setup-JZGHWQCG.js +17 -0
- package/ee/invoke-skills/artifact-filter.test.ts +49 -0
- package/ee/invoke-skills/artifact-filter.ts +38 -0
- package/ee/invoke-skills/create-sandbox.ts +56 -4
- package/ee/python/requirements.txt +5 -0
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -2252,11 +2252,6 @@ interface PythonSetupResult {
|
|
|
2252
2252
|
/** Full output from setup script */
|
|
2253
2253
|
output?: string;
|
|
2254
2254
|
}
|
|
2255
|
-
/**
|
|
2256
|
-
* Check if Python environment is already set up
|
|
2257
|
-
* Note: This only checks if the venv exists, not if packages are installed.
|
|
2258
|
-
* Use validatePythonEnvironment() for a more thorough check.
|
|
2259
|
-
*/
|
|
2260
2255
|
declare function isPythonEnvironmentSetup(packageRoot?: string): boolean;
|
|
2261
2256
|
/**
|
|
2262
2257
|
* Set up the Python environment by running the setup script
|
package/dist/index.d.ts
CHANGED
|
@@ -2252,11 +2252,6 @@ interface PythonSetupResult {
|
|
|
2252
2252
|
/** Full output from setup script */
|
|
2253
2253
|
output?: string;
|
|
2254
2254
|
}
|
|
2255
|
-
/**
|
|
2256
|
-
* Check if Python environment is already set up
|
|
2257
|
-
* Note: This only checks if the venv exists, not if packages are installed.
|
|
2258
|
-
* Use validatePythonEnvironment() for a more thorough check.
|
|
2259
|
-
*/
|
|
2260
2255
|
declare function isPythonEnvironmentSetup(packageRoot?: string): boolean;
|
|
2261
2256
|
/**
|
|
2262
2257
|
* Set up the Python environment by running the setup script
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isPythonEnvironmentSetup,
|
|
6
6
|
setupPythonEnvironment,
|
|
7
7
|
validatePythonEnvironment
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-27K2CO47.js";
|
|
9
9
|
import {
|
|
10
10
|
COMPACTION_INSUFFICIENT,
|
|
11
11
|
ContextCompactionRequiredError,
|
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
verifyCredentialNonce,
|
|
89
89
|
waitForLiteLLMReady,
|
|
90
90
|
withRetry
|
|
91
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-AWMU6QXB.js";
|
|
92
92
|
import {
|
|
93
93
|
LiteLLMAdminError,
|
|
94
94
|
findLiteLLMModel,
|
|
@@ -8285,10 +8285,12 @@ var generateSync = async ({
|
|
|
8285
8285
|
}
|
|
8286
8286
|
let project;
|
|
8287
8287
|
let sessionItems;
|
|
8288
|
+
let sessionOwnerId;
|
|
8288
8289
|
if (session) {
|
|
8289
8290
|
const sessionData = await getSession({ sessionID: session });
|
|
8290
8291
|
sessionItems = sessionData.session_items;
|
|
8291
8292
|
project = sessionData.project;
|
|
8293
|
+
sessionOwnerId = sessionData.user ?? void 0;
|
|
8292
8294
|
}
|
|
8293
8295
|
const model = languageModel;
|
|
8294
8296
|
console.log("[EXULU] Model created for generating sync.");
|
|
@@ -8392,7 +8394,8 @@ var generateSync = async ({
|
|
|
8392
8394
|
agent,
|
|
8393
8395
|
memoryItems,
|
|
8394
8396
|
contextWindow,
|
|
8395
|
-
disabledTools
|
|
8397
|
+
disabledTools,
|
|
8398
|
+
sessionOwnerId
|
|
8396
8399
|
);
|
|
8397
8400
|
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
8398
8401
|
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
@@ -8627,10 +8630,12 @@ var generateStream = async ({
|
|
|
8627
8630
|
let previousMessagesContent = previousMessages || [];
|
|
8628
8631
|
let project;
|
|
8629
8632
|
let sessionItems;
|
|
8633
|
+
let sessionOwnerId;
|
|
8630
8634
|
if (session) {
|
|
8631
8635
|
const sessionData = await getSession({ sessionID: session });
|
|
8632
8636
|
project = sessionData.project;
|
|
8633
8637
|
sessionItems = sessionData.session_items;
|
|
8638
|
+
sessionOwnerId = sessionData.user ?? void 0;
|
|
8634
8639
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
8635
8640
|
const previousMessages2 = await getAgentMessages({
|
|
8636
8641
|
session,
|
|
@@ -8810,7 +8815,8 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
8810
8815
|
agent,
|
|
8811
8816
|
memoryItems,
|
|
8812
8817
|
contextWindow,
|
|
8813
|
-
disabledTools
|
|
8818
|
+
disabledTools,
|
|
8819
|
+
sessionOwnerId
|
|
8814
8820
|
);
|
|
8815
8821
|
console.log("[EXULU] Converted tools", Object.keys(tools));
|
|
8816
8822
|
const includesContextSearchTool = currentTools?.some(
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import {
|
|
3
|
+
getPackageRoot,
|
|
4
|
+
getPythonSetupInstructions,
|
|
5
|
+
getPythonVenvPath,
|
|
6
|
+
isPythonEnvironmentSetup,
|
|
7
|
+
setupPythonEnvironment,
|
|
8
|
+
validatePythonEnvironment
|
|
9
|
+
} from "./chunk-27K2CO47.js";
|
|
10
|
+
export {
|
|
11
|
+
getPackageRoot,
|
|
12
|
+
getPythonSetupInstructions,
|
|
13
|
+
getPythonVenvPath,
|
|
14
|
+
isPythonEnvironmentSetup,
|
|
15
|
+
setupPythonEnvironment,
|
|
16
|
+
validatePythonEnvironment
|
|
17
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isIgnoredArtifactPath, capArtifacts, needsDownload } from "./artifact-filter";
|
|
2
|
+
|
|
3
|
+
describe("isIgnoredArtifactPath — dependency and cache trees are not session artifacts", () => {
|
|
4
|
+
it("ignores virtualenvs, node_modules, caches and VCS metadata at any depth", () => {
|
|
5
|
+
for (const p of [
|
|
6
|
+
"venv/pyvenv.cfg",
|
|
7
|
+
"venv/lib/python3.11/site-packages/docx/api.py",
|
|
8
|
+
".venv/bin/python",
|
|
9
|
+
"node_modules/lodash/index.js",
|
|
10
|
+
"scripts/__pycache__/create.cpython-311.pyc",
|
|
11
|
+
".cache/pip/http/x",
|
|
12
|
+
".git/objects/ab/cd",
|
|
13
|
+
"project/env/lib/python3.11/site-packages/x.py",
|
|
14
|
+
]) {
|
|
15
|
+
expect(isIgnoredArtifactPath(p)).toBe(true);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("keeps the files the agent actually produces", () => {
|
|
20
|
+
for (const p of ["Besprechungsprotokoll.docx", "create_minutes.py", "out/report.pdf", "Angebot/Angebot_107426.docx"]) {
|
|
21
|
+
expect(isIgnoredArtifactPath(p)).toBe(false);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("capArtifacts — a bash command that touches thousands of files must not flood the tool result", () => {
|
|
27
|
+
const many = Array.from({ length: 250 }, (_, i) => ({ relativePath: `out/file_${i}.txt`, url: `https://s3/${i}` }));
|
|
28
|
+
|
|
29
|
+
it("returns everything below the cap untouched", () => {
|
|
30
|
+
const { kept, omitted } = capArtifacts(many.slice(0, 10), 50);
|
|
31
|
+
expect(kept).toHaveLength(10);
|
|
32
|
+
expect(omitted).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps only the first N and reports how many were omitted", () => {
|
|
36
|
+
const { kept, omitted } = capArtifacts(many, 50);
|
|
37
|
+
expect(kept).toHaveLength(50);
|
|
38
|
+
expect(kept[0]!.relativePath).toBe("out/file_0.txt");
|
|
39
|
+
expect(omitted).toBe(200);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("needsDownload — resyncing S3 session files into a live sandbox", () => {
|
|
44
|
+
it("downloads when the local file is missing or differs in size, skips identical files", () => {
|
|
45
|
+
expect(needsDownload(undefined, 100)).toBe(true);
|
|
46
|
+
expect(needsDownload(99, 100)).toBe(true);
|
|
47
|
+
expect(needsDownload(100, 100)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sandbox mirrors every file a shell command creates under the session directory to
|
|
3
|
+
* S3 and lists each one (with a presigned URL) in the tool result. A single
|
|
4
|
+
* `python -m venv venv && pip install python-docx` creates thousands of files, which
|
|
5
|
+
* turned one tool result into ~1.8M characters and killed the session's context window.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const IGNORED_SEGMENTS = new Set([
|
|
9
|
+
"venv",
|
|
10
|
+
".venv",
|
|
11
|
+
"env",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"__pycache__",
|
|
14
|
+
"site-packages",
|
|
15
|
+
"dist-packages",
|
|
16
|
+
".cache",
|
|
17
|
+
".git",
|
|
18
|
+
".pytest_cache",
|
|
19
|
+
".mypy_cache",
|
|
20
|
+
".ipynb_checkpoints",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/** True when any path segment is a dependency, cache or VCS directory. */
|
|
24
|
+
export function isIgnoredArtifactPath(relativePath: string): boolean {
|
|
25
|
+
return relativePath.split(/[\\/]+/).some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_ARTIFACT_CAP = 50;
|
|
29
|
+
|
|
30
|
+
export function capArtifacts<T>(artifacts: T[], max: number = DEFAULT_ARTIFACT_CAP): { kept: T[]; omitted: number } {
|
|
31
|
+
if (artifacts.length <= max) return { kept: artifacts, omitted: 0 };
|
|
32
|
+
return { kept: artifacts.slice(0, max), omitted: artifacts.length - max };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Whether an S3 session file must be (re)downloaded into the live sandbox directory. */
|
|
36
|
+
export function needsDownload(localSize: number | undefined, remoteSize: number): boolean {
|
|
37
|
+
return localSize === undefined || localSize !== remoteSize;
|
|
38
|
+
}
|
|
@@ -8,6 +8,7 @@ import { join, dirname, resolve, relative, posix } from 'node:path'
|
|
|
8
8
|
import { exec, spawn } from 'node:child_process'
|
|
9
9
|
import { promisify } from 'node:util'
|
|
10
10
|
import { listS3ObjectsByPrefix, getS3ObjectBytes, uploadFile, getPresignedUrl, type S3FileObject } from '@SRC/uppy/index.ts'
|
|
11
|
+
import { isIgnoredArtifactPath, capArtifacts, needsDownload } from './artifact-filter'
|
|
11
12
|
import { getNpmGlobalRoot } from '@SRC/exulu/system-dependencies.ts'
|
|
12
13
|
import type { ExuluConfig } from '@SRC/exulu/app/index.ts'
|
|
13
14
|
import { createBashTool, type Sandbox } from "bash-tool";
|
|
@@ -270,6 +271,7 @@ async function restoreArtifactsFromS3(
|
|
|
270
271
|
sessionId: string,
|
|
271
272
|
userId: number | string,
|
|
272
273
|
config: ExuluConfig,
|
|
274
|
+
opts: { onlyMissing?: boolean } = {},
|
|
273
275
|
): Promise<void> {
|
|
274
276
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`
|
|
275
277
|
let objects: S3FileObject[]
|
|
@@ -297,8 +299,18 @@ async function restoreArtifactsFromS3(
|
|
|
297
299
|
const idx = obj.key.indexOf(userPrefix)
|
|
298
300
|
const relativePath = idx >= 0 ? obj.key.slice(idx + userPrefix.length) : ''
|
|
299
301
|
if (!relativePath) continue // directory marker or unexpected key shape
|
|
302
|
+
if (isIgnoredArtifactPath(relativePath)) continue // never pull dependency trees back
|
|
300
303
|
|
|
301
304
|
const localPath = join(sessionDir, relativePath)
|
|
305
|
+
if (opts.onlyMissing) {
|
|
306
|
+
let localSize: number | undefined
|
|
307
|
+
try {
|
|
308
|
+
localSize = (await stat(localPath)).size
|
|
309
|
+
} catch {
|
|
310
|
+
localSize = undefined
|
|
311
|
+
}
|
|
312
|
+
if (!needsDownload(localSize, obj.size)) continue
|
|
313
|
+
}
|
|
302
314
|
try {
|
|
303
315
|
// Use binary-safe fetch — session artifacts now include PDFs, .docx
|
|
304
316
|
// and other binary formats (from user uploads as well as agent
|
|
@@ -380,6 +392,20 @@ export async function downloadKeyIntoSandbox(opts: {
|
|
|
380
392
|
* cache AND no session directory on disk), previously persisted artifacts for
|
|
381
393
|
* the session are restored from S3 into the fresh session directory.
|
|
382
394
|
*/
|
|
395
|
+
/**
|
|
396
|
+
* Loaded lazily: python-setup resolves the package root via import.meta.url, which
|
|
397
|
+
* must not be evaluated when this module is merely imported (jest runs CJS).
|
|
398
|
+
*/
|
|
399
|
+
async function resolvePythonVenvPath(): Promise<string | undefined> {
|
|
400
|
+
try {
|
|
401
|
+
const { getPythonVenvPath } = await import('@SRC/utils/python-setup.ts')
|
|
402
|
+
return getPythonVenvPath()
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.warn('[SKILLS] Could not resolve the Python venv for the session sandbox; skill scripts fall back to the system python.', err)
|
|
405
|
+
return undefined
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
383
409
|
export async function createSessionSandbox(
|
|
384
410
|
sessionId: string,
|
|
385
411
|
skills: SkillRef[],
|
|
@@ -404,6 +430,16 @@ export async function createSessionSandbox(
|
|
|
404
430
|
cached.installedSkills.set(skill.id, skill.current_version)
|
|
405
431
|
}
|
|
406
432
|
|
|
433
|
+
// Files uploaded through the side panel (or by an email routine) after the
|
|
434
|
+
// sandbox was created live only in S3 until re-synced; without this the agent's
|
|
435
|
+
// `ls` never sees them although the system prompt promises it will.
|
|
436
|
+
if (userId && config.fileUploads) {
|
|
437
|
+
try {
|
|
438
|
+
await restoreArtifactsFromS3(cached.handle.sessionDir, sessionId, userId, config, { onlyMissing: true })
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.error(`[SKILLS] Failed to re-sync S3 session files for session ${sessionId}; continuing.`, err)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
407
443
|
return cached.handle
|
|
408
444
|
}
|
|
409
445
|
|
|
@@ -439,8 +475,9 @@ export async function createSessionSandbox(
|
|
|
439
475
|
// Restore artifacts from S3 only on a true cold start. If the dir already
|
|
440
476
|
// existed, the local files are at least as new as S3 and may contain
|
|
441
477
|
// unsaved-to-S3 writes from a prior process.
|
|
442
|
-
if (userId && config.fileUploads
|
|
443
|
-
|
|
478
|
+
if (userId && config.fileUploads) {
|
|
479
|
+
// Cold start restores everything; a surviving on-disk dir only fetches what changed.
|
|
480
|
+
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config, { onlyMissing: dirExisted })
|
|
444
481
|
}
|
|
445
482
|
|
|
446
483
|
// Probe whether bwrap can actually create namespaces on this host. On a
|
|
@@ -492,6 +529,9 @@ export async function createSessionSandbox(
|
|
|
492
529
|
// fail with a clear MODULE_NOT_FOUND, matching what the user would see
|
|
493
530
|
// outside the sandbox).
|
|
494
531
|
const npmGlobalRoot = await getNpmGlobalRoot()
|
|
532
|
+
// The @exulu/backend Python venv carries the skill runtime deps (python-docx …).
|
|
533
|
+
// Put its bin dir first on PATH so `python3` inside the sandbox resolves to it.
|
|
534
|
+
const pythonVenvPath = await resolvePythonVenvPath()
|
|
495
535
|
|
|
496
536
|
// Per-session policy. Passed to every wrapWithSandbox() invocation made
|
|
497
537
|
// from within this closure. customConfig wins over the singleton's
|
|
@@ -510,6 +550,7 @@ export async function createSessionSandbox(
|
|
|
510
550
|
// the sandbox. Without this, `require('docx')` fails with
|
|
511
551
|
// EPERM even when NODE_PATH points the resolver here.
|
|
512
552
|
...(npmGlobalRoot ? [npmGlobalRoot] : []),
|
|
553
|
+
...(pythonVenvPath ? [pythonVenvPath] : []),
|
|
513
554
|
],
|
|
514
555
|
allowWrite: [sessionDir],
|
|
515
556
|
denyWrite: [],
|
|
@@ -563,6 +604,9 @@ export async function createSessionSandbox(
|
|
|
563
604
|
...configuredVariables,
|
|
564
605
|
...process.env,
|
|
565
606
|
...(npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}),
|
|
607
|
+
...(pythonVenvPath
|
|
608
|
+
? { PATH: `${join(pythonVenvPath, 'bin')}:${process.env.PATH ?? ''}`, VIRTUAL_ENV: pythonVenvPath }
|
|
609
|
+
: {}),
|
|
566
610
|
}
|
|
567
611
|
|
|
568
612
|
// Either wrap a command with bwrap/sandbox-exec or return it unchanged when
|
|
@@ -757,6 +801,7 @@ export async function createSessionSandbox(
|
|
|
757
801
|
for (const entry of entries) {
|
|
758
802
|
const full = join(dir, entry.name)
|
|
759
803
|
if (full === skillsDir) continue
|
|
804
|
+
if (isIgnoredArtifactPath(relative(sessionDir, full))) continue
|
|
760
805
|
if (entry.isDirectory()) {
|
|
761
806
|
await walk(full)
|
|
762
807
|
} else if (entry.isFile()) {
|
|
@@ -917,19 +962,26 @@ export async function createSessionSandbox(
|
|
|
917
962
|
// marker block isn't truncated. Only files with a presigned URL
|
|
918
963
|
// appear here; locally-only entries would just confuse the user.
|
|
919
964
|
let stdout = result?.stdout ?? ''
|
|
920
|
-
|
|
965
|
+
// Cap the listing: one command can touch thousands of files, and every
|
|
966
|
+
// entry carries a presigned URL. The full set is still mirrored to S3.
|
|
967
|
+
const { kept, omitted } = capArtifacts(artifacts)
|
|
968
|
+
const withUrls = kept.filter((a) => a.url)
|
|
921
969
|
if (withUrls.length > 0) {
|
|
922
970
|
const lines = ['', '[exulu-artifacts]']
|
|
923
971
|
for (const a of withUrls) {
|
|
924
972
|
lines.push(` ${a.relativePath}: ${a.url}`)
|
|
925
973
|
}
|
|
974
|
+
if (omitted > 0) {
|
|
975
|
+
lines.push(` … ${omitted} more file(s) were created and mirrored but are not listed here.`)
|
|
976
|
+
}
|
|
926
977
|
stdout = `${stdout}\n${lines.join('\n')}`
|
|
927
978
|
}
|
|
928
979
|
|
|
929
980
|
return {
|
|
930
981
|
...result,
|
|
931
982
|
stdout,
|
|
932
|
-
artifacts,
|
|
983
|
+
artifacts: kept,
|
|
984
|
+
...(omitted > 0 ? { artifactsOmitted: omitted } : {}),
|
|
933
985
|
}
|
|
934
986
|
},
|
|
935
987
|
})
|
|
@@ -52,3 +52,8 @@ prisma==0.15.0
|
|
|
52
52
|
# NOT included in litellm[proxy]; without it, requests to Vertex models
|
|
53
53
|
# fail with "No module named 'vertexai'".
|
|
54
54
|
google-cloud-aiplatform>=1.38
|
|
55
|
+
|
|
56
|
+
# Skill runtime dependencies — the session sandbox puts this venv's bin dir on PATH so
|
|
57
|
+
# skill scripts (e.g. the built-in docx-manipulation skill) find their libraries without
|
|
58
|
+
# creating a venv per session (which mirrored thousands of files into the tool result).
|
|
59
|
+
python-docx>=1.1
|