@exulu/backend 3.7.3 → 4.0.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/{chunk-T6JVFT7L.js → chunk-QMN6MVHQ.js} +6 -1
- package/dist/{chunk-BNTL6LYY.js → chunk-RBEWHG7I.js} +404 -59
- 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-6RU4IZMI.js} +1 -1
- package/dist/index.cjs +957 -474
- package/dist/index.d.cts +3 -6
- package/dist/index.d.ts +3 -6
- package/dist/index.js +258 -182
- package/dist/python-setup-DRJ3QX5F.js +17 -0
- package/ee/LICENSE.md +2 -2
- package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
- package/ee/agentic-retrieval/pipeline/config.ts +15 -0
- package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
- package/ee/agentic-retrieval/pipeline/index.ts +67 -13
- package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
- package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
- package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
- package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
- package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
- package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
- package/ee/agentic-retrieval/pipeline/search.ts +9 -6
- package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
- package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
- package/ee/agentic-retrieval/pipeline/types.ts +2 -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/documents/processing/README.md +2 -3
- package/ee/python/documents/processing/doc_processor.ts +21 -61
- package/ee/python/documents/processing/split_pdf.py +25 -30
- package/ee/python/documents/processing/tests/__init__.py +0 -0
- package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
- package/ee/python/requirements.txt +17 -2
- package/ee/python/setup.sh +40 -1
- package/ee/python/transcription/pipeline.py +109 -15
- package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
- package/ee/workers.ts +2 -7
- package/license.md +2 -2
- package/package.json +3 -4
- package/scripts/postinstall.cjs +52 -1
- package/ee/python/documents/processing/document_to_markdown.py +0 -413
package/dist/index.cjs
CHANGED
|
@@ -3727,6 +3727,40 @@ Current item: ${JSON.stringify(summary)}`
|
|
|
3727
3727
|
}
|
|
3728
3728
|
});
|
|
3729
3729
|
|
|
3730
|
+
// ee/invoke-skills/artifact-filter.ts
|
|
3731
|
+
function isIgnoredArtifactPath(relativePath) {
|
|
3732
|
+
return relativePath.split(/[\\/]+/).some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
3733
|
+
}
|
|
3734
|
+
function capArtifacts(artifacts, max = DEFAULT_ARTIFACT_CAP) {
|
|
3735
|
+
if (artifacts.length <= max) return { kept: artifacts, omitted: 0 };
|
|
3736
|
+
return { kept: artifacts.slice(0, max), omitted: artifacts.length - max };
|
|
3737
|
+
}
|
|
3738
|
+
function needsDownload(localSize, remoteSize) {
|
|
3739
|
+
return localSize === void 0 || localSize !== remoteSize;
|
|
3740
|
+
}
|
|
3741
|
+
var IGNORED_SEGMENTS, DEFAULT_ARTIFACT_CAP;
|
|
3742
|
+
var init_artifact_filter = __esm({
|
|
3743
|
+
"ee/invoke-skills/artifact-filter.ts"() {
|
|
3744
|
+
"use strict";
|
|
3745
|
+
init_cjs_shims();
|
|
3746
|
+
IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
3747
|
+
"venv",
|
|
3748
|
+
".venv",
|
|
3749
|
+
"env",
|
|
3750
|
+
"node_modules",
|
|
3751
|
+
"__pycache__",
|
|
3752
|
+
"site-packages",
|
|
3753
|
+
"dist-packages",
|
|
3754
|
+
".cache",
|
|
3755
|
+
".git",
|
|
3756
|
+
".pytest_cache",
|
|
3757
|
+
".mypy_cache",
|
|
3758
|
+
".ipynb_checkpoints"
|
|
3759
|
+
]);
|
|
3760
|
+
DEFAULT_ARTIFACT_CAP = 50;
|
|
3761
|
+
}
|
|
3762
|
+
});
|
|
3763
|
+
|
|
3730
3764
|
// src/exulu/system-dependencies.ts
|
|
3731
3765
|
async function getNpmGlobalRoot() {
|
|
3732
3766
|
if (cachedNpmGlobalRoot !== void 0) return cachedNpmGlobalRoot;
|
|
@@ -3856,6 +3890,229 @@ var init_variable = __esm({
|
|
|
3856
3890
|
}
|
|
3857
3891
|
});
|
|
3858
3892
|
|
|
3893
|
+
// src/utils/python-setup.ts
|
|
3894
|
+
var python_setup_exports = {};
|
|
3895
|
+
__export(python_setup_exports, {
|
|
3896
|
+
getPackageRoot: () => getPackageRoot,
|
|
3897
|
+
getPythonSetupInstructions: () => getPythonSetupInstructions,
|
|
3898
|
+
getPythonVenvPath: () => getPythonVenvPath,
|
|
3899
|
+
isPythonEnvironmentSetup: () => isPythonEnvironmentSetup,
|
|
3900
|
+
setupPythonEnvironment: () => setupPythonEnvironment,
|
|
3901
|
+
validatePythonEnvironment: () => validatePythonEnvironment
|
|
3902
|
+
});
|
|
3903
|
+
function getPackageRoot() {
|
|
3904
|
+
const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
3905
|
+
let currentDir = (0, import_path.dirname)(currentFile);
|
|
3906
|
+
let attempts = 0;
|
|
3907
|
+
const maxAttempts = 10;
|
|
3908
|
+
while (attempts < maxAttempts) {
|
|
3909
|
+
const packageJsonPath = (0, import_path.join)(currentDir, "package.json");
|
|
3910
|
+
if ((0, import_fs.existsSync)(packageJsonPath)) {
|
|
3911
|
+
try {
|
|
3912
|
+
const packageJson = JSON.parse((0, import_fs.readFileSync)(packageJsonPath, "utf-8"));
|
|
3913
|
+
if (packageJson.name === "@exulu/backend") {
|
|
3914
|
+
return currentDir;
|
|
3915
|
+
}
|
|
3916
|
+
} catch {
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
const parentDir = (0, import_path.resolve)(currentDir, "..");
|
|
3920
|
+
if (parentDir === currentDir) {
|
|
3921
|
+
break;
|
|
3922
|
+
}
|
|
3923
|
+
currentDir = parentDir;
|
|
3924
|
+
attempts++;
|
|
3925
|
+
}
|
|
3926
|
+
const fallback = (0, import_path.resolve)((0, import_path.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
|
|
3927
|
+
return fallback;
|
|
3928
|
+
}
|
|
3929
|
+
function getSetupScriptPath(packageRoot) {
|
|
3930
|
+
return (0, import_path.resolve)(packageRoot, "ee/python/setup.sh");
|
|
3931
|
+
}
|
|
3932
|
+
function getVenvPath(packageRoot) {
|
|
3933
|
+
return (0, import_path.resolve)(packageRoot, "ee/python/.venv");
|
|
3934
|
+
}
|
|
3935
|
+
function getPythonVenvPath(packageRoot) {
|
|
3936
|
+
const root = packageRoot ?? getPackageRoot();
|
|
3937
|
+
return isPythonEnvironmentSetup(root) ? getVenvPath(root) : void 0;
|
|
3938
|
+
}
|
|
3939
|
+
function isPythonEnvironmentSetup(packageRoot) {
|
|
3940
|
+
const root = packageRoot ?? getPackageRoot();
|
|
3941
|
+
const venvPath = getVenvPath(root);
|
|
3942
|
+
const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
|
|
3943
|
+
return (0, import_fs.existsSync)(venvPath) && (0, import_fs.existsSync)(pythonPath);
|
|
3944
|
+
}
|
|
3945
|
+
async function setupPythonEnvironment(options = {}) {
|
|
3946
|
+
const {
|
|
3947
|
+
packageRoot = getPackageRoot(),
|
|
3948
|
+
force = false,
|
|
3949
|
+
verbose = false,
|
|
3950
|
+
timeout = 6e5
|
|
3951
|
+
// 10 minutes
|
|
3952
|
+
} = options;
|
|
3953
|
+
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
3954
|
+
if (verbose) {
|
|
3955
|
+
console.log("\u2713 Python environment already set up");
|
|
3956
|
+
}
|
|
3957
|
+
return {
|
|
3958
|
+
success: true,
|
|
3959
|
+
message: "Python environment already exists",
|
|
3960
|
+
alreadyExists: true
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
3964
|
+
if (!(0, import_fs.existsSync)(setupScriptPath)) {
|
|
3965
|
+
return {
|
|
3966
|
+
success: false,
|
|
3967
|
+
message: `Setup script not found at: ${setupScriptPath}`,
|
|
3968
|
+
alreadyExists: false
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
try {
|
|
3972
|
+
if (verbose) {
|
|
3973
|
+
console.log("Setting up Python environment...");
|
|
3974
|
+
}
|
|
3975
|
+
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
3976
|
+
cwd: packageRoot,
|
|
3977
|
+
timeout,
|
|
3978
|
+
env: {
|
|
3979
|
+
...process.env,
|
|
3980
|
+
// Ensure script can write to the directory
|
|
3981
|
+
PYTHONDONTWRITEBYTECODE: "1"
|
|
3982
|
+
},
|
|
3983
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3984
|
+
// 10MB buffer
|
|
3985
|
+
});
|
|
3986
|
+
const output = stdout + stderr;
|
|
3987
|
+
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
3988
|
+
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
3989
|
+
if (verbose) {
|
|
3990
|
+
console.log(output);
|
|
3991
|
+
}
|
|
3992
|
+
return {
|
|
3993
|
+
success: true,
|
|
3994
|
+
message: "Python environment set up successfully",
|
|
3995
|
+
alreadyExists: false,
|
|
3996
|
+
pythonVersion,
|
|
3997
|
+
output
|
|
3998
|
+
};
|
|
3999
|
+
} catch (error) {
|
|
4000
|
+
const errorOutput = error.stdout + error.stderr;
|
|
4001
|
+
return {
|
|
4002
|
+
success: false,
|
|
4003
|
+
message: `Setup failed: ${error.message}`,
|
|
4004
|
+
alreadyExists: false,
|
|
4005
|
+
output: errorOutput
|
|
4006
|
+
};
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
function getPythonSetupInstructions() {
|
|
4010
|
+
return `
|
|
4011
|
+
Python environment not set up. Please run one of the following commands:
|
|
4012
|
+
|
|
4013
|
+
Option 1 (Automatic):
|
|
4014
|
+
import { setupPythonEnvironment } from '@exulu/backend';
|
|
4015
|
+
await setupPythonEnvironment();
|
|
4016
|
+
|
|
4017
|
+
Option 2 (Manual - for package consumers):
|
|
4018
|
+
npx @exulu/backend setup-python
|
|
4019
|
+
|
|
4020
|
+
Option 3 (Manual - for contributors):
|
|
4021
|
+
npm run python:setup
|
|
4022
|
+
|
|
4023
|
+
These commands will automatically create a Python virtual environment (.venv)
|
|
4024
|
+
in the @exulu/backend package and install all required dependencies.
|
|
4025
|
+
|
|
4026
|
+
Requirements:
|
|
4027
|
+
- Python 3.10 or higher must be installed
|
|
4028
|
+
- pip must be available
|
|
4029
|
+
- venv module must be available (for creating virtual environments)
|
|
4030
|
+
|
|
4031
|
+
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
4032
|
+
- macOS: brew install python@3.12
|
|
4033
|
+
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
4034
|
+
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
4035
|
+
- Windows: Download from https://www.python.org/downloads/
|
|
4036
|
+
|
|
4037
|
+
Note: In Docker containers, ensure you install all three components:
|
|
4038
|
+
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
4039
|
+
Alpine: apk add python3 py3-pip python3-dev
|
|
4040
|
+
`.trim();
|
|
4041
|
+
}
|
|
4042
|
+
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
4043
|
+
const root = packageRoot ?? getPackageRoot();
|
|
4044
|
+
const venvPath = getVenvPath(root);
|
|
4045
|
+
const pythonPath = (0, import_path.join)(venvPath, "bin", "python");
|
|
4046
|
+
if (!(0, import_fs.existsSync)(venvPath)) {
|
|
4047
|
+
return {
|
|
4048
|
+
valid: false,
|
|
4049
|
+
message: getPythonSetupInstructions()
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
if (!(0, import_fs.existsSync)(pythonPath)) {
|
|
4053
|
+
return {
|
|
4054
|
+
valid: false,
|
|
4055
|
+
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
try {
|
|
4059
|
+
await execAsync2(`"${pythonPath}" --version`, { cwd: root });
|
|
4060
|
+
} catch {
|
|
4061
|
+
return {
|
|
4062
|
+
valid: false,
|
|
4063
|
+
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
if (checkPackages) {
|
|
4067
|
+
const criticalPackages = ["pypdf", "transformers"];
|
|
4068
|
+
const missingPackages = [];
|
|
4069
|
+
for (const pkg of criticalPackages) {
|
|
4070
|
+
try {
|
|
4071
|
+
await execAsync2(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
4072
|
+
cwd: root,
|
|
4073
|
+
timeout: 1e4
|
|
4074
|
+
// 10 second timeout per import check
|
|
4075
|
+
});
|
|
4076
|
+
} catch {
|
|
4077
|
+
missingPackages.push(pkg);
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
if (missingPackages.length > 0) {
|
|
4081
|
+
return {
|
|
4082
|
+
valid: false,
|
|
4083
|
+
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
4084
|
+
|
|
4085
|
+
This usually happens when:
|
|
4086
|
+
1. The .venv folder was copied but dependencies were not installed
|
|
4087
|
+
2. The package was installed via npm but setup script was not run
|
|
4088
|
+
|
|
4089
|
+
Please run:
|
|
4090
|
+
await setupPythonEnvironment({ force: true })
|
|
4091
|
+
|
|
4092
|
+
Or manually run the setup script:
|
|
4093
|
+
bash ` + getSetupScriptPath(root)
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
return {
|
|
4098
|
+
valid: true,
|
|
4099
|
+
message: "Python environment is valid"
|
|
4100
|
+
};
|
|
4101
|
+
}
|
|
4102
|
+
var import_child_process, import_util, import_path, import_fs, import_url, execAsync2;
|
|
4103
|
+
var init_python_setup = __esm({
|
|
4104
|
+
"src/utils/python-setup.ts"() {
|
|
4105
|
+
"use strict";
|
|
4106
|
+
init_cjs_shims();
|
|
4107
|
+
import_child_process = require("child_process");
|
|
4108
|
+
import_util = require("util");
|
|
4109
|
+
import_path = require("path");
|
|
4110
|
+
import_fs = require("fs");
|
|
4111
|
+
import_url = require("url");
|
|
4112
|
+
execAsync2 = (0, import_util.promisify)(import_child_process.exec);
|
|
4113
|
+
}
|
|
4114
|
+
});
|
|
4115
|
+
|
|
3859
4116
|
// ee/invoke-skills/create-sandbox.ts
|
|
3860
4117
|
function probeSandboxSupport() {
|
|
3861
4118
|
if (sandboxProbePromise) return sandboxProbePromise;
|
|
@@ -3941,7 +4198,7 @@ function resolveSessionPath(inputPath, sessionDir) {
|
|
|
3941
4198
|
}
|
|
3942
4199
|
return resolved;
|
|
3943
4200
|
}
|
|
3944
|
-
async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
4201
|
+
async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config, opts = {}) {
|
|
3945
4202
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`;
|
|
3946
4203
|
let objects;
|
|
3947
4204
|
try {
|
|
@@ -3961,7 +4218,17 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
|
3961
4218
|
const idx = obj.key.indexOf(userPrefix);
|
|
3962
4219
|
const relativePath = idx >= 0 ? obj.key.slice(idx + userPrefix.length) : "";
|
|
3963
4220
|
if (!relativePath) continue;
|
|
4221
|
+
if (isIgnoredArtifactPath(relativePath)) continue;
|
|
3964
4222
|
const localPath = (0, import_node_path4.join)(sessionDir, relativePath);
|
|
4223
|
+
if (opts.onlyMissing) {
|
|
4224
|
+
let localSize;
|
|
4225
|
+
try {
|
|
4226
|
+
localSize = (await (0, import_promises.stat)(localPath)).size;
|
|
4227
|
+
} catch {
|
|
4228
|
+
localSize = void 0;
|
|
4229
|
+
}
|
|
4230
|
+
if (!needsDownload(localSize, obj.size)) continue;
|
|
4231
|
+
}
|
|
3965
4232
|
try {
|
|
3966
4233
|
const bytes = await getS3ObjectBytes(obj.key, config);
|
|
3967
4234
|
await (0, import_promises.mkdir)((0, import_node_path4.dirname)(localPath), { recursive: true });
|
|
@@ -3995,6 +4262,15 @@ async function downloadKeyIntoSandbox(opts) {
|
|
|
3995
4262
|
await (0, import_promises.writeFile)(localPath, bytes);
|
|
3996
4263
|
return { written: true, localPath };
|
|
3997
4264
|
}
|
|
4265
|
+
async function resolvePythonVenvPath() {
|
|
4266
|
+
try {
|
|
4267
|
+
const { getPythonVenvPath: getPythonVenvPath2 } = await Promise.resolve().then(() => (init_python_setup(), python_setup_exports));
|
|
4268
|
+
return getPythonVenvPath2();
|
|
4269
|
+
} catch (err) {
|
|
4270
|
+
console.warn("[SKILLS] Could not resolve the Python venv for the session sandbox; skill scripts fall back to the system python.", err);
|
|
4271
|
+
return void 0;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
3998
4274
|
async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
3999
4275
|
const cached = sandboxCache.get(sessionId);
|
|
4000
4276
|
if (cached) {
|
|
@@ -4008,6 +4284,13 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
4008
4284
|
await downloadSkill(skill, skillsDirectory2, config);
|
|
4009
4285
|
cached.installedSkills.set(skill.id, skill.current_version);
|
|
4010
4286
|
}
|
|
4287
|
+
if (userId && config.fileUploads) {
|
|
4288
|
+
try {
|
|
4289
|
+
await restoreArtifactsFromS3(cached.handle.sessionDir, sessionId, userId, config, { onlyMissing: true });
|
|
4290
|
+
} catch (err) {
|
|
4291
|
+
console.error(`[SKILLS] Failed to re-sync S3 session files for session ${sessionId}; continuing.`, err);
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4011
4294
|
return cached.handle;
|
|
4012
4295
|
}
|
|
4013
4296
|
const sessionDir = (0, import_node_path4.join)("/tmp", "exulu-sessions", sessionId);
|
|
@@ -4025,8 +4308,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
4025
4308
|
`[SKILLS] S3 artifact persistence disabled for session ${sessionId} (userId=${userId ?? "missing"}, fileUploads=${config.fileUploads ? "configured" : "missing"})`
|
|
4026
4309
|
);
|
|
4027
4310
|
}
|
|
4028
|
-
if (userId && config.fileUploads
|
|
4029
|
-
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
|
|
4311
|
+
if (userId && config.fileUploads) {
|
|
4312
|
+
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config, { onlyMissing: dirExisted });
|
|
4030
4313
|
}
|
|
4031
4314
|
const probe = await probeSandboxSupport();
|
|
4032
4315
|
const useDirectExec = !probe.canSandbox;
|
|
@@ -4061,6 +4344,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4061
4344
|
await import_sandbox_runtime.SandboxManager.initialize(baselineSandboxConfig);
|
|
4062
4345
|
}
|
|
4063
4346
|
const npmGlobalRoot = await getNpmGlobalRoot();
|
|
4347
|
+
const pythonVenvPath = await resolvePythonVenvPath();
|
|
4064
4348
|
const sessionSandboxConfig = {
|
|
4065
4349
|
network: {
|
|
4066
4350
|
allowedDomains: [],
|
|
@@ -4075,7 +4359,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4075
4359
|
// Allow Node to read globally-installed packages from inside
|
|
4076
4360
|
// the sandbox. Without this, `require('docx')` fails with
|
|
4077
4361
|
// EPERM even when NODE_PATH points the resolver here.
|
|
4078
|
-
...npmGlobalRoot ? [npmGlobalRoot] : []
|
|
4362
|
+
...npmGlobalRoot ? [npmGlobalRoot] : [],
|
|
4363
|
+
...pythonVenvPath ? [pythonVenvPath] : []
|
|
4079
4364
|
],
|
|
4080
4365
|
allowWrite: [sessionDir],
|
|
4081
4366
|
denyWrite: []
|
|
@@ -4093,7 +4378,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4093
4378
|
const sandboxedExecEnv = {
|
|
4094
4379
|
...configuredVariables,
|
|
4095
4380
|
...process.env,
|
|
4096
|
-
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
|
|
4381
|
+
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {},
|
|
4382
|
+
...pythonVenvPath ? { PATH: `${(0, import_node_path4.join)(pythonVenvPath, "bin")}:${process.env.PATH ?? ""}`, VIRTUAL_ENV: pythonVenvPath } : {}
|
|
4097
4383
|
};
|
|
4098
4384
|
const wrapIfNeeded = async (command) => {
|
|
4099
4385
|
if (useDirectExec) return command;
|
|
@@ -4102,7 +4388,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4102
4388
|
const runWrapped = async (command) => {
|
|
4103
4389
|
const wrapped = await wrapIfNeeded(command);
|
|
4104
4390
|
try {
|
|
4105
|
-
const { stdout, stderr } = await
|
|
4391
|
+
const { stdout, stderr } = await execAsync3(wrapped, {
|
|
4106
4392
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
4107
4393
|
shell: "/bin/bash",
|
|
4108
4394
|
env: sandboxedExecEnv
|
|
@@ -4218,6 +4504,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4218
4504
|
for (const entry of entries) {
|
|
4219
4505
|
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
4220
4506
|
if (full === skillsDir) continue;
|
|
4507
|
+
if (isIgnoredArtifactPath((0, import_node_path4.relative)(sessionDir, full))) continue;
|
|
4221
4508
|
if (entry.isDirectory()) {
|
|
4222
4509
|
await walk(full);
|
|
4223
4510
|
} else if (entry.isFile()) {
|
|
@@ -4313,19 +4600,24 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
4313
4600
|
}
|
|
4314
4601
|
}
|
|
4315
4602
|
let stdout = result?.stdout ?? "";
|
|
4316
|
-
const
|
|
4603
|
+
const { kept, omitted } = capArtifacts(artifacts);
|
|
4604
|
+
const withUrls = kept.filter((a) => a.url);
|
|
4317
4605
|
if (withUrls.length > 0) {
|
|
4318
4606
|
const lines = ["", "[exulu-artifacts]"];
|
|
4319
4607
|
for (const a of withUrls) {
|
|
4320
4608
|
lines.push(` ${a.relativePath}: ${a.url}`);
|
|
4321
4609
|
}
|
|
4610
|
+
if (omitted > 0) {
|
|
4611
|
+
lines.push(` \u2026 ${omitted} more file(s) were created and mirrored but are not listed here.`);
|
|
4612
|
+
}
|
|
4322
4613
|
stdout = `${stdout}
|
|
4323
4614
|
${lines.join("\n")}`;
|
|
4324
4615
|
}
|
|
4325
4616
|
return {
|
|
4326
4617
|
...result,
|
|
4327
4618
|
stdout,
|
|
4328
|
-
artifacts
|
|
4619
|
+
artifacts: kept,
|
|
4620
|
+
...omitted > 0 ? { artifactsOmitted: omitted } : {}
|
|
4329
4621
|
};
|
|
4330
4622
|
}
|
|
4331
4623
|
});
|
|
@@ -4345,7 +4637,7 @@ ${lines.join("\n")}`;
|
|
|
4345
4637
|
sandboxCache.set(sessionId, { handle, installedSkills });
|
|
4346
4638
|
return handle;
|
|
4347
4639
|
}
|
|
4348
|
-
var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2, import_zod6, import_crypto_js3, getAllExuluVariables,
|
|
4640
|
+
var import_sandbox_runtime, import_promises, import_node_fs5, import_node_path4, import_node_child_process3, import_node_util2, import_bash_tool, import_ai2, import_zod6, import_crypto_js3, getAllExuluVariables, execAsync3, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
|
|
4349
4641
|
var init_create_sandbox = __esm({
|
|
4350
4642
|
"ee/invoke-skills/create-sandbox.ts"() {
|
|
4351
4643
|
"use strict";
|
|
@@ -4357,6 +4649,7 @@ var init_create_sandbox = __esm({
|
|
|
4357
4649
|
import_node_child_process3 = require("child_process");
|
|
4358
4650
|
import_node_util2 = require("util");
|
|
4359
4651
|
init_uppy();
|
|
4652
|
+
init_artifact_filter();
|
|
4360
4653
|
init_system_dependencies();
|
|
4361
4654
|
import_bash_tool = require("bash-tool");
|
|
4362
4655
|
import_ai2 = require("ai");
|
|
@@ -4390,7 +4683,7 @@ var init_create_sandbox = __esm({
|
|
|
4390
4683
|
}
|
|
4391
4684
|
return out;
|
|
4392
4685
|
};
|
|
4393
|
-
|
|
4686
|
+
execAsync3 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
|
|
4394
4687
|
EXEC_MAX_BUFFER = 32 * 1024 * 1024;
|
|
4395
4688
|
SANDBOX_FALLBACK_INSTRUCTIONS = 'Skill sandboxing is running in DEGRADED mode: bwrap cannot create user namespaces on this host, so commands\nexecute directly. The container remains the isolation boundary and resolveSessionPath still scopes\nreadFile/writeFile to the session directory at the JS layer, but bash commands are NOT kernel-sandboxed.\n\nTo restore full sandboxing on Ubuntu 23.10+ / 24.04+ hosts (kernel 6.5+):\n sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n echo "kernel.apparmor_restrict_unprivileged_userns=0" | sudo tee /etc/sysctl.d/60-userns.conf\n sudo sysctl --system\n\nOn Debian hosts where the same symptom appears:\n sudo sysctl -w kernel.unprivileged_userns_clone=1\n\nSet EXULU_REQUIRE_SANDBOX=1 to fail startup instead of degrading.';
|
|
4396
4689
|
degradedModeLogged = false;
|
|
@@ -4629,6 +4922,18 @@ var init_auth_tool_model_output = __esm({
|
|
|
4629
4922
|
}
|
|
4630
4923
|
});
|
|
4631
4924
|
|
|
4925
|
+
// src/exulu/session-files.ts
|
|
4926
|
+
function sessionFilePrefix(ownerId, sessionId, s3prefix) {
|
|
4927
|
+
const general = s3prefix ? `${s3prefix.replace(/\/+$/, "")}/` : "";
|
|
4928
|
+
return `${general}user_${ownerId}/sessions/${sessionId}/`;
|
|
4929
|
+
}
|
|
4930
|
+
var init_session_files = __esm({
|
|
4931
|
+
"src/exulu/session-files.ts"() {
|
|
4932
|
+
"use strict";
|
|
4933
|
+
init_cjs_shims();
|
|
4934
|
+
}
|
|
4935
|
+
});
|
|
4936
|
+
|
|
4632
4937
|
// src/templates/tools/session-file-read-tool.ts
|
|
4633
4938
|
var import_zod7, DEFAULT_LIMIT, MAX_CONTENT_CHARS, createSessionFileReadTool;
|
|
4634
4939
|
var init_session_file_read_tool = __esm({
|
|
@@ -4636,6 +4941,7 @@ var init_session_file_read_tool = __esm({
|
|
|
4636
4941
|
"use strict";
|
|
4637
4942
|
init_cjs_shims();
|
|
4638
4943
|
import_zod7 = require("zod");
|
|
4944
|
+
init_session_files();
|
|
4639
4945
|
init_tool();
|
|
4640
4946
|
init_uppy();
|
|
4641
4947
|
DEFAULT_LIMIT = 250;
|
|
@@ -4643,7 +4949,8 @@ var init_session_file_read_tool = __esm({
|
|
|
4643
4949
|
createSessionFileReadTool = ({
|
|
4644
4950
|
sessionID,
|
|
4645
4951
|
user,
|
|
4646
|
-
exuluConfig
|
|
4952
|
+
exuluConfig,
|
|
4953
|
+
ownerId
|
|
4647
4954
|
}) => {
|
|
4648
4955
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
4649
4956
|
const readSessionFileExecute = async ({ filename, offset, limit }) => {
|
|
@@ -4654,8 +4961,7 @@ var init_session_file_read_tool = __esm({
|
|
|
4654
4961
|
};
|
|
4655
4962
|
}
|
|
4656
4963
|
const uploads = exuluConfig.fileUploads;
|
|
4657
|
-
const
|
|
4658
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
4964
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
4659
4965
|
try {
|
|
4660
4966
|
const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
|
|
4661
4967
|
const res = await fetch(url);
|
|
@@ -4759,6 +5065,18 @@ var init_document_render_helpers = __esm({
|
|
|
4759
5065
|
});
|
|
4760
5066
|
|
|
4761
5067
|
// src/templates/tools/parse-document-tool.ts
|
|
5068
|
+
function looksLikeGarbledTextLayer(text) {
|
|
5069
|
+
let control = 0;
|
|
5070
|
+
let visible = 0;
|
|
5071
|
+
for (const ch of text) {
|
|
5072
|
+
const code = ch.charCodeAt(0);
|
|
5073
|
+
if (code === 9 || code === 10 || code === 12 || code === 13 || code === 32) continue;
|
|
5074
|
+
visible++;
|
|
5075
|
+
if (code < 32 || code === 127) control++;
|
|
5076
|
+
}
|
|
5077
|
+
if (visible < 40) return false;
|
|
5078
|
+
return control / visible > 0.01;
|
|
5079
|
+
}
|
|
4762
5080
|
var import_zod8, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, createParseDocumentTool;
|
|
4763
5081
|
var init_parse_document_tool = __esm({
|
|
4764
5082
|
"src/templates/tools/parse-document-tool.ts"() {
|
|
@@ -4770,6 +5088,7 @@ var init_parse_document_tool = __esm({
|
|
|
4770
5088
|
init_tool();
|
|
4771
5089
|
init_uppy();
|
|
4772
5090
|
init_document_render_helpers();
|
|
5091
|
+
init_session_files();
|
|
4773
5092
|
DEFAULT_LIMIT2 = 250;
|
|
4774
5093
|
MAX_CONTENT_CHARS2 = 16e3;
|
|
4775
5094
|
MIN_CHARS_PER_PAGE = 20;
|
|
@@ -4789,7 +5108,8 @@ var init_parse_document_tool = __esm({
|
|
|
4789
5108
|
createParseDocumentTool = ({
|
|
4790
5109
|
sessionID,
|
|
4791
5110
|
user,
|
|
4792
|
-
exuluConfig
|
|
5111
|
+
exuluConfig,
|
|
5112
|
+
ownerId
|
|
4793
5113
|
}) => {
|
|
4794
5114
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
4795
5115
|
const parseDocumentExecute = async ({
|
|
@@ -4814,8 +5134,7 @@ var init_parse_document_tool = __esm({
|
|
|
4814
5134
|
return { error: `The pages option is only supported for PDF files \u2014 "${ext}" documents are extracted whole.` };
|
|
4815
5135
|
}
|
|
4816
5136
|
const uploads = exuluConfig.fileUploads;
|
|
4817
|
-
const
|
|
4818
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
5137
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
4819
5138
|
try {
|
|
4820
5139
|
const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
|
|
4821
5140
|
const res = await fetch(url);
|
|
@@ -4830,6 +5149,11 @@ var init_parse_document_tool = __esm({
|
|
|
4830
5149
|
const pageTexts = raw.replace(/\f$/, "").split("\f");
|
|
4831
5150
|
totalPages = pageTexts.length;
|
|
4832
5151
|
const nonWhitespace = raw.replace(/\s/g, "").length;
|
|
5152
|
+
if (looksLikeGarbledTextLayer(raw)) {
|
|
5153
|
+
return {
|
|
5154
|
+
error: `"${safeName}" has a text layer that is unreadable (its font encoding maps glyphs to the wrong characters, so words and especially numbers come out wrong or vanish). Do not use extracted text from this file. Use view_document_page to read the pages visually, or suggest the user add the document to a knowledge base with a document processor for full OCR.`
|
|
5155
|
+
};
|
|
5156
|
+
}
|
|
4833
5157
|
if (nonWhitespace < totalPages * MIN_CHARS_PER_PAGE) {
|
|
4834
5158
|
return {
|
|
4835
5159
|
error: `"${safeName}" has no extractable text layer (likely a scan or image-based PDF). Use view_document_page to look at pages visually, or suggest the user add the document to a knowledge base with a document processor for full OCR.`
|
|
@@ -4928,7 +5252,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
4928
5252
|
const bytes = await getS3ObjectBytes(sourceKey, config);
|
|
4929
5253
|
await (0, import_promises3.writeFile)(inputPath, bytes);
|
|
4930
5254
|
try {
|
|
4931
|
-
await
|
|
5255
|
+
await execAsync4(
|
|
4932
5256
|
`soffice --headless --convert-to pdf "${inputPath}" --outdir "${CACHE_OUT}"`,
|
|
4933
5257
|
{ timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }
|
|
4934
5258
|
);
|
|
@@ -4954,7 +5278,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
4954
5278
|
inFlight.set(safeEtag, promise);
|
|
4955
5279
|
return promise;
|
|
4956
5280
|
}
|
|
4957
|
-
var import_node_child_process5, import_node_fs6, import_promises3, import_node_path7, import_node_util4,
|
|
5281
|
+
var import_node_child_process5, import_node_fs6, import_promises3, import_node_path7, import_node_util4, execAsync4, CACHE_ROOT, CACHE_IN, CACHE_OUT, inFlight, PreviewRenderError;
|
|
4958
5282
|
var init_pdf_preview_cache = __esm({
|
|
4959
5283
|
"src/sessions/pdf-preview-cache.ts"() {
|
|
4960
5284
|
"use strict";
|
|
@@ -4965,7 +5289,7 @@ var init_pdf_preview_cache = __esm({
|
|
|
4965
5289
|
import_node_path7 = require("path");
|
|
4966
5290
|
import_node_util4 = require("util");
|
|
4967
5291
|
init_uppy();
|
|
4968
|
-
|
|
5292
|
+
execAsync4 = (0, import_node_util4.promisify)(import_node_child_process5.exec);
|
|
4969
5293
|
CACHE_ROOT = "/tmp/exulu-pdf-cache";
|
|
4970
5294
|
CACHE_IN = (0, import_node_path7.join)(CACHE_ROOT, "_in");
|
|
4971
5295
|
CACHE_OUT = (0, import_node_path7.join)(CACHE_ROOT, "_out");
|
|
@@ -5165,6 +5489,7 @@ var init_view_document_page_tool = __esm({
|
|
|
5165
5489
|
"use strict";
|
|
5166
5490
|
init_cjs_shims();
|
|
5167
5491
|
import_zod9 = require("zod");
|
|
5492
|
+
init_session_files();
|
|
5168
5493
|
import_node_path8 = require("path");
|
|
5169
5494
|
init_tool();
|
|
5170
5495
|
init_uppy();
|
|
@@ -5197,7 +5522,8 @@ var init_view_document_page_tool = __esm({
|
|
|
5197
5522
|
createViewDocumentPageTool = ({
|
|
5198
5523
|
sessionID,
|
|
5199
5524
|
user,
|
|
5200
|
-
exuluConfig
|
|
5525
|
+
exuluConfig,
|
|
5526
|
+
ownerId
|
|
5201
5527
|
}) => {
|
|
5202
5528
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
5203
5529
|
const viewDocumentPageExecute = async ({ filename, page, model }, options) => {
|
|
@@ -5230,8 +5556,7 @@ var init_view_document_page_tool = __esm({
|
|
|
5230
5556
|
}
|
|
5231
5557
|
}
|
|
5232
5558
|
const uploads = exuluConfig.fileUploads;
|
|
5233
|
-
const
|
|
5234
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
5559
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
5235
5560
|
const pageNumber = page ?? 1;
|
|
5236
5561
|
try {
|
|
5237
5562
|
let imageBytes;
|
|
@@ -5305,13 +5630,13 @@ var init_view_document_page_tool = __esm({
|
|
|
5305
5630
|
});
|
|
5306
5631
|
|
|
5307
5632
|
// src/exulu/audit/config.ts
|
|
5308
|
-
var import_os,
|
|
5633
|
+
var import_os, import_path2, normalizePrefix, hasAllS3Fields, resolveAuditConfig;
|
|
5309
5634
|
var init_config = __esm({
|
|
5310
5635
|
"src/exulu/audit/config.ts"() {
|
|
5311
5636
|
"use strict";
|
|
5312
5637
|
init_cjs_shims();
|
|
5313
5638
|
import_os = __toESM(require("os"), 1);
|
|
5314
|
-
|
|
5639
|
+
import_path2 = __toESM(require("path"), 1);
|
|
5315
5640
|
normalizePrefix = (p) => {
|
|
5316
5641
|
const raw = (p ?? "audit").trim().replace(/^\/+|\/+$/g, "");
|
|
5317
5642
|
return `${raw || "audit"}/`;
|
|
@@ -5343,7 +5668,7 @@ var init_config = __esm({
|
|
|
5343
5668
|
retentionDays: a.retentionDays,
|
|
5344
5669
|
manageLifecycle: a.manageLifecycle ?? !usingSharedFileUploadsBucket,
|
|
5345
5670
|
usingSharedFileUploadsBucket,
|
|
5346
|
-
spoolDir: a.spoolDir ??
|
|
5671
|
+
spoolDir: a.spoolDir ?? import_path2.default.join(import_os.default.tmpdir(), "exulu-audit-spool"),
|
|
5347
5672
|
flush: {
|
|
5348
5673
|
maxRecords: a.flush?.maxRecords ?? 100,
|
|
5349
5674
|
maxIntervalMs: a.flush?.maxIntervalMs ?? 5e3
|
|
@@ -5464,29 +5789,29 @@ ${JSON.stringify(config, null, 2)}`
|
|
|
5464
5789
|
});
|
|
5465
5790
|
|
|
5466
5791
|
// src/exulu/audit/sink.ts
|
|
5467
|
-
var import_crypto2,
|
|
5792
|
+
var import_crypto2, import_fs2, import_path3, createFsSpoolStore, pad, AuditSink;
|
|
5468
5793
|
var init_sink = __esm({
|
|
5469
5794
|
"src/exulu/audit/sink.ts"() {
|
|
5470
5795
|
"use strict";
|
|
5471
5796
|
init_cjs_shims();
|
|
5472
5797
|
import_crypto2 = require("crypto");
|
|
5473
|
-
|
|
5474
|
-
|
|
5798
|
+
import_fs2 = require("fs");
|
|
5799
|
+
import_path3 = __toESM(require("path"), 1);
|
|
5475
5800
|
createFsSpoolStore = (dir) => ({
|
|
5476
5801
|
write: async (name, body) => {
|
|
5477
|
-
await
|
|
5478
|
-
await
|
|
5802
|
+
await import_fs2.promises.mkdir(dir, { recursive: true });
|
|
5803
|
+
await import_fs2.promises.writeFile(import_path3.default.join(dir, name), body, "utf8");
|
|
5479
5804
|
},
|
|
5480
5805
|
list: async () => {
|
|
5481
5806
|
try {
|
|
5482
|
-
return (await
|
|
5807
|
+
return (await import_fs2.promises.readdir(dir)).filter((f) => f.endsWith(".ndjson"));
|
|
5483
5808
|
} catch {
|
|
5484
5809
|
return [];
|
|
5485
5810
|
}
|
|
5486
5811
|
},
|
|
5487
|
-
read: async (name) =>
|
|
5812
|
+
read: async (name) => import_fs2.promises.readFile(import_path3.default.join(dir, name), "utf8"),
|
|
5488
5813
|
remove: async (name) => {
|
|
5489
|
-
await
|
|
5814
|
+
await import_fs2.promises.rm(import_path3.default.join(dir, name), { force: true });
|
|
5490
5815
|
}
|
|
5491
5816
|
});
|
|
5492
5817
|
pad = (n) => String(n).padStart(2, "0");
|
|
@@ -6032,7 +6357,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6032
6357
|
await Promise.all(promises2);
|
|
6033
6358
|
return tool4;
|
|
6034
6359
|
};
|
|
6035
|
-
convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
|
|
6360
|
+
convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools, sessionOwnerId) => {
|
|
6036
6361
|
if (!currentTools) return {};
|
|
6037
6362
|
if (!allExuluTools) {
|
|
6038
6363
|
allExuluTools = [];
|
|
@@ -6049,7 +6374,7 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6049
6374
|
sessionID,
|
|
6050
6375
|
currentSkills || [],
|
|
6051
6376
|
exuluConfig,
|
|
6052
|
-
user?.id
|
|
6377
|
+
sessionOwnerId ?? user?.id
|
|
6053
6378
|
);
|
|
6054
6379
|
} catch (err) {
|
|
6055
6380
|
console.error(
|
|
@@ -6114,15 +6439,15 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6114
6439
|
currentTools.push(sessionItemsRetrievalTool);
|
|
6115
6440
|
}
|
|
6116
6441
|
}
|
|
6117
|
-
const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig });
|
|
6442
|
+
const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
6118
6443
|
if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
|
|
6119
6444
|
currentTools.push(sessionFileReadTool);
|
|
6120
6445
|
}
|
|
6121
|
-
const parseDocumentTool = createParseDocumentTool({ sessionID, user, exuluConfig });
|
|
6446
|
+
const parseDocumentTool = createParseDocumentTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
6122
6447
|
if (parseDocumentTool && !disabled.has(parseDocumentTool.id)) {
|
|
6123
6448
|
currentTools.push(parseDocumentTool);
|
|
6124
6449
|
}
|
|
6125
|
-
const viewDocumentPageTool = createViewDocumentPageTool({ sessionID, user, exuluConfig });
|
|
6450
|
+
const viewDocumentPageTool = createViewDocumentPageTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
6126
6451
|
if (viewDocumentPageTool && !disabled.has(viewDocumentPageTool.id)) {
|
|
6127
6452
|
currentTools.push(viewDocumentPageTool);
|
|
6128
6453
|
}
|
|
@@ -6826,7 +7151,19 @@ var init_config2 = __esm({
|
|
|
6826
7151
|
pinBoost: import_zod11.z.number().min(0).max(1).default(0.15),
|
|
6827
7152
|
identifierBoost: import_zod11.z.number().min(0).max(1).default(0.15),
|
|
6828
7153
|
pageWindow: import_zod11.z.number().int().min(0).default(1),
|
|
6829
|
-
maxQueriesPerContext: import_zod11.z.number().int().positive().default(5)
|
|
7154
|
+
maxQueriesPerContext: import_zod11.z.number().int().positive().default(5),
|
|
7155
|
+
/**
|
|
7156
|
+
* Orchestration engine. "v1" is the sequential flow every agent ran before 2026-09;
|
|
7157
|
+
* "v2" merges the phase-1 LLM hops and runs identifier pins in parallel. Per agent,
|
|
7158
|
+
* so a candidate agent can run v2 while the production agent stays on v1.
|
|
7159
|
+
*/
|
|
7160
|
+
engine: import_zod11.z.enum(["v1", "v2"]).default("v1"),
|
|
7161
|
+
/** v2 sub-features; each can be switched off on its own to bisect a regression. */
|
|
7162
|
+
v2: import_zod11.z.object({
|
|
7163
|
+
mergedMemoryCall: import_zod11.z.boolean().default(true),
|
|
7164
|
+
mergedRoutingCall: import_zod11.z.boolean().default(true),
|
|
7165
|
+
parallelPins: import_zod11.z.boolean().default(true)
|
|
7166
|
+
}).default({ mergedMemoryCall: true, mergedRoutingCall: true, parallelPins: true })
|
|
6830
7167
|
});
|
|
6831
7168
|
boolVal = (v) => v === true || v === "true" || v === 1;
|
|
6832
7169
|
strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
|
|
@@ -6921,6 +7258,23 @@ var init_micro_call = __esm({
|
|
|
6921
7258
|
}
|
|
6922
7259
|
});
|
|
6923
7260
|
|
|
7261
|
+
// ee/agentic-retrieval/pipeline/timing.ts
|
|
7262
|
+
async function withTiming(sink, key, work, now = Date.now) {
|
|
7263
|
+
if (!sink) return work();
|
|
7264
|
+
const started = now();
|
|
7265
|
+
try {
|
|
7266
|
+
return await work();
|
|
7267
|
+
} finally {
|
|
7268
|
+
sink[key] = Math.max(0, now() - started);
|
|
7269
|
+
}
|
|
7270
|
+
}
|
|
7271
|
+
var init_timing = __esm({
|
|
7272
|
+
"ee/agentic-retrieval/pipeline/timing.ts"() {
|
|
7273
|
+
"use strict";
|
|
7274
|
+
init_cjs_shims();
|
|
7275
|
+
}
|
|
7276
|
+
});
|
|
7277
|
+
|
|
6924
7278
|
// ee/agentic-retrieval/pipeline/text-utils.ts
|
|
6925
7279
|
function extractIdentifierTokens(parts) {
|
|
6926
7280
|
const tokens = /* @__PURE__ */ new Set();
|
|
@@ -7228,6 +7582,49 @@ If the question references none, return hasMatches false and an empty array.`;
|
|
|
7228
7582
|
});
|
|
7229
7583
|
|
|
7230
7584
|
// ee/agentic-retrieval/pipeline/routing.ts
|
|
7585
|
+
async function runMergedRoutingCall({
|
|
7586
|
+
model,
|
|
7587
|
+
question,
|
|
7588
|
+
knownIdentifiers,
|
|
7589
|
+
enabledContexts,
|
|
7590
|
+
routingRules,
|
|
7591
|
+
extraInstructions
|
|
7592
|
+
}) {
|
|
7593
|
+
const kbListing = enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n");
|
|
7594
|
+
const rulesLines = routingRules.map((r) => `- ${r.id} (${r.label}): ${r.description}`).join("\n");
|
|
7595
|
+
const system = `You analyse the user's request and answer ${routingRules.length ? "three" : "two"} questions in one go.
|
|
7596
|
+
|
|
7597
|
+
A. DOCUMENT / PAGE REFERENCE (docPage):
|
|
7598
|
+
${buildDocPagePrompt(knownIdentifiers)}
|
|
7599
|
+
|
|
7600
|
+
B. EXPLICIT KNOWLEDGE BASE REQUEST (explicitlyRequestedKnowledgeBases): check if the user has EXPLICITLY asked you to search in one or multiple of the following knowledge bases:
|
|
7601
|
+
${kbListing}
|
|
7602
|
+
EXPLICIT means the user names a knowledge base or clearly commands searching a specific source (e.g. "search in the tickets", "look this up in the manuals KB"). A question that merely CONCERNS a topic related to a knowledge base's name or contents (e.g. asking about software changes, norms, or a product) is NOT an explicit request. When in doubt, return an empty array. If explicit, return the knowledge base ids.
|
|
7603
|
+
` + (routingRules.length ? `
|
|
7604
|
+
C. CLASSIFICATION (classification): classify the request into exactly one of these categories:
|
|
7605
|
+
${rulesLines}` + (extraInstructions ? `
|
|
7606
|
+
<instructions>
|
|
7607
|
+
${extraInstructions}
|
|
7608
|
+
</instructions>` : "") : "");
|
|
7609
|
+
const ids = enabledContexts.map((c) => c.id);
|
|
7610
|
+
const schema = import_zod13.z.object({
|
|
7611
|
+
docPage: import_zod13.z.object({
|
|
7612
|
+
hasFilenameHint: import_zod13.z.boolean(),
|
|
7613
|
+
filenameHints: import_zod13.z.array(import_zod13.z.string()).optional(),
|
|
7614
|
+
hasPageHint: import_zod13.z.boolean(),
|
|
7615
|
+
pageNumber: import_zod13.z.number().int().nullable().optional()
|
|
7616
|
+
}),
|
|
7617
|
+
explicitlyRequestedKnowledgeBases: import_zod13.z.array(import_zod13.z.enum(ids)),
|
|
7618
|
+
...routingRules.length ? {
|
|
7619
|
+
classification: import_zod13.z.object({
|
|
7620
|
+
ruleId: import_zod13.z.enum(routingRules.map((r) => r.id)),
|
|
7621
|
+
reason: import_zod13.z.string()
|
|
7622
|
+
})
|
|
7623
|
+
} : {}
|
|
7624
|
+
});
|
|
7625
|
+
const { output } = await microCall({ model, system, messages: [{ role: "user", content: question }], schema });
|
|
7626
|
+
return output;
|
|
7627
|
+
}
|
|
7231
7628
|
async function runRoutingPhase(opts) {
|
|
7232
7629
|
const {
|
|
7233
7630
|
question,
|
|
@@ -7237,6 +7634,7 @@ async function runRoutingPhase(opts) {
|
|
|
7237
7634
|
preselectedItems,
|
|
7238
7635
|
extraInstructions,
|
|
7239
7636
|
knownIdentifiers = [],
|
|
7637
|
+
mergedCall = false,
|
|
7240
7638
|
model
|
|
7241
7639
|
} = opts;
|
|
7242
7640
|
try {
|
|
@@ -7256,7 +7654,23 @@ async function runRoutingPhase(opts) {
|
|
|
7256
7654
|
` + enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n") + `
|
|
7257
7655
|
EXPLICIT means the user names a knowledge base or clearly commands searching a specific source (e.g. "search in the tickets", "look this up in the manuals KB"). A question that merely CONCERNS a topic related to a knowledge base's name or contents (e.g. asking about software changes, norms, or a product) is NOT an explicit request \u2014 classification routing handles those. When in doubt, return an empty array.
|
|
7258
7656
|
If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
7259
|
-
const
|
|
7657
|
+
const wantsClassification = routingRules.length > 0 && preselectedItems.size === 0;
|
|
7658
|
+
const merged = mergedCall ? await withTiming(
|
|
7659
|
+
opts.timings,
|
|
7660
|
+
"routing.mergedMs",
|
|
7661
|
+
() => runMergedRoutingCall({
|
|
7662
|
+
model,
|
|
7663
|
+
question,
|
|
7664
|
+
knownIdentifiers,
|
|
7665
|
+
enabledContexts,
|
|
7666
|
+
routingRules: wantsClassification ? routingRules : [],
|
|
7667
|
+
extraInstructions
|
|
7668
|
+
}).catch((err) => {
|
|
7669
|
+
console.warn("[EXULU pipeline] merged routing call failed \u2014 falling back to the v1 hops.", err);
|
|
7670
|
+
return null;
|
|
7671
|
+
})
|
|
7672
|
+
) : null;
|
|
7673
|
+
const [docPageRaw, explicitKBRaw] = merged ? [{ output: merged.docPage }, { output: { explicitlyRequestedKnowledgeBases: merged.explicitlyRequestedKnowledgeBases } }] : await withTiming(opts.timings, "routing.detectMs", () => Promise.all([
|
|
7260
7674
|
(async () => {
|
|
7261
7675
|
try {
|
|
7262
7676
|
return await microCall({
|
|
@@ -7298,7 +7712,7 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
7298
7712
|
return { output: { explicitlyRequestedKnowledgeBases: [] } };
|
|
7299
7713
|
}
|
|
7300
7714
|
})()
|
|
7301
|
-
]);
|
|
7715
|
+
]));
|
|
7302
7716
|
const userPinnedItemIdsByContext = /* @__PURE__ */ new Map();
|
|
7303
7717
|
let userRequestedPage = null;
|
|
7304
7718
|
if (docPageRaw.output.hasFilenameHint && docPageRaw.output.filenameHints?.length) {
|
|
@@ -7376,7 +7790,7 @@ ${extraInstructions}
|
|
|
7376
7790
|
</instructions>`;
|
|
7377
7791
|
}
|
|
7378
7792
|
try {
|
|
7379
|
-
const { output: classified } = await microCall({
|
|
7793
|
+
const { output: classified } = merged?.classification ? { output: merged.classification } : await withTiming(opts.timings, "routing.classifyMs", () => microCall({
|
|
7380
7794
|
model,
|
|
7381
7795
|
system: classifyPrompt,
|
|
7382
7796
|
messages: [{ role: "user", content: question }],
|
|
@@ -7384,7 +7798,7 @@ ${extraInstructions}
|
|
|
7384
7798
|
ruleId: import_zod13.z.enum(ruleIds),
|
|
7385
7799
|
reason: import_zod13.z.string()
|
|
7386
7800
|
})
|
|
7387
|
-
});
|
|
7801
|
+
}));
|
|
7388
7802
|
const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
|
|
7389
7803
|
if (matchedRule) {
|
|
7390
7804
|
const main = matchedRule.main.filter((id) => enabledIds.has(id));
|
|
@@ -7444,6 +7858,7 @@ var init_routing = __esm({
|
|
|
7444
7858
|
init_cjs_shims();
|
|
7445
7859
|
import_zod13 = require("zod");
|
|
7446
7860
|
init_micro_call();
|
|
7861
|
+
init_timing();
|
|
7447
7862
|
init_prefilter();
|
|
7448
7863
|
init_text_utils();
|
|
7449
7864
|
MAX_USER_PIN_MATCHES = 8;
|
|
@@ -7585,7 +8000,8 @@ async function recallMemoryByKeywords({
|
|
|
7585
8000
|
importantKeyword,
|
|
7586
8001
|
user,
|
|
7587
8002
|
role,
|
|
7588
|
-
memoryContext
|
|
8003
|
+
memoryContext,
|
|
8004
|
+
timings
|
|
7589
8005
|
}) {
|
|
7590
8006
|
const allKeywords = [
|
|
7591
8007
|
...new Set(
|
|
@@ -7602,7 +8018,7 @@ async function recallMemoryByKeywords({
|
|
|
7602
8018
|
...new Set(allKeywords.flatMap(deriveKeywordVariants).map(stripSeparators))
|
|
7603
8019
|
].filter((v) => v.length >= 4);
|
|
7604
8020
|
if (!allVariants.length) return [];
|
|
7605
|
-
const items = await loadMemoryItems(memoryContext);
|
|
8021
|
+
const items = await withTiming(timings, "memory.keywordRecall.itemsMs", () => loadMemoryItems(memoryContext));
|
|
7606
8022
|
const scored = [];
|
|
7607
8023
|
for (const item of items) {
|
|
7608
8024
|
const haystack = stripSeparators(
|
|
@@ -7624,14 +8040,14 @@ async function recallMemoryByKeywords({
|
|
|
7624
8040
|
"[EXULU pipeline] keyword-triggered memory matches:",
|
|
7625
8041
|
topMatches.map((s) => `${s.name} (hits=${s.hits}, important=${s.importantHit})`)
|
|
7626
8042
|
);
|
|
7627
|
-
const chunks = await singleSearch({
|
|
8043
|
+
const chunks = await withTiming(timings, "memory.keywordRecall.searchMs", () => singleSearch({
|
|
7628
8044
|
query: allKeywords.join(", "),
|
|
7629
|
-
config: { method: "
|
|
8045
|
+
config: { method: "tsvector", cutoffs: void 0, limit: 50 },
|
|
7630
8046
|
user,
|
|
7631
8047
|
role,
|
|
7632
8048
|
pinnedItemIds: topMatches.map((s) => s.id),
|
|
7633
8049
|
context: memoryContext
|
|
7634
|
-
});
|
|
8050
|
+
}));
|
|
7635
8051
|
return chunks;
|
|
7636
8052
|
}
|
|
7637
8053
|
function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
@@ -7645,7 +8061,103 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
|
7645
8061
|
steps
|
|
7646
8062
|
};
|
|
7647
8063
|
}
|
|
8064
|
+
async function runMergedMemoryCall({
|
|
8065
|
+
model,
|
|
8066
|
+
retrievedMemory,
|
|
8067
|
+
question,
|
|
8068
|
+
keywords,
|
|
8069
|
+
importantKeyword,
|
|
8070
|
+
memoryConfig,
|
|
8071
|
+
glossary
|
|
8072
|
+
}) {
|
|
8073
|
+
const glossaryText = glossary.length > 0 ? `
|
|
8074
|
+
The organization's documents use the following abbreviations/terms:
|
|
8075
|
+
${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
8076
|
+
const system = `
|
|
8077
|
+
You review the shared company memory for the user's question and answer FOUR questions in one go.
|
|
8078
|
+
|
|
8079
|
+
1. RELEVANCE (relevantChunkIds): return the chunk_ids of chunks containing information relevant to the
|
|
8080
|
+
question, or an empty array. Be generous: include chunks that are topically related, share key
|
|
8081
|
+
terminology, describe the same symptom from a different angle, or could plausibly help diagnose the
|
|
8082
|
+
issue \u2014 even if they don't answer the question directly. Memory entries are deliberately broad,
|
|
8083
|
+
hand-curated hints written by domain experts; the user's wording will rarely match the memory verbatim.
|
|
8084
|
+
When in doubt, include the chunk.
|
|
8085
|
+
|
|
8086
|
+
2. OVERRIDE (override): decide whether ONE of the relevant chunks should become the AUTHORITATIVE basis
|
|
8087
|
+
of the answer, taking precedence over the official documentation even if the documents state something
|
|
8088
|
+
different. This is a deliberately STRICT check. Set overrides=true ONLY if a single chunk, on its own,
|
|
8089
|
+
contains a DIRECT and SUFFICIENT answer to exactly what the user asked. Being topically related, sharing
|
|
8090
|
+
terminology, describing the same component, or only partially addressing the question is NOT sufficient:
|
|
8091
|
+
then set overrides=false. When in doubt, set overrides=false. Memory entries may capture field experience
|
|
8092
|
+
that the manuals get wrong, so a confident, direct match is meant to win over the documents.
|
|
8093
|
+
${memoryConfig.override ? "" : "(Override is disabled for this agent: return overrides=false.)"}
|
|
8094
|
+
|
|
8095
|
+
3. FILE PRIORITIZATION (filePrioritization): set shouldPrioritizeFiles=true only if a relevant memory
|
|
8096
|
+
entry explicitly says to look in, prioritize, prefer, or always search a particular document, file, or
|
|
8097
|
+
file family (for example "When asked about X, always search in Y-Dateien first"). General background
|
|
8098
|
+
facts, glossaries, or synonyms are NOT a file prioritization instruction. When true, return
|
|
8099
|
+
fileNameHints exactly as referenced in the memory, bare names without folder paths.
|
|
8100
|
+
${memoryConfig.filePrioritization ? "" : "(File prioritization is disabled for this agent: return false.)"}
|
|
8101
|
+
|
|
8102
|
+
4. QUERY AUGMENTATION (augmentation): if, and only if, the relevant memory (or the glossary below)
|
|
8103
|
+
contains synonyms or similar terms for what the user asked, return the user question and keywords
|
|
8104
|
+
updated to include those synonyms \u2014 always keeping the original wording as well. Otherwise return the
|
|
8105
|
+
original question, keywords and important keyword unchanged.
|
|
8106
|
+
${memoryConfig.queryAugmentation ? "" : "(Query augmentation is disabled for this agent: return the originals.)"}
|
|
8107
|
+
|
|
8108
|
+
<memory_chunks>
|
|
8109
|
+
${retrievedMemory.map((chunk) => `- ${chunk.chunk_id}: ${chunk.item_name} - ${chunk.chunk_content}`).join("\n")}
|
|
8110
|
+
</memory_chunks>
|
|
8111
|
+
${glossaryText}
|
|
8112
|
+
`;
|
|
8113
|
+
const { output } = await microCall({
|
|
8114
|
+
model,
|
|
8115
|
+
system,
|
|
8116
|
+
messages: [
|
|
8117
|
+
{
|
|
8118
|
+
role: "user",
|
|
8119
|
+
content: `
|
|
8120
|
+
<user_question>${question}</user_question>
|
|
8121
|
+
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
8122
|
+
<important_keyword>${importantKeyword}</important_keyword>
|
|
8123
|
+
`
|
|
8124
|
+
}
|
|
8125
|
+
],
|
|
8126
|
+
schema: import_zod14.z.object({
|
|
8127
|
+
relevantChunkIds: import_zod14.z.array(import_zod14.z.string()).describe("chunk_ids (UUIDs at the start of each bullet) of relevant chunks; empty array if none."),
|
|
8128
|
+
override: import_zod14.z.object({
|
|
8129
|
+
overrides: import_zod14.z.boolean().describe("True ONLY if a chunk directly and sufficiently answers the question."),
|
|
8130
|
+
confidence: import_zod14.z.enum(["high", "medium", "low"]),
|
|
8131
|
+
authoritativeChunkIds: import_zod14.z.array(import_zod14.z.string()).describe("chunk_ids that directly answer the question; empty if overrides is false."),
|
|
8132
|
+
reason: import_zod14.z.string().describe("One short sentence.")
|
|
8133
|
+
}),
|
|
8134
|
+
filePrioritization: import_zod14.z.object({
|
|
8135
|
+
shouldPrioritizeFiles: import_zod14.z.boolean(),
|
|
8136
|
+
fileNameHints: import_zod14.z.array(import_zod14.z.string()).optional()
|
|
8137
|
+
}),
|
|
8138
|
+
augmentation: import_zod14.z.object({
|
|
8139
|
+
updatedUserQuestion: import_zod14.z.string(),
|
|
8140
|
+
updatedRelevantKeywords: import_zod14.z.array(import_zod14.z.string()),
|
|
8141
|
+
updatedImportantKeyword: import_zod14.z.string()
|
|
8142
|
+
})
|
|
8143
|
+
})
|
|
8144
|
+
});
|
|
8145
|
+
return output;
|
|
8146
|
+
}
|
|
8147
|
+
function mergedFollowups(merged, memoryConfig, hasAugmentationContent, question, importantKeyword) {
|
|
8148
|
+
const overrideResult = {
|
|
8149
|
+
output: memoryConfig.override ? merged.override : { overrides: false, confidence: "low", authoritativeChunkIds: [], reason: "" }
|
|
8150
|
+
};
|
|
8151
|
+
const fileResult = {
|
|
8152
|
+
output: memoryConfig.filePrioritization ? merged.filePrioritization : { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
8153
|
+
};
|
|
8154
|
+
const queryResult = {
|
|
8155
|
+
output: memoryConfig.queryAugmentation && hasAugmentationContent ? merged.augmentation : { updatedUserQuestion: question, updatedRelevantKeywords: [], updatedImportantKeyword: importantKeyword }
|
|
8156
|
+
};
|
|
8157
|
+
return [overrideResult, fileResult, queryResult];
|
|
8158
|
+
}
|
|
7648
8159
|
async function runMemoryPhase({
|
|
8160
|
+
timings,
|
|
7649
8161
|
memoryChunks,
|
|
7650
8162
|
memoryContext,
|
|
7651
8163
|
question,
|
|
@@ -7656,7 +8168,8 @@ async function runMemoryPhase({
|
|
|
7656
8168
|
model,
|
|
7657
8169
|
memoryConfig,
|
|
7658
8170
|
glossary,
|
|
7659
|
-
documentContexts
|
|
8171
|
+
documentContexts,
|
|
8172
|
+
mergedCall = false
|
|
7660
8173
|
}) {
|
|
7661
8174
|
try {
|
|
7662
8175
|
if (!memoryConfig.enabled || memoryChunks.length === 0 && !memoryContext) {
|
|
@@ -7666,13 +8179,14 @@ async function runMemoryPhase({
|
|
|
7666
8179
|
let retrieved_memory = [...memoryChunks];
|
|
7667
8180
|
if (memoryContext) {
|
|
7668
8181
|
try {
|
|
7669
|
-
const keywordMatched = await recallMemoryByKeywords({
|
|
8182
|
+
const keywordMatched = await withTiming(timings, "memory.keywordRecallMs", () => recallMemoryByKeywords({
|
|
7670
8183
|
keywords,
|
|
7671
8184
|
importantKeyword,
|
|
7672
8185
|
user,
|
|
7673
8186
|
role,
|
|
7674
|
-
memoryContext
|
|
7675
|
-
|
|
8187
|
+
memoryContext,
|
|
8188
|
+
timings
|
|
8189
|
+
}));
|
|
7676
8190
|
if (keywordMatched.length > 0) {
|
|
7677
8191
|
const seen = new Set(retrieved_memory.map((c) => c.chunk_id));
|
|
7678
8192
|
const additions = keywordMatched.filter((c) => !seen.has(c.chunk_id));
|
|
@@ -7697,8 +8211,24 @@ async function runMemoryPhase({
|
|
|
7697
8211
|
</memory_chunks>
|
|
7698
8212
|
`;
|
|
7699
8213
|
let relevantMemoryChunks = [];
|
|
8214
|
+
let mergedOutput;
|
|
7700
8215
|
try {
|
|
7701
|
-
|
|
8216
|
+
if (mergedCall) {
|
|
8217
|
+
mergedOutput = await withTiming(
|
|
8218
|
+
timings,
|
|
8219
|
+
"memory.mergedMs",
|
|
8220
|
+
() => runMergedMemoryCall({
|
|
8221
|
+
model,
|
|
8222
|
+
retrievedMemory: retrieved_memory,
|
|
8223
|
+
question,
|
|
8224
|
+
keywords,
|
|
8225
|
+
importantKeyword,
|
|
8226
|
+
memoryConfig,
|
|
8227
|
+
glossary
|
|
8228
|
+
})
|
|
8229
|
+
);
|
|
8230
|
+
}
|
|
8231
|
+
const { output: output_relevant_memory } = mergedOutput ? { output: { relevantChunkIds: mergedOutput.relevantChunkIds } } : await withTiming(timings, "memory.relevanceMs", () => microCall({
|
|
7702
8232
|
model,
|
|
7703
8233
|
system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
|
|
7704
8234
|
messages: [
|
|
@@ -7716,7 +8246,7 @@ async function runMemoryPhase({
|
|
|
7716
8246
|
"The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
|
|
7717
8247
|
)
|
|
7718
8248
|
})
|
|
7719
|
-
});
|
|
8249
|
+
}));
|
|
7720
8250
|
const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
|
|
7721
8251
|
relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
|
|
7722
8252
|
} catch (e) {
|
|
@@ -7806,7 +8336,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
7806
8336
|
|
|
7807
8337
|
Otherwise, return the original user question, relevant keywords and important keyword.
|
|
7808
8338
|
`;
|
|
7809
|
-
const [overrideResult, fileResult, queryResult] = await Promise.all([
|
|
8339
|
+
const [overrideResult, fileResult, queryResult] = mergedOutput ? mergedFollowups(mergedOutput, memoryConfig, hasAugmentationContent, question, importantKeyword) : await withTiming(timings, "memory.followupsMs", () => Promise.all([
|
|
7810
8340
|
// Override check: strict gate to decide if memory should be authoritative
|
|
7811
8341
|
memoryConfig.override ? microCall({
|
|
7812
8342
|
model,
|
|
@@ -7887,7 +8417,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
7887
8417
|
updatedImportantKeyword: importantKeyword
|
|
7888
8418
|
}
|
|
7889
8419
|
})
|
|
7890
|
-
]);
|
|
8420
|
+
]));
|
|
7891
8421
|
const overrideIds = new Set(overrideResult.output?.authoritativeChunkIds ?? []);
|
|
7892
8422
|
const authoritativeChunks = memoryChunksForAnswer.filter(
|
|
7893
8423
|
(c) => c.chunk_id && overrideIds.has(c.chunk_id)
|
|
@@ -7966,6 +8496,7 @@ var init_memory = __esm({
|
|
|
7966
8496
|
init_cjs_shims();
|
|
7967
8497
|
import_zod14 = require("zod");
|
|
7968
8498
|
init_micro_call();
|
|
8499
|
+
init_timing();
|
|
7969
8500
|
init_multi_query();
|
|
7970
8501
|
init_prefilter();
|
|
7971
8502
|
init_text_utils();
|
|
@@ -8087,7 +8618,7 @@ async function searchContexts(opts) {
|
|
|
8087
8618
|
skipPrefilter
|
|
8088
8619
|
} = opts;
|
|
8089
8620
|
const chunkArrays = await Promise.all(
|
|
8090
|
-
contextIds.map(async (
|
|
8621
|
+
contextIds.map((ctxId) => withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}Ms`, async () => {
|
|
8091
8622
|
try {
|
|
8092
8623
|
const ctx = contextsById.get(ctxId);
|
|
8093
8624
|
if (!ctx) return [];
|
|
@@ -8125,13 +8656,13 @@ async function searchContexts(opts) {
|
|
|
8125
8656
|
if (multiQuery) {
|
|
8126
8657
|
let hydePassage = null;
|
|
8127
8658
|
if (hyde) {
|
|
8128
|
-
hydePassage = await generateHydePassage({
|
|
8659
|
+
hydePassage = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.hydeMs`, () => generateHydePassage({
|
|
8129
8660
|
originalQuestion: question,
|
|
8130
8661
|
relevantKeywords: keywords,
|
|
8131
8662
|
importantKeyword,
|
|
8132
8663
|
styleHint,
|
|
8133
8664
|
model
|
|
8134
|
-
});
|
|
8665
|
+
}));
|
|
8135
8666
|
}
|
|
8136
8667
|
const candidates = [
|
|
8137
8668
|
question,
|
|
@@ -8146,14 +8677,14 @@ async function searchContexts(opts) {
|
|
|
8146
8677
|
}
|
|
8147
8678
|
if (kind === "conversations") {
|
|
8148
8679
|
if (keywordPrefilter && pinnedItemIds.length === 0) {
|
|
8149
|
-
const prefiltered = await fuzzyPrefilter({
|
|
8680
|
+
const prefiltered = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.prefilterMs`, () => fuzzyPrefilter({
|
|
8150
8681
|
cacheKey: `conversations:${ctxId}`,
|
|
8151
8682
|
relevantKeywords: keywords,
|
|
8152
8683
|
importantKeyword,
|
|
8153
8684
|
context: ctx,
|
|
8154
8685
|
fields: ["name", "id", "external_id", "description"],
|
|
8155
8686
|
normalize: (i) => [i.name, i.description].filter(Boolean).join(": ")
|
|
8156
|
-
});
|
|
8687
|
+
}));
|
|
8157
8688
|
pinnedItemIds = prefiltered.map((r) => r.id);
|
|
8158
8689
|
}
|
|
8159
8690
|
const keywordQuery = keywords.length ? keywords.join(" ") + " " + importantKeyword : question;
|
|
@@ -8168,7 +8699,7 @@ async function searchContexts(opts) {
|
|
|
8168
8699
|
console.warn(`[EXULU pipeline] searchContexts failed for context "${ctxId}":`, err);
|
|
8169
8700
|
return [];
|
|
8170
8701
|
}
|
|
8171
|
-
})
|
|
8702
|
+
}))
|
|
8172
8703
|
);
|
|
8173
8704
|
return { chunks: chunkArrays.flat() };
|
|
8174
8705
|
}
|
|
@@ -8181,6 +8712,7 @@ var init_search = __esm({
|
|
|
8181
8712
|
init_multi_query();
|
|
8182
8713
|
init_hyde();
|
|
8183
8714
|
init_prefilter();
|
|
8715
|
+
init_timing();
|
|
8184
8716
|
init_text_utils();
|
|
8185
8717
|
tagContext = (chunks, ctx) => chunks.map((c) => ({ ...c, context: { id: ctx.id, name: ctx.name ?? ctx.id } }));
|
|
8186
8718
|
}
|
|
@@ -8334,6 +8866,33 @@ var init_rerank = __esm({
|
|
|
8334
8866
|
}
|
|
8335
8867
|
});
|
|
8336
8868
|
|
|
8869
|
+
// ee/agentic-retrieval/pipeline/pin-rerun.ts
|
|
8870
|
+
function needsPinRerun(originalQuestion, updatedQuestion) {
|
|
8871
|
+
if (originalQuestion === updatedQuestion) return false;
|
|
8872
|
+
const before = designations(originalQuestion);
|
|
8873
|
+
for (const token of designations(updatedQuestion)) {
|
|
8874
|
+
if (!before.has(token)) return true;
|
|
8875
|
+
}
|
|
8876
|
+
return false;
|
|
8877
|
+
}
|
|
8878
|
+
function designations(text) {
|
|
8879
|
+
const out = /* @__PURE__ */ new Set();
|
|
8880
|
+
for (const raw of text.split(/[\s,;:()\[\]"'?!]+/)) {
|
|
8881
|
+
const token = raw.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
|
|
8882
|
+
if (!token) continue;
|
|
8883
|
+
const hasDigit = /\p{N}/u.test(token);
|
|
8884
|
+
const capitals = (token.match(/\p{Lu}/gu) ?? []).length;
|
|
8885
|
+
if (hasDigit || capitals >= 2) out.add(token.toLowerCase());
|
|
8886
|
+
}
|
|
8887
|
+
return out;
|
|
8888
|
+
}
|
|
8889
|
+
var init_pin_rerun = __esm({
|
|
8890
|
+
"ee/agentic-retrieval/pipeline/pin-rerun.ts"() {
|
|
8891
|
+
"use strict";
|
|
8892
|
+
init_cjs_shims();
|
|
8893
|
+
}
|
|
8894
|
+
});
|
|
8895
|
+
|
|
8337
8896
|
// ee/agentic-retrieval/pipeline/index.ts
|
|
8338
8897
|
function addChunks(result, chunks) {
|
|
8339
8898
|
const seen = new Set(result.chunks.map((c) => c.chunk_id));
|
|
@@ -8499,6 +9058,14 @@ function createAgenticRetrievalTool(opts) {
|
|
|
8499
9058
|
usage: [],
|
|
8500
9059
|
totalTokens: 0
|
|
8501
9060
|
};
|
|
9061
|
+
const t0 = Date.now();
|
|
9062
|
+
const timings = {};
|
|
9063
|
+
let tPhase = t0;
|
|
9064
|
+
const lap = (name) => {
|
|
9065
|
+
const now = Date.now();
|
|
9066
|
+
timings[name] = (timings[name] ?? 0) + (now - tPhase);
|
|
9067
|
+
tPhase = now;
|
|
9068
|
+
};
|
|
8502
9069
|
try {
|
|
8503
9070
|
let enabledContexts = contexts.filter(
|
|
8504
9071
|
(ctx) => cfg.knowledgeBases[ctx.id]?.enabled !== false
|
|
@@ -8571,8 +9138,19 @@ function createAgenticRetrievalTool(opts) {
|
|
|
8571
9138
|
resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
|
|
8572
9139
|
${projectScope.customInstructions}` : ""
|
|
8573
9140
|
].filter(Boolean).join("\n");
|
|
8574
|
-
const
|
|
8575
|
-
|
|
9141
|
+
const engineV2 = cfg.tuning.engine === "v2";
|
|
9142
|
+
const v2 = cfg.tuning.v2;
|
|
9143
|
+
const pinsFor = (question) => resolveIdentifierPins({
|
|
9144
|
+
question,
|
|
9145
|
+
identifierSets: cfg.vocabulary.identifiers,
|
|
9146
|
+
contextsById,
|
|
9147
|
+
kbKindById,
|
|
9148
|
+
model: utilityModel
|
|
9149
|
+
});
|
|
9150
|
+
const [memResult, routResult, parallelPins] = await Promise.all([
|
|
9151
|
+
withTiming(timings, "memoryMs", () => runMemoryPhase({
|
|
9152
|
+
timings,
|
|
9153
|
+
mergedCall: engineV2 && v2.mergedMemoryCall,
|
|
8576
9154
|
memoryChunks: memoryItems ?? [],
|
|
8577
9155
|
memoryContext,
|
|
8578
9156
|
question: userQuery,
|
|
@@ -8584,8 +9162,10 @@ ${projectScope.customInstructions}` : ""
|
|
|
8584
9162
|
memoryConfig: cfg.memory,
|
|
8585
9163
|
glossary: cfg.vocabulary.glossary,
|
|
8586
9164
|
documentContexts
|
|
8587
|
-
}),
|
|
8588
|
-
runRoutingPhase({
|
|
9165
|
+
})),
|
|
9166
|
+
withTiming(timings, "routingMs", () => runRoutingPhase({
|
|
9167
|
+
timings,
|
|
9168
|
+
mergedCall: engineV2 && v2.mergedRoutingCall,
|
|
8589
9169
|
question: userQuery,
|
|
8590
9170
|
enabledContexts,
|
|
8591
9171
|
documentContexts,
|
|
@@ -8596,8 +9176,12 @@ ${projectScope.customInstructions}` : ""
|
|
|
8596
9176
|
// detector must never treat these as filename hints.
|
|
8597
9177
|
knownIdentifiers: cfg.vocabulary.identifiers.flatMap((i) => i.examples),
|
|
8598
9178
|
model: utilityModel
|
|
8599
|
-
})
|
|
9179
|
+
})),
|
|
9180
|
+
// engine v2: identifier pins depend only on the question, so they run alongside
|
|
9181
|
+
// memory and routing instead of after them (re-run below if memory rewrote the question).
|
|
9182
|
+
engineV2 && v2.parallelPins ? withTiming(timings, "pinsParallelMs", () => pinsFor(userQuery)) : Promise.resolve(null)
|
|
8600
9183
|
]);
|
|
9184
|
+
lap("memoryRoutingMs");
|
|
8601
9185
|
for (const step of [...memResult.steps, ...routResult.steps]) {
|
|
8602
9186
|
result.steps.push({
|
|
8603
9187
|
stepNumber: 1,
|
|
@@ -8647,13 +9231,8 @@ ${projectScope.customInstructions}` : ""
|
|
|
8647
9231
|
memoryPinnedItemIdsByContext,
|
|
8648
9232
|
memoryOverride
|
|
8649
9233
|
} = memResult;
|
|
8650
|
-
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await
|
|
8651
|
-
|
|
8652
|
-
identifierSets: cfg.vocabulary.identifiers,
|
|
8653
|
-
contextsById,
|
|
8654
|
-
kbKindById,
|
|
8655
|
-
model: utilityModel
|
|
8656
|
-
});
|
|
9234
|
+
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = parallelPins && !needsPinRerun(userQuery, updatedQuestion) ? parallelPins : await pinsFor(updatedQuestion);
|
|
9235
|
+
lap("pinsMs");
|
|
8657
9236
|
for (const step of pinSteps) {
|
|
8658
9237
|
result.steps.push({
|
|
8659
9238
|
stepNumber: 1,
|
|
@@ -8683,6 +9262,8 @@ ${projectScope.customInstructions}` : ""
|
|
|
8683
9262
|
rewrites: cfg.vocabulary.rewrites,
|
|
8684
9263
|
styleHint: cfg.vocabulary.styleHint,
|
|
8685
9264
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
9265
|
+
timings,
|
|
9266
|
+
timingPrefix: "search.main",
|
|
8686
9267
|
skipPrefilter: false
|
|
8687
9268
|
}),
|
|
8688
9269
|
fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
|
|
@@ -8703,9 +9284,12 @@ ${projectScope.customInstructions}` : ""
|
|
|
8703
9284
|
rewrites: cfg.vocabulary.rewrites,
|
|
8704
9285
|
styleHint: cfg.vocabulary.styleHint,
|
|
8705
9286
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
9287
|
+
timings,
|
|
9288
|
+
timingPrefix: "search.fallback",
|
|
8706
9289
|
skipPrefilter: true
|
|
8707
9290
|
}) : Promise.resolve({ chunks: [] })
|
|
8708
9291
|
]);
|
|
9292
|
+
lap("searchMs");
|
|
8709
9293
|
const pinnedItemIds = /* @__PURE__ */ new Set([
|
|
8710
9294
|
...(function* () {
|
|
8711
9295
|
for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
|
|
@@ -8768,6 +9352,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
8768
9352
|
chunks: [],
|
|
8769
9353
|
tokens: 0
|
|
8770
9354
|
});
|
|
9355
|
+
lap("rerankMs");
|
|
8771
9356
|
addChunks(result, mainRerank.limited_results);
|
|
8772
9357
|
yield { result: serializeOutput(result) };
|
|
8773
9358
|
const literalLookupSatisfied = hasExplicitDocAndPage && mainRerank.limited_results.length > 0 && mainRerank.limited_results.some((r) => {
|
|
@@ -8816,6 +9401,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
8816
9401
|
});
|
|
8817
9402
|
result.reasoning.push({ text: "Fallback results reranked", tools: [] });
|
|
8818
9403
|
addChunks(result, fallbackRerank.limited_results);
|
|
9404
|
+
lap("fallbackRerankMs");
|
|
8819
9405
|
yield { result: serializeOutput(result) };
|
|
8820
9406
|
}
|
|
8821
9407
|
if (memoryOverride.active) {
|
|
@@ -8838,9 +9424,19 @@ Verified answer:
|
|
|
8838
9424
|
addChunks(result, memoryOverride.chunks);
|
|
8839
9425
|
yield { result: serializeOutput(result) };
|
|
8840
9426
|
}
|
|
9427
|
+
timings.totalMs = Date.now() - t0;
|
|
9428
|
+
result.timings = timings;
|
|
9429
|
+
result.steps.push({
|
|
9430
|
+
stepNumber: 1,
|
|
9431
|
+
text: `Timing: memory+routing ${timings.memoryRoutingMs ?? 0}ms (memory ${timings.memoryMs ?? 0}ms, routing ${timings.routingMs ?? 0}ms), pins ${timings.pinsMs ?? 0}ms, search ${timings.searchMs ?? 0}ms, rerank ${timings.rerankMs ?? 0}ms` + (timings.fallbackRerankMs !== void 0 ? `, fallback rerank ${timings.fallbackRerankMs}ms` : "") + `, total ${timings.totalMs}ms`,
|
|
9432
|
+
toolCalls: [],
|
|
9433
|
+
chunks: [],
|
|
9434
|
+
tokens: 0
|
|
9435
|
+
});
|
|
8841
9436
|
if (cfg.logging) {
|
|
8842
|
-
console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length }));
|
|
9437
|
+
console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length, timings }));
|
|
8843
9438
|
}
|
|
9439
|
+
yield { result: serializeOutput(result) };
|
|
8844
9440
|
return { result: serializeOutput(result) };
|
|
8845
9441
|
} catch (err) {
|
|
8846
9442
|
console.warn("[EXULU pipeline] retrieval pipeline failed:", err);
|
|
@@ -8875,6 +9471,8 @@ var init_pipeline = __esm({
|
|
|
8875
9471
|
init_prefilter();
|
|
8876
9472
|
init_search();
|
|
8877
9473
|
init_rerank();
|
|
9474
|
+
init_timing();
|
|
9475
|
+
init_pin_rerun();
|
|
8878
9476
|
init_global_ids();
|
|
8879
9477
|
init_global_ids();
|
|
8880
9478
|
}
|
|
@@ -10283,6 +10881,68 @@ function resolveSearchQueryTexts(query) {
|
|
|
10283
10881
|
hybridOrQuery: buildFullTextOrQuery(query, ftsText)
|
|
10284
10882
|
};
|
|
10285
10883
|
}
|
|
10884
|
+
function chooseFullTextQuery(opts) {
|
|
10885
|
+
if (opts.strictMatches || countOrTerms(opts.orText) > MAX_OR_TERMS) {
|
|
10886
|
+
return { fn: "plainto_tsquery", text: opts.strictText };
|
|
10887
|
+
}
|
|
10888
|
+
return { fn: "websearch_to_tsquery", text: opts.orText };
|
|
10889
|
+
}
|
|
10890
|
+
var MAX_OR_TERMS = 12;
|
|
10891
|
+
function countOrTerms(orText) {
|
|
10892
|
+
return orText.trim() ? orText.split(/\s+or\s+/i).length : 0;
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10895
|
+
// src/graphql/resolvers/expand-neighbours.ts
|
|
10896
|
+
init_cjs_shims();
|
|
10897
|
+
function planNeighbourFetch(results, expand) {
|
|
10898
|
+
const before = Math.max(0, expand.before ?? 0);
|
|
10899
|
+
const after = Math.max(0, expand.after ?? 0);
|
|
10900
|
+
const plan = /* @__PURE__ */ new Map();
|
|
10901
|
+
if (before === 0 && after === 0) return plan;
|
|
10902
|
+
const present = new Set(results.map((r) => `${r.item_id}-${r.chunk_index}`));
|
|
10903
|
+
for (const r of results) {
|
|
10904
|
+
for (let i = r.chunk_index - before; i <= r.chunk_index + after; i++) {
|
|
10905
|
+
if (i < 0 || i === r.chunk_index || present.has(`${r.item_id}-${i}`)) continue;
|
|
10906
|
+
if (!plan.has(r.item_id)) plan.set(r.item_id, /* @__PURE__ */ new Set());
|
|
10907
|
+
plan.get(r.item_id).add(i);
|
|
10908
|
+
}
|
|
10909
|
+
}
|
|
10910
|
+
return plan;
|
|
10911
|
+
}
|
|
10912
|
+
function mergeNeighbours(results, rows, plan, context) {
|
|
10913
|
+
const byItem = /* @__PURE__ */ new Map();
|
|
10914
|
+
for (const r of results) if (!byItem.has(r.item_id)) byItem.set(r.item_id, r);
|
|
10915
|
+
const merged = /* @__PURE__ */ new Map();
|
|
10916
|
+
for (const r of results) merged.set(`${r.item_id}-${r.chunk_index}`, r);
|
|
10917
|
+
for (const row of rows) {
|
|
10918
|
+
if (!plan.get(row.source)?.has(row.chunk_index)) continue;
|
|
10919
|
+
const key = `${row.source}-${row.chunk_index}`;
|
|
10920
|
+
if (merged.has(key)) continue;
|
|
10921
|
+
const parent = byItem.get(row.source);
|
|
10922
|
+
if (!parent) continue;
|
|
10923
|
+
merged.set(key, {
|
|
10924
|
+
chunk_content: row.content,
|
|
10925
|
+
chunk_index: row.chunk_index,
|
|
10926
|
+
chunk_id: row.id,
|
|
10927
|
+
chunk_source: row.source,
|
|
10928
|
+
chunk_metadata: row.metadata,
|
|
10929
|
+
chunk_created_at: row.createdAt,
|
|
10930
|
+
chunk_updated_at: row.updatedAt,
|
|
10931
|
+
item_updated_at: parent.item_updated_at,
|
|
10932
|
+
item_created_at: parent.item_created_at,
|
|
10933
|
+
item_id: parent.item_id,
|
|
10934
|
+
item_external_id: parent.item_external_id,
|
|
10935
|
+
item_name: parent.item_name,
|
|
10936
|
+
chunk_cosine_distance: 0,
|
|
10937
|
+
chunk_fts_rank: 0,
|
|
10938
|
+
chunk_hybrid_score: 0,
|
|
10939
|
+
context
|
|
10940
|
+
});
|
|
10941
|
+
}
|
|
10942
|
+
return Array.from(merged.values()).sort(
|
|
10943
|
+
(a, b) => a.item_id === b.item_id ? a.chunk_index - b.chunk_index : 0
|
|
10944
|
+
);
|
|
10945
|
+
}
|
|
10286
10946
|
|
|
10287
10947
|
// src/graphql/resolvers/apply-sorting.ts
|
|
10288
10948
|
init_cjs_shims();
|
|
@@ -11273,6 +11933,14 @@ var agentsSchema = {
|
|
|
11273
11933
|
name: "max_tool_steps",
|
|
11274
11934
|
type: "number"
|
|
11275
11935
|
},
|
|
11936
|
+
{
|
|
11937
|
+
// Thinking budget of the answer model, forwarded as LiteLLM's
|
|
11938
|
+
// reasoning_effort ("none" | "disable" | "minimal" | "low" | "medium" |
|
|
11939
|
+
// "high"). null = provider default. See resolve-reasoning-effort.ts.
|
|
11940
|
+
// Auto-ALTERed on boot.
|
|
11941
|
+
name: "reasoning_effort",
|
|
11942
|
+
type: "text"
|
|
11943
|
+
},
|
|
11276
11944
|
{
|
|
11277
11945
|
name: "guest_access",
|
|
11278
11946
|
type: "boolean",
|
|
@@ -11990,6 +12658,15 @@ var convertContextToTableDefinition = (context) => {
|
|
|
11990
12658
|
// src/graphql/resolvers/vector-search.ts
|
|
11991
12659
|
init_statistics2();
|
|
11992
12660
|
|
|
12661
|
+
// src/graphql/resolvers/query-embedding-policy.ts
|
|
12662
|
+
init_cjs_shims();
|
|
12663
|
+
function needsQueryEmbedding(method) {
|
|
12664
|
+
return method !== "tsvector";
|
|
12665
|
+
}
|
|
12666
|
+
function boostsWithQueryEntities(method) {
|
|
12667
|
+
return method !== "tsvector";
|
|
12668
|
+
}
|
|
12669
|
+
|
|
11993
12670
|
// src/exulu/entities/index.ts
|
|
11994
12671
|
init_cjs_shims();
|
|
11995
12672
|
init_client();
|
|
@@ -12770,7 +13447,9 @@ var vectorSearch = async ({
|
|
|
12770
13447
|
const embedText = texts.embedText;
|
|
12771
13448
|
hybridOrQuery = texts.hybridOrQuery;
|
|
12772
13449
|
query = texts.ftsText;
|
|
12773
|
-
if (
|
|
13450
|
+
if (!needsQueryEmbedding(method)) {
|
|
13451
|
+
_embedSource = "none";
|
|
13452
|
+
} else if (queryEmbedding && queryEmbedding.length) {
|
|
12774
13453
|
vector = queryEmbedding;
|
|
12775
13454
|
_embedSource = "reused";
|
|
12776
13455
|
} else {
|
|
@@ -12803,8 +13482,10 @@ var vectorSearch = async ({
|
|
|
12803
13482
|
vector = queryVector;
|
|
12804
13483
|
_embedSource = "computed";
|
|
12805
13484
|
}
|
|
12806
|
-
|
|
12807
|
-
|
|
13485
|
+
if (vector.length) {
|
|
13486
|
+
vectorStr = `ARRAY[${vector.join(",")}]`;
|
|
13487
|
+
vectorExpr = `${vectorStr}::vector`;
|
|
13488
|
+
}
|
|
12808
13489
|
}
|
|
12809
13490
|
let keywordsQuery = [];
|
|
12810
13491
|
if (keywords) {
|
|
@@ -12862,14 +13543,22 @@ var vectorSearch = async ({
|
|
|
12862
13543
|
]);
|
|
12863
13544
|
resultChunks = await chunksQuery;
|
|
12864
13545
|
break;
|
|
12865
|
-
case "hybridSearch":
|
|
13546
|
+
case "hybridSearch": {
|
|
13547
|
+
let strictMatches = false;
|
|
13548
|
+
if (query && hybridOrQuery) {
|
|
13549
|
+
const probe = await db2(chunksTable + " as chunks").select(db2.raw("1")).whereRaw(`(${languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ")})`, languages.map(() => query)).first();
|
|
13550
|
+
strictMatches = Boolean(probe);
|
|
13551
|
+
}
|
|
13552
|
+
const fullText = chooseFullTextQuery({ strictMatches, strictText: query ?? "", orText: hybridOrQuery });
|
|
13553
|
+
const ftsFn = fullText.fn;
|
|
13554
|
+
hybridOrQuery = fullText.text;
|
|
12866
13555
|
const matchCount = Math.min(limit * 2);
|
|
12867
13556
|
const fullTextWeight = 2;
|
|
12868
13557
|
const semanticWeight = 1;
|
|
12869
13558
|
const rrfK = 50;
|
|
12870
|
-
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts,
|
|
13559
|
+
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ");
|
|
12871
13560
|
const ftRankParams = languages.map(() => hybridOrQuery);
|
|
12872
|
-
const ftMatchExpression = languages.map((lang) => `chunks.fts @@
|
|
13561
|
+
const ftMatchExpression = languages.map((lang) => `chunks.fts @@ ${ftsFn}('${lang}', ?)`).join(" OR ");
|
|
12873
13562
|
const ftMatchParams = languages.map(() => hybridOrQuery);
|
|
12874
13563
|
let fullTextQuery = db2(chunksTable + " as chunks").select([
|
|
12875
13564
|
"chunks.id",
|
|
@@ -12910,7 +13599,7 @@ var vectorSearch = async ({
|
|
|
12910
13599
|
db2.raw('items."updatedAt" as item_updated_at'),
|
|
12911
13600
|
db2.raw('items."createdAt" as item_created_at'),
|
|
12912
13601
|
db2.raw(
|
|
12913
|
-
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
13602
|
+
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) AS fts_rank`,
|
|
12914
13603
|
languages.map(() => hybridOrQuery)
|
|
12915
13604
|
),
|
|
12916
13605
|
db2.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
|
|
@@ -12936,12 +13625,14 @@ var vectorSearch = async ({
|
|
|
12936
13625
|
`,
|
|
12937
13626
|
[rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
|
|
12938
13627
|
).whereRaw(
|
|
12939
|
-
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
13628
|
+
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) > ?)`,
|
|
12940
13629
|
[...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
|
|
12941
13630
|
).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
|
|
12942
13631
|
cutoffs?.cosineDistance || 0
|
|
12943
13632
|
]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
|
|
12944
13633
|
resultChunks = await hybridQuery;
|
|
13634
|
+
break;
|
|
13635
|
+
}
|
|
12945
13636
|
}
|
|
12946
13637
|
if (process.env.EXULU_VS_TIMING) {
|
|
12947
13638
|
console.log(
|
|
@@ -12993,7 +13684,7 @@ var vectorSearch = async ({
|
|
|
12993
13684
|
}
|
|
12994
13685
|
let queryEntities = [];
|
|
12995
13686
|
let entityInsights;
|
|
12996
|
-
if (entitiesOn && rawQuery) {
|
|
13687
|
+
if (entitiesOn && rawQuery && boostsWithQueryEntities(method)) {
|
|
12997
13688
|
try {
|
|
12998
13689
|
const types = await hydrateEntityTypes(context);
|
|
12999
13690
|
const { mentions: queryMentions } = await extractEntitiesForItem({
|
|
@@ -13012,117 +13703,31 @@ var vectorSearch = async ({
|
|
|
13012
13703
|
const max = Math.max(...scores);
|
|
13013
13704
|
const range = max - min;
|
|
13014
13705
|
const boostWeight = context.entities?.boostWeight ?? 0.3;
|
|
13015
|
-
results = results.map((r, i) => {
|
|
13016
|
-
const base = range > 0 ? ((scores[i] ?? 0) - min) / range : 1;
|
|
13017
|
-
const shared = sharedCounts.get(r.chunk_id) || 0;
|
|
13018
|
-
const boost = boostWeight * (shared / (shared + 1));
|
|
13019
|
-
return { r, score: base + boost };
|
|
13020
|
-
}).sort((a, b) => b.score - a.score).map((x) => x.r);
|
|
13021
|
-
}
|
|
13022
|
-
} catch (err) {
|
|
13023
|
-
console.error("[EXULU] Entity boost step failed (continuing):", err.message);
|
|
13024
|
-
}
|
|
13025
|
-
}
|
|
13026
|
-
results = results.slice(0, limit);
|
|
13027
|
-
if (expand?.before || expand?.after) {
|
|
13028
|
-
const expandedMap = /* @__PURE__ */ new Map();
|
|
13029
|
-
for (const chunk of results) {
|
|
13030
|
-
expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
|
|
13031
|
-
}
|
|
13032
|
-
if (expand?.before) {
|
|
13033
|
-
for (const chunk of results) {
|
|
13034
|
-
const indicesToFetch = Array.from(
|
|
13035
|
-
{ length: expand.before },
|
|
13036
|
-
(_, i) => chunk.chunk_index - expand.before + i
|
|
13037
|
-
).filter((index) => index >= 0);
|
|
13038
|
-
await Promise.all(
|
|
13039
|
-
indicesToFetch.map(async (index) => {
|
|
13040
|
-
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
13041
|
-
return;
|
|
13042
|
-
}
|
|
13043
|
-
const expandedChunk = await db2(chunksTable).where({
|
|
13044
|
-
source: chunk.item_id,
|
|
13045
|
-
chunk_index: index
|
|
13046
|
-
}).first();
|
|
13047
|
-
if (expandedChunk) {
|
|
13048
|
-
if (expandedChunk) {
|
|
13049
|
-
expandedMap.set(`${chunk.item_id}-${index}`, {
|
|
13050
|
-
chunk_content: expandedChunk.content,
|
|
13051
|
-
chunk_index: expandedChunk.chunk_index,
|
|
13052
|
-
chunk_id: expandedChunk.id,
|
|
13053
|
-
chunk_source: expandedChunk.source,
|
|
13054
|
-
chunk_metadata: expandedChunk.metadata,
|
|
13055
|
-
chunk_created_at: expandedChunk.createdAt,
|
|
13056
|
-
chunk_updated_at: expandedChunk.updatedAt,
|
|
13057
|
-
item_updated_at: chunk.item_updated_at,
|
|
13058
|
-
item_created_at: chunk.item_created_at,
|
|
13059
|
-
item_id: chunk.item_id,
|
|
13060
|
-
item_external_id: chunk.item_external_id,
|
|
13061
|
-
item_name: chunk.item_name,
|
|
13062
|
-
chunk_cosine_distance: 0,
|
|
13063
|
-
chunk_fts_rank: 0,
|
|
13064
|
-
chunk_hybrid_score: 0,
|
|
13065
|
-
context: {
|
|
13066
|
-
name: table.name.singular,
|
|
13067
|
-
id: table.id || ""
|
|
13068
|
-
}
|
|
13069
|
-
});
|
|
13070
|
-
}
|
|
13071
|
-
}
|
|
13072
|
-
})
|
|
13073
|
-
);
|
|
13074
|
-
}
|
|
13075
|
-
}
|
|
13076
|
-
if (expand?.after) {
|
|
13077
|
-
for (const chunk of results) {
|
|
13078
|
-
const indicesToFetch = Array.from(
|
|
13079
|
-
{ length: expand.after },
|
|
13080
|
-
(_, i) => chunk.chunk_index + i + 1
|
|
13081
|
-
);
|
|
13082
|
-
await Promise.all(
|
|
13083
|
-
indicesToFetch.map(async (index) => {
|
|
13084
|
-
if (expandedMap.has(`${chunk.item_id}-${index}`)) {
|
|
13085
|
-
return;
|
|
13086
|
-
}
|
|
13087
|
-
const expandedChunk = await db2(chunksTable).where({
|
|
13088
|
-
source: chunk.item_id,
|
|
13089
|
-
chunk_index: index
|
|
13090
|
-
}).first();
|
|
13091
|
-
if (expandedChunk) {
|
|
13092
|
-
expandedMap.set(`${chunk.item_id}-${index}`, {
|
|
13093
|
-
chunk_content: expandedChunk.content,
|
|
13094
|
-
chunk_index: expandedChunk.chunk_index,
|
|
13095
|
-
chunk_id: expandedChunk.id,
|
|
13096
|
-
chunk_source: expandedChunk.source,
|
|
13097
|
-
chunk_metadata: expandedChunk.metadata,
|
|
13098
|
-
chunk_created_at: expandedChunk.createdAt,
|
|
13099
|
-
chunk_updated_at: expandedChunk.updatedAt,
|
|
13100
|
-
item_updated_at: chunk.item_updated_at,
|
|
13101
|
-
item_created_at: chunk.item_created_at,
|
|
13102
|
-
item_id: chunk.item_id,
|
|
13103
|
-
item_external_id: chunk.item_external_id,
|
|
13104
|
-
item_name: chunk.item_name,
|
|
13105
|
-
chunk_cosine_distance: 0,
|
|
13106
|
-
chunk_fts_rank: 0,
|
|
13107
|
-
chunk_hybrid_score: 0,
|
|
13108
|
-
context: {
|
|
13109
|
-
name: table.name.singular,
|
|
13110
|
-
id: table.id || ""
|
|
13111
|
-
}
|
|
13112
|
-
});
|
|
13113
|
-
}
|
|
13114
|
-
})
|
|
13115
|
-
);
|
|
13706
|
+
results = results.map((r, i) => {
|
|
13707
|
+
const base = range > 0 ? ((scores[i] ?? 0) - min) / range : 1;
|
|
13708
|
+
const shared = sharedCounts.get(r.chunk_id) || 0;
|
|
13709
|
+
const boost = boostWeight * (shared / (shared + 1));
|
|
13710
|
+
return { r, score: base + boost };
|
|
13711
|
+
}).sort((a, b) => b.score - a.score).map((x) => x.r);
|
|
13116
13712
|
}
|
|
13713
|
+
} catch (err) {
|
|
13714
|
+
console.error("[EXULU] Entity boost step failed (continuing):", err.message);
|
|
13715
|
+
}
|
|
13716
|
+
}
|
|
13717
|
+
results = results.slice(0, limit);
|
|
13718
|
+
if (expand?.before || expand?.after) {
|
|
13719
|
+
const plan = planNeighbourFetch(results, expand);
|
|
13720
|
+
if (plan.size > 0) {
|
|
13721
|
+
const itemIds = Array.from(plan.keys());
|
|
13722
|
+
const indices = Array.from(new Set(Array.from(plan.values()).flatMap((s) => Array.from(s))));
|
|
13723
|
+
const rows = await db2(chunksTable).select(["id", "source", "chunk_index", "content", "metadata", "createdAt", "updatedAt"]).whereIn("source", itemIds).whereIn("chunk_index", indices);
|
|
13724
|
+
results = mergeNeighbours(results, rows, plan, { name: table.name.singular, id: table.id || "" });
|
|
13117
13725
|
}
|
|
13118
|
-
results = Array.from(expandedMap.values());
|
|
13119
13726
|
results = results.sort((a, b) => {
|
|
13120
13727
|
if (a.item_id !== b.item_id) {
|
|
13121
13728
|
return a.item_id.localeCompare(b.item_id);
|
|
13122
13729
|
}
|
|
13123
|
-
|
|
13124
|
-
const bIndex = Number(b.chunk_index);
|
|
13125
|
-
return aIndex - bIndex;
|
|
13730
|
+
return Number(a.chunk_index) - Number(b.chunk_index);
|
|
13126
13731
|
});
|
|
13127
13732
|
}
|
|
13128
13733
|
if (entitiesOn) {
|
|
@@ -16713,6 +17318,20 @@ function serializeError(err, depth = 0) {
|
|
|
16713
17318
|
return { message: String(err) };
|
|
16714
17319
|
}
|
|
16715
17320
|
|
|
17321
|
+
// src/exulu/turn-metadata.ts
|
|
17322
|
+
init_cjs_shims();
|
|
17323
|
+
function finishTurnMetadata(opts) {
|
|
17324
|
+
const now = opts.now ?? Date.now();
|
|
17325
|
+
return {
|
|
17326
|
+
totalTokens: opts.totalUsage.totalTokens,
|
|
17327
|
+
reasoningTokens: opts.totalUsage.reasoningTokens,
|
|
17328
|
+
inputTokens: opts.totalUsage.inputTokens,
|
|
17329
|
+
outputTokens: opts.totalUsage.outputTokens,
|
|
17330
|
+
cachedInputTokens: opts.totalUsage.cachedInputTokens,
|
|
17331
|
+
durationMs: Math.max(0, now - opts.startedAt)
|
|
17332
|
+
};
|
|
17333
|
+
}
|
|
17334
|
+
|
|
16716
17335
|
// src/utils/enabled-tools.ts
|
|
16717
17336
|
init_cjs_shims();
|
|
16718
17337
|
init_pipeline();
|
|
@@ -16837,6 +17456,58 @@ async function resolveFreshFileUrl(url, opts) {
|
|
|
16837
17456
|
}
|
|
16838
17457
|
}
|
|
16839
17458
|
|
|
17459
|
+
// src/exulu/session-file-listing.ts
|
|
17460
|
+
init_cjs_shims();
|
|
17461
|
+
init_uppy();
|
|
17462
|
+
init_artifact_filter();
|
|
17463
|
+
init_session_files();
|
|
17464
|
+
var DEFAULT_MAX = 25;
|
|
17465
|
+
function formatSize(bytes) {
|
|
17466
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
17467
|
+
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
17468
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17469
|
+
}
|
|
17470
|
+
function formatAge(then, now) {
|
|
17471
|
+
const minutes = Math.max(0, Math.round((now.getTime() - then.getTime()) / 6e4));
|
|
17472
|
+
if (minutes < 1) return "just now";
|
|
17473
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
|
|
17474
|
+
const hours = Math.round(minutes / 60);
|
|
17475
|
+
if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
17476
|
+
const days = Math.round(hours / 24);
|
|
17477
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
17478
|
+
}
|
|
17479
|
+
function describeSessionFiles(files, opts = {}) {
|
|
17480
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
17481
|
+
const max = opts.max ?? DEFAULT_MAX;
|
|
17482
|
+
const usable = files.filter((f) => f.name && !f.name.endsWith("/") && !isIgnoredArtifactPath(f.name)).sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
17483
|
+
if (usable.length === 0) return "";
|
|
17484
|
+
const shown = usable.slice(0, max);
|
|
17485
|
+
const lines = shown.map((f) => {
|
|
17486
|
+
const isNew = opts.lastTurnAt ? f.lastModified.getTime() > opts.lastTurnAt.getTime() : false;
|
|
17487
|
+
return `- ${f.name} (${formatSize(f.size)}, ${formatAge(f.lastModified, now)})${isNew ? " [NEW since your last answer]" : ""}`;
|
|
17488
|
+
});
|
|
17489
|
+
const omitted = usable.length - shown.length;
|
|
17490
|
+
if (omitted > 0) lines.push(`\u2026 and ${omitted} more file${omitted === 1 ? "" : "s"} (list them with \`ls\`).`);
|
|
17491
|
+
return "Files currently in this session (newest first):\n" + lines.join("\n") + '\nThese files are available to you. When the user refers to one of these files, to "the document" or "the attachment", or asks something only such a file can answer, read it with parse_document, view_document_page or read_session_file. Otherwise proceed as usual, e.g. with the knowledge bases.';
|
|
17492
|
+
}
|
|
17493
|
+
async function loadSessionFileListing(opts) {
|
|
17494
|
+
const uploads = opts.exuluConfig?.fileUploads;
|
|
17495
|
+
if (!uploads?.s3Bucket) return "";
|
|
17496
|
+
const prefix = sessionFilePrefix(opts.ownerId, opts.sessionID, uploads.s3prefix);
|
|
17497
|
+
try {
|
|
17498
|
+
const objects = await listS3ObjectsByPrefix(prefix, opts.exuluConfig);
|
|
17499
|
+
const files = objects.map((o) => ({
|
|
17500
|
+
name: o.key.slice(o.key.indexOf(prefix) + prefix.length),
|
|
17501
|
+
size: o.size,
|
|
17502
|
+
lastModified: new Date(o.lastModified)
|
|
17503
|
+
}));
|
|
17504
|
+
return describeSessionFiles(files, { lastTurnAt: opts.lastTurnAt });
|
|
17505
|
+
} catch (err) {
|
|
17506
|
+
console.warn(`[EXULU] could not list session files for prompt (session ${opts.sessionID}):`, err);
|
|
17507
|
+
return "";
|
|
17508
|
+
}
|
|
17509
|
+
}
|
|
17510
|
+
|
|
16840
17511
|
// src/exulu/generate-stream.ts
|
|
16841
17512
|
init_uppy();
|
|
16842
17513
|
var import_ai6 = require("ai");
|
|
@@ -17088,11 +17759,12 @@ function resolveTurnStepBudget(maxStepCount, agent) {
|
|
|
17088
17759
|
}
|
|
17089
17760
|
return DEFAULT_MAX_STEPS;
|
|
17090
17761
|
}
|
|
17762
|
+
var TOOL_INPUT_FLATTEN_CHARS = 6e3;
|
|
17091
17763
|
function flattenPart(part) {
|
|
17092
17764
|
const p = part;
|
|
17093
17765
|
if (p?.type === "text") return p.text ?? "";
|
|
17094
17766
|
if (p?.type === "tool-call") {
|
|
17095
|
-
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0,
|
|
17767
|
+
return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, TOOL_INPUT_FLATTEN_CHARS)}`;
|
|
17096
17768
|
}
|
|
17097
17769
|
if (p?.type === "tool-result") {
|
|
17098
17770
|
const out = p.output?.value ?? p.output;
|
|
@@ -17114,7 +17786,7 @@ function flattenToolHistory(messages) {
|
|
|
17114
17786
|
return m;
|
|
17115
17787
|
});
|
|
17116
17788
|
}
|
|
17117
|
-
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
17789
|
+
var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. Do not invent, estimate or "fill in" values that were not actually gathered: report only what the tools returned or what you wrote down, and name explicitly what is missing. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
|
|
17118
17790
|
function finalAnswerGuard(maxSteps) {
|
|
17119
17791
|
return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
|
|
17120
17792
|
toolChoice: "none",
|
|
@@ -17139,6 +17811,37 @@ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
|
|
|
17139
17811
|
};
|
|
17140
17812
|
}
|
|
17141
17813
|
|
|
17814
|
+
// src/exulu/resolve-reasoning-effort.ts
|
|
17815
|
+
init_cjs_shims();
|
|
17816
|
+
var REASONING_EFFORTS = ["none", "disable", "minimal", "low", "medium", "high"];
|
|
17817
|
+
function resolveReasoningEffort(agent) {
|
|
17818
|
+
const raw = agent?.reasoning_effort;
|
|
17819
|
+
if (typeof raw !== "string") return void 0;
|
|
17820
|
+
const normalized = raw.trim().toLowerCase();
|
|
17821
|
+
return REASONING_EFFORTS.includes(normalized) ? normalized : void 0;
|
|
17822
|
+
}
|
|
17823
|
+
function resolveProviderOptions(agent) {
|
|
17824
|
+
const effort = resolveReasoningEffort(agent);
|
|
17825
|
+
return {
|
|
17826
|
+
openai: { reasoningSummary: "auto" },
|
|
17827
|
+
...effort ? { litellm: { reasoningEffort: effort } } : {}
|
|
17828
|
+
};
|
|
17829
|
+
}
|
|
17830
|
+
|
|
17831
|
+
// src/exulu/stream-error.ts
|
|
17832
|
+
init_cjs_shims();
|
|
17833
|
+
function onChatStreamError({ error }) {
|
|
17834
|
+
const detail = error instanceof Error ? error.message : error === void 0 ? "unknown error" : safeStringify(error);
|
|
17835
|
+
console.error("[EXULU] chat stream error.", detail);
|
|
17836
|
+
}
|
|
17837
|
+
function safeStringify(value) {
|
|
17838
|
+
try {
|
|
17839
|
+
return JSON.stringify(value) ?? String(value);
|
|
17840
|
+
} catch {
|
|
17841
|
+
return String(value);
|
|
17842
|
+
}
|
|
17843
|
+
}
|
|
17844
|
+
|
|
17142
17845
|
// src/exulu/generate-stream.ts
|
|
17143
17846
|
init_sanitize_tool_name();
|
|
17144
17847
|
init_tool_image_attachments();
|
|
@@ -17236,6 +17939,15 @@ var saveChat = async ({
|
|
|
17236
17939
|
await mutation;
|
|
17237
17940
|
}
|
|
17238
17941
|
};
|
|
17942
|
+
var lastMessageTime = (rows) => {
|
|
17943
|
+
let latest;
|
|
17944
|
+
for (const row of rows) {
|
|
17945
|
+
if (!row.createdAt) continue;
|
|
17946
|
+
const d = new Date(row.createdAt);
|
|
17947
|
+
if (!latest || d > latest) latest = d;
|
|
17948
|
+
}
|
|
17949
|
+
return latest;
|
|
17950
|
+
};
|
|
17239
17951
|
var getAgentMessages = async ({
|
|
17240
17952
|
session,
|
|
17241
17953
|
user,
|
|
@@ -17292,10 +18004,13 @@ var generateSync = async ({
|
|
|
17292
18004
|
}
|
|
17293
18005
|
let project;
|
|
17294
18006
|
let sessionItems;
|
|
18007
|
+
let sessionOwnerId;
|
|
18008
|
+
let lastTurnAt;
|
|
17295
18009
|
if (session) {
|
|
17296
18010
|
const sessionData = await getSession({ sessionID: session });
|
|
17297
18011
|
sessionItems = sessionData.session_items;
|
|
17298
18012
|
project = sessionData.project;
|
|
18013
|
+
sessionOwnerId = sessionData.user ?? void 0;
|
|
17299
18014
|
}
|
|
17300
18015
|
const model = languageModel;
|
|
17301
18016
|
console.log("[EXULU] Model created for generating sync.");
|
|
@@ -17305,6 +18020,7 @@ var generateSync = async ({
|
|
|
17305
18020
|
session,
|
|
17306
18021
|
user: user.id
|
|
17307
18022
|
});
|
|
18023
|
+
lastTurnAt = lastMessageTime(previousMessages);
|
|
17308
18024
|
const previousMessagesContent = previousMessages.map(
|
|
17309
18025
|
(message) => JSON.parse(message.content)
|
|
17310
18026
|
);
|
|
@@ -17399,7 +18115,8 @@ var generateSync = async ({
|
|
|
17399
18115
|
agent,
|
|
17400
18116
|
memoryItems,
|
|
17401
18117
|
contextWindow,
|
|
17402
|
-
disabledTools
|
|
18118
|
+
disabledTools,
|
|
18119
|
+
sessionOwnerId
|
|
17403
18120
|
);
|
|
17404
18121
|
const agenticEntry = currentTools?.find((t) => t.id === "agentic_context_search");
|
|
17405
18122
|
const agenticToolKey = agenticEntry ? sanitizeToolName(agenticEntry.name) : void 0;
|
|
@@ -17466,6 +18183,15 @@ var generateSync = async ({
|
|
|
17466
18183
|
commands like \`node create_doc.js\`) live in the same place. These files are scoped to
|
|
17467
18184
|
this single session; they are NOT visible in other sessions, projects, or knowledge bases.
|
|
17468
18185
|
`;
|
|
18186
|
+
if (session) {
|
|
18187
|
+
const listing = await loadSessionFileListing({
|
|
18188
|
+
sessionID: session,
|
|
18189
|
+
ownerId: sessionOwnerId ?? user?.id ?? "api",
|
|
18190
|
+
exuluConfig,
|
|
18191
|
+
lastTurnAt
|
|
18192
|
+
});
|
|
18193
|
+
if (listing) system += "\n\n" + listing;
|
|
18194
|
+
}
|
|
17469
18195
|
system += `
|
|
17470
18196
|
|
|
17471
18197
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
@@ -17634,10 +18360,13 @@ var generateStream = async ({
|
|
|
17634
18360
|
let previousMessagesContent = previousMessages || [];
|
|
17635
18361
|
let project;
|
|
17636
18362
|
let sessionItems;
|
|
18363
|
+
let sessionOwnerId;
|
|
18364
|
+
let lastTurnAt;
|
|
17637
18365
|
if (session) {
|
|
17638
18366
|
const sessionData = await getSession({ sessionID: session });
|
|
17639
18367
|
project = sessionData.project;
|
|
17640
18368
|
sessionItems = sessionData.session_items;
|
|
18369
|
+
sessionOwnerId = sessionData.user ?? void 0;
|
|
17641
18370
|
console.log("[EXULU] loading previous messages from session: " + session);
|
|
17642
18371
|
const previousMessages2 = await getAgentMessages({
|
|
17643
18372
|
session,
|
|
@@ -17648,6 +18377,7 @@ var generateStream = async ({
|
|
|
17648
18377
|
includeAllUsers: isRunSessionMetadata(sessionData.metadata)
|
|
17649
18378
|
});
|
|
17650
18379
|
previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
|
|
18380
|
+
lastTurnAt = lastMessageTime(previousMessages2);
|
|
17651
18381
|
}
|
|
17652
18382
|
const model = languageModel;
|
|
17653
18383
|
messages = await (0, import_ai6.validateUIMessages)({
|
|
@@ -17790,6 +18520,15 @@ ${skillsList}
|
|
|
17790
18520
|
truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
|
|
17791
18521
|
to page through it \u2014 do not ask the user to re-upload.
|
|
17792
18522
|
`;
|
|
18523
|
+
if (session) {
|
|
18524
|
+
const listing = await loadSessionFileListing({
|
|
18525
|
+
sessionID: session,
|
|
18526
|
+
ownerId: sessionOwnerId ?? user?.id ?? "api",
|
|
18527
|
+
exuluConfig,
|
|
18528
|
+
lastTurnAt
|
|
18529
|
+
});
|
|
18530
|
+
if (listing) system += "\n\n" + listing;
|
|
18531
|
+
}
|
|
17793
18532
|
system += `
|
|
17794
18533
|
|
|
17795
18534
|
When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
|
|
@@ -17817,7 +18556,8 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
17817
18556
|
agent,
|
|
17818
18557
|
memoryItems,
|
|
17819
18558
|
contextWindow,
|
|
17820
|
-
disabledTools
|
|
18559
|
+
disabledTools,
|
|
18560
|
+
sessionOwnerId
|
|
17821
18561
|
);
|
|
17822
18562
|
console.log("[EXULU] Converted tools", Object.keys(tools));
|
|
17823
18563
|
const includesContextSearchTool = currentTools?.some(
|
|
@@ -17891,18 +18631,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
17891
18631
|
// for the first step or change other parameters.
|
|
17892
18632
|
system,
|
|
17893
18633
|
maxRetries: 2,
|
|
17894
|
-
|
|
17895
|
-
|
|
17896
|
-
|
|
17897
|
-
}
|
|
17898
|
-
},
|
|
18634
|
+
// OpenAI reasoning summaries + the agent's optional thinking budget
|
|
18635
|
+
// (agents.reasoning_effort → LiteLLM reasoning_effort).
|
|
18636
|
+
providerOptions: resolveProviderOptions(agent),
|
|
17899
18637
|
tools,
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
throw new Error(
|
|
17903
|
-
`Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
|
|
17904
|
-
);
|
|
17905
|
-
},
|
|
18638
|
+
// Log only — throwing here crashed the process (see stream-error.ts).
|
|
18639
|
+
onError: onChatStreamError,
|
|
17906
18640
|
// todo allow configuring the step budget per skill
|
|
17907
18641
|
prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
|
|
17908
18642
|
stopWhen: [(0, import_ai6.stepCountIs)(turnBudget), (0, import_ai6.hasToolCall)("image_generation")]
|
|
@@ -19766,13 +20500,7 @@ var processUiMessagesFlow = async ({
|
|
|
19766
20500
|
messageMetadata: ({ part }) => {
|
|
19767
20501
|
console.log("[EXULU] part", part.type);
|
|
19768
20502
|
if (part.type === "finish") {
|
|
19769
|
-
return {
|
|
19770
|
-
totalTokens: part.totalUsage.totalTokens,
|
|
19771
|
-
reasoningTokens: part.totalUsage.reasoningTokens,
|
|
19772
|
-
inputTokens: part.totalUsage.inputTokens,
|
|
19773
|
-
outputTokens: part.totalUsage.outputTokens,
|
|
19774
|
-
cachedInputTokens: part.totalUsage.cachedInputTokens
|
|
19775
|
-
};
|
|
20503
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
|
|
19776
20504
|
}
|
|
19777
20505
|
return void 0;
|
|
19778
20506
|
},
|
|
@@ -19977,7 +20705,7 @@ var mapRoutineRunRow = (row, routineById) => {
|
|
|
19977
20705
|
|
|
19978
20706
|
// src/graphql/schemas/index.ts
|
|
19979
20707
|
init_entitlements();
|
|
19980
|
-
var
|
|
20708
|
+
var import_fs3 = require("fs");
|
|
19981
20709
|
|
|
19982
20710
|
// src/exulu/transcription/service.ts
|
|
19983
20711
|
init_cjs_shims();
|
|
@@ -23983,7 +24711,7 @@ var import_utils5 = require("@apollo/utils.keyvaluecache");
|
|
|
23983
24711
|
var import_body_parser = __toESM(require("body-parser"), 1);
|
|
23984
24712
|
var import_crypto_js7 = require("crypto-js");
|
|
23985
24713
|
var import_openai = require("openai");
|
|
23986
|
-
var
|
|
24714
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
23987
24715
|
var import_node_crypto15 = require("crypto");
|
|
23988
24716
|
var import_api2 = require("@opentelemetry/api");
|
|
23989
24717
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
@@ -25320,7 +26048,7 @@ var REQUEST_SIZE_LIMIT = "50mb";
|
|
|
25320
26048
|
var getExuluVersionNumber = async () => {
|
|
25321
26049
|
try {
|
|
25322
26050
|
const path4 = process.cwd();
|
|
25323
|
-
const packageJson =
|
|
26051
|
+
const packageJson = import_fs4.default.readFileSync(path4 + "/package.json", "utf8");
|
|
25324
26052
|
const packageData = JSON.parse(packageJson);
|
|
25325
26053
|
const exuluVersion = packageData.dependencies["@exulu/backend"];
|
|
25326
26054
|
console.log(`[EXULU] Installed exulu-backend version: ${exuluVersion}`);
|
|
@@ -25803,6 +26531,7 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
|
|
|
25803
26531
|
|
|
25804
26532
|
${customInstructions}` : agent.instructions;
|
|
25805
26533
|
if (headers.session) markStreamActive(headers.session);
|
|
26534
|
+
const turnStartedAt = Date.now();
|
|
25806
26535
|
let result;
|
|
25807
26536
|
try {
|
|
25808
26537
|
result = await generateStream({
|
|
@@ -25844,13 +26573,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
25844
26573
|
};
|
|
25845
26574
|
}
|
|
25846
26575
|
if (part.type === "finish") {
|
|
25847
|
-
return {
|
|
25848
|
-
totalTokens: part.totalUsage.totalTokens,
|
|
25849
|
-
reasoningTokens: part.totalUsage.reasoningTokens,
|
|
25850
|
-
inputTokens: part.totalUsage.inputTokens,
|
|
25851
|
-
outputTokens: part.totalUsage.outputTokens,
|
|
25852
|
-
cachedInputTokens: part.totalUsage.cachedInputTokens
|
|
25853
|
-
};
|
|
26576
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
|
|
25854
26577
|
}
|
|
25855
26578
|
return void 0;
|
|
25856
26579
|
},
|
|
@@ -30301,210 +31024,7 @@ init_singleton();
|
|
|
30301
31024
|
init_entitlements();
|
|
30302
31025
|
init_system_dependencies();
|
|
30303
31026
|
init_supervisor();
|
|
30304
|
-
|
|
30305
|
-
// src/utils/python-setup.ts
|
|
30306
|
-
init_cjs_shims();
|
|
30307
|
-
var import_child_process = require("child_process");
|
|
30308
|
-
var import_util = require("util");
|
|
30309
|
-
var import_path3 = require("path");
|
|
30310
|
-
var import_fs4 = require("fs");
|
|
30311
|
-
var import_url = require("url");
|
|
30312
|
-
var execAsync4 = (0, import_util.promisify)(import_child_process.exec);
|
|
30313
|
-
function getPackageRoot() {
|
|
30314
|
-
const currentFile = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
30315
|
-
let currentDir = (0, import_path3.dirname)(currentFile);
|
|
30316
|
-
let attempts = 0;
|
|
30317
|
-
const maxAttempts = 10;
|
|
30318
|
-
while (attempts < maxAttempts) {
|
|
30319
|
-
const packageJsonPath = (0, import_path3.join)(currentDir, "package.json");
|
|
30320
|
-
if ((0, import_fs4.existsSync)(packageJsonPath)) {
|
|
30321
|
-
try {
|
|
30322
|
-
const packageJson = JSON.parse((0, import_fs4.readFileSync)(packageJsonPath, "utf-8"));
|
|
30323
|
-
if (packageJson.name === "@exulu/backend") {
|
|
30324
|
-
return currentDir;
|
|
30325
|
-
}
|
|
30326
|
-
} catch {
|
|
30327
|
-
}
|
|
30328
|
-
}
|
|
30329
|
-
const parentDir = (0, import_path3.resolve)(currentDir, "..");
|
|
30330
|
-
if (parentDir === currentDir) {
|
|
30331
|
-
break;
|
|
30332
|
-
}
|
|
30333
|
-
currentDir = parentDir;
|
|
30334
|
-
attempts++;
|
|
30335
|
-
}
|
|
30336
|
-
const fallback = (0, import_path3.resolve)((0, import_path3.dirname)((0, import_url.fileURLToPath)(importMetaUrl)), "../..");
|
|
30337
|
-
return fallback;
|
|
30338
|
-
}
|
|
30339
|
-
function getSetupScriptPath(packageRoot) {
|
|
30340
|
-
return (0, import_path3.resolve)(packageRoot, "ee/python/setup.sh");
|
|
30341
|
-
}
|
|
30342
|
-
function getVenvPath(packageRoot) {
|
|
30343
|
-
return (0, import_path3.resolve)(packageRoot, "ee/python/.venv");
|
|
30344
|
-
}
|
|
30345
|
-
function isPythonEnvironmentSetup(packageRoot) {
|
|
30346
|
-
const root = packageRoot ?? getPackageRoot();
|
|
30347
|
-
const venvPath = getVenvPath(root);
|
|
30348
|
-
const pythonPath = (0, import_path3.join)(venvPath, "bin", "python");
|
|
30349
|
-
return (0, import_fs4.existsSync)(venvPath) && (0, import_fs4.existsSync)(pythonPath);
|
|
30350
|
-
}
|
|
30351
|
-
async function setupPythonEnvironment(options = {}) {
|
|
30352
|
-
const {
|
|
30353
|
-
packageRoot = getPackageRoot(),
|
|
30354
|
-
force = false,
|
|
30355
|
-
verbose = false,
|
|
30356
|
-
timeout = 6e5
|
|
30357
|
-
// 10 minutes
|
|
30358
|
-
} = options;
|
|
30359
|
-
if (!force && isPythonEnvironmentSetup(packageRoot)) {
|
|
30360
|
-
if (verbose) {
|
|
30361
|
-
console.log("\u2713 Python environment already set up");
|
|
30362
|
-
}
|
|
30363
|
-
return {
|
|
30364
|
-
success: true,
|
|
30365
|
-
message: "Python environment already exists",
|
|
30366
|
-
alreadyExists: true
|
|
30367
|
-
};
|
|
30368
|
-
}
|
|
30369
|
-
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
30370
|
-
if (!(0, import_fs4.existsSync)(setupScriptPath)) {
|
|
30371
|
-
return {
|
|
30372
|
-
success: false,
|
|
30373
|
-
message: `Setup script not found at: ${setupScriptPath}`,
|
|
30374
|
-
alreadyExists: false
|
|
30375
|
-
};
|
|
30376
|
-
}
|
|
30377
|
-
try {
|
|
30378
|
-
if (verbose) {
|
|
30379
|
-
console.log("Setting up Python environment...");
|
|
30380
|
-
}
|
|
30381
|
-
const { stdout, stderr } = await execAsync4(`bash "${setupScriptPath}"`, {
|
|
30382
|
-
cwd: packageRoot,
|
|
30383
|
-
timeout,
|
|
30384
|
-
env: {
|
|
30385
|
-
...process.env,
|
|
30386
|
-
// Ensure script can write to the directory
|
|
30387
|
-
PYTHONDONTWRITEBYTECODE: "1"
|
|
30388
|
-
},
|
|
30389
|
-
maxBuffer: 10 * 1024 * 1024
|
|
30390
|
-
// 10MB buffer
|
|
30391
|
-
});
|
|
30392
|
-
const output = stdout + stderr;
|
|
30393
|
-
const versionMatch = output.match(/Python (\d+\.\d+\.\d+)/);
|
|
30394
|
-
const pythonVersion = versionMatch ? versionMatch[1] : void 0;
|
|
30395
|
-
if (verbose) {
|
|
30396
|
-
console.log(output);
|
|
30397
|
-
}
|
|
30398
|
-
return {
|
|
30399
|
-
success: true,
|
|
30400
|
-
message: "Python environment set up successfully",
|
|
30401
|
-
alreadyExists: false,
|
|
30402
|
-
pythonVersion,
|
|
30403
|
-
output
|
|
30404
|
-
};
|
|
30405
|
-
} catch (error) {
|
|
30406
|
-
const errorOutput = error.stdout + error.stderr;
|
|
30407
|
-
return {
|
|
30408
|
-
success: false,
|
|
30409
|
-
message: `Setup failed: ${error.message}`,
|
|
30410
|
-
alreadyExists: false,
|
|
30411
|
-
output: errorOutput
|
|
30412
|
-
};
|
|
30413
|
-
}
|
|
30414
|
-
}
|
|
30415
|
-
function getPythonSetupInstructions() {
|
|
30416
|
-
return `
|
|
30417
|
-
Python environment not set up. Please run one of the following commands:
|
|
30418
|
-
|
|
30419
|
-
Option 1 (Automatic):
|
|
30420
|
-
import { setupPythonEnvironment } from '@exulu/backend';
|
|
30421
|
-
await setupPythonEnvironment();
|
|
30422
|
-
|
|
30423
|
-
Option 2 (Manual - for package consumers):
|
|
30424
|
-
npx @exulu/backend setup-python
|
|
30425
|
-
|
|
30426
|
-
Option 3 (Manual - for contributors):
|
|
30427
|
-
npm run python:setup
|
|
30428
|
-
|
|
30429
|
-
These commands will automatically create a Python virtual environment (.venv)
|
|
30430
|
-
in the @exulu/backend package and install all required dependencies.
|
|
30431
|
-
|
|
30432
|
-
Requirements:
|
|
30433
|
-
- Python 3.10 or higher must be installed
|
|
30434
|
-
- pip must be available
|
|
30435
|
-
- venv module must be available (for creating virtual environments)
|
|
30436
|
-
|
|
30437
|
-
If Python dependencies are not installed, install them first, then run one of the commands above:
|
|
30438
|
-
- macOS: brew install python@3.12
|
|
30439
|
-
- Ubuntu/Debian: sudo apt-get install python3.12 python3-pip python3-venv
|
|
30440
|
-
- Alpine Linux: apk add python3 py3-pip python3-dev
|
|
30441
|
-
- Windows: Download from https://www.python.org/downloads/
|
|
30442
|
-
|
|
30443
|
-
Note: In Docker containers, ensure you install all three components:
|
|
30444
|
-
Ubuntu/Debian: apt-get install -y python3 python3-pip python3-venv
|
|
30445
|
-
Alpine: apk add python3 py3-pip python3-dev
|
|
30446
|
-
`.trim();
|
|
30447
|
-
}
|
|
30448
|
-
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
30449
|
-
const root = packageRoot ?? getPackageRoot();
|
|
30450
|
-
const venvPath = getVenvPath(root);
|
|
30451
|
-
const pythonPath = (0, import_path3.join)(venvPath, "bin", "python");
|
|
30452
|
-
if (!(0, import_fs4.existsSync)(venvPath)) {
|
|
30453
|
-
return {
|
|
30454
|
-
valid: false,
|
|
30455
|
-
message: getPythonSetupInstructions()
|
|
30456
|
-
};
|
|
30457
|
-
}
|
|
30458
|
-
if (!(0, import_fs4.existsSync)(pythonPath)) {
|
|
30459
|
-
return {
|
|
30460
|
-
valid: false,
|
|
30461
|
-
message: "Python virtual environment is corrupted. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
30462
|
-
};
|
|
30463
|
-
}
|
|
30464
|
-
try {
|
|
30465
|
-
await execAsync4(`"${pythonPath}" --version`, { cwd: root });
|
|
30466
|
-
} catch {
|
|
30467
|
-
return {
|
|
30468
|
-
valid: false,
|
|
30469
|
-
message: "Python executable is not working. Please run:\n await setupPythonEnvironment({ force: true })"
|
|
30470
|
-
};
|
|
30471
|
-
}
|
|
30472
|
-
if (checkPackages) {
|
|
30473
|
-
const criticalPackages = ["docling", "transformers"];
|
|
30474
|
-
const missingPackages = [];
|
|
30475
|
-
for (const pkg of criticalPackages) {
|
|
30476
|
-
try {
|
|
30477
|
-
await execAsync4(`"${pythonPath}" -c "import ${pkg}"`, {
|
|
30478
|
-
cwd: root,
|
|
30479
|
-
timeout: 1e4
|
|
30480
|
-
// 10 second timeout per import check
|
|
30481
|
-
});
|
|
30482
|
-
} catch {
|
|
30483
|
-
missingPackages.push(pkg);
|
|
30484
|
-
}
|
|
30485
|
-
}
|
|
30486
|
-
if (missingPackages.length > 0) {
|
|
30487
|
-
return {
|
|
30488
|
-
valid: false,
|
|
30489
|
-
message: `Python environment exists but required packages are not installed: ${missingPackages.join(", ")}
|
|
30490
|
-
|
|
30491
|
-
This usually happens when:
|
|
30492
|
-
1. The .venv folder was copied but dependencies were not installed
|
|
30493
|
-
2. The package was installed via npm but setup script was not run
|
|
30494
|
-
|
|
30495
|
-
Please run:
|
|
30496
|
-
await setupPythonEnvironment({ force: true })
|
|
30497
|
-
|
|
30498
|
-
Or manually run the setup script:
|
|
30499
|
-
bash ` + getSetupScriptPath(root)
|
|
30500
|
-
};
|
|
30501
|
-
}
|
|
30502
|
-
}
|
|
30503
|
-
return {
|
|
30504
|
-
valid: true,
|
|
30505
|
-
message: "Python environment is valid"
|
|
30506
|
-
};
|
|
30507
|
-
}
|
|
31027
|
+
init_python_setup();
|
|
30508
31028
|
|
|
30509
31029
|
// src/templates/contexts/index.ts
|
|
30510
31030
|
init_cjs_shims();
|
|
@@ -32493,6 +33013,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
32493
33013
|
};
|
|
32494
33014
|
|
|
32495
33015
|
// src/postgres/init-litellm-db.ts
|
|
33016
|
+
init_python_setup();
|
|
32496
33017
|
var initLitellmDb = async () => {
|
|
32497
33018
|
await initLiteLLMDatabase(getPackageRoot());
|
|
32498
33019
|
console.log("[EXULU] LiteLLM database initialized.");
|
|
@@ -33027,6 +33548,9 @@ var MarkdownChunker = class {
|
|
|
33027
33548
|
}
|
|
33028
33549
|
};
|
|
33029
33550
|
|
|
33551
|
+
// src/index.ts
|
|
33552
|
+
init_python_setup();
|
|
33553
|
+
|
|
33030
33554
|
// ee/python/documents/processing/doc_processor.ts
|
|
33031
33555
|
init_cjs_shims();
|
|
33032
33556
|
var fs4 = __toESM(require("fs"), 1);
|
|
@@ -33049,6 +33573,7 @@ var import_util3 = require("util");
|
|
|
33049
33573
|
var import_path4 = require("path");
|
|
33050
33574
|
var import_fs5 = require("fs");
|
|
33051
33575
|
var import_url2 = require("url");
|
|
33576
|
+
init_python_setup();
|
|
33052
33577
|
var execAsync5 = (0, import_util3.promisify)(import_child_process2.exec);
|
|
33053
33578
|
function getPackageRoot2() {
|
|
33054
33579
|
const currentFile = (0, import_url2.fileURLToPath)(importMetaUrl);
|
|
@@ -33570,7 +34095,7 @@ function reconstructTableHeaders(document2, validationResults, verbose = false)
|
|
|
33570
34095
|
}
|
|
33571
34096
|
}
|
|
33572
34097
|
async function validateWithVLM(document2, model, verbose = false, concurrency = 10) {
|
|
33573
|
-
console.log(`[EXULU] Starting VLM validation for
|
|
34098
|
+
console.log(`[EXULU] Starting VLM validation for processor output, ${document2.length} pages...`);
|
|
33574
34099
|
console.log(`[EXULU] Concurrency limit: ${concurrency}`);
|
|
33575
34100
|
const limit = (0, import_p_limit.default)(concurrency);
|
|
33576
34101
|
const validationResults = /* @__PURE__ */ new Map();
|
|
@@ -33705,48 +34230,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
|
|
|
33705
34230
|
async function processPdf(buffer, paths, config, verbose = false) {
|
|
33706
34231
|
try {
|
|
33707
34232
|
let json = [];
|
|
33708
|
-
if (config?.processor.name === "
|
|
33709
|
-
console.log(`[EXULU] Validating Python environment...`);
|
|
33710
|
-
const validation = await validatePythonEnvironment(void 0, true);
|
|
33711
|
-
if (!validation.valid) {
|
|
33712
|
-
console.log(`[EXULU] Python environment not ready, setting up automatically...`);
|
|
33713
|
-
console.log(`[EXULU] Reason: ${validation.message}`);
|
|
33714
|
-
const setupResult = await setupPythonEnvironment({
|
|
33715
|
-
verbose: true,
|
|
33716
|
-
force: false
|
|
33717
|
-
// Only setup if not already done
|
|
33718
|
-
});
|
|
33719
|
-
if (!setupResult.success) {
|
|
33720
|
-
throw new Error(`Failed to setup Python environment: ${setupResult.message}
|
|
33721
|
-
|
|
33722
|
-
${setupResult.output || ""}`);
|
|
33723
|
-
}
|
|
33724
|
-
console.log(`[EXULU] Python environment setup completed successfully`);
|
|
33725
|
-
} else {
|
|
33726
|
-
console.log(`[EXULU] Python environment is valid`);
|
|
33727
|
-
}
|
|
33728
|
-
console.log(`[EXULU] Processing document with document_to_markdown.py`);
|
|
33729
|
-
const result = await executePythonScript({
|
|
33730
|
-
scriptPath: "ee/python/documents/processing/document_to_markdown.py",
|
|
33731
|
-
args: [
|
|
33732
|
-
paths.source,
|
|
33733
|
-
"-o",
|
|
33734
|
-
paths.json,
|
|
33735
|
-
"--images-dir",
|
|
33736
|
-
paths.images
|
|
33737
|
-
],
|
|
33738
|
-
timeout: 30 * 60 * 1e3
|
|
33739
|
-
// 30 minutes for large documents
|
|
33740
|
-
});
|
|
33741
|
-
if (result.stderr) {
|
|
33742
|
-
console.log("Processing info:", result.stderr.trim());
|
|
33743
|
-
}
|
|
33744
|
-
if (!result.success) {
|
|
33745
|
-
throw new Error(`Document processing failed: ${result.stderr}`);
|
|
33746
|
-
}
|
|
33747
|
-
const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
|
|
33748
|
-
json = JSON.parse(jsonContent);
|
|
33749
|
-
} else if (config?.processor.name === "officeparser") {
|
|
34233
|
+
if (config?.processor.name === "officeparser") {
|
|
33750
34234
|
const text = await (0, import_officeparser3.parseOfficeAsync)(buffer, {
|
|
33751
34235
|
outputErrorToConsole: false,
|
|
33752
34236
|
newlineDelimiter: "\n"
|
|
@@ -33837,14 +34321,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
|
|
|
33837
34321
|
image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
|
|
33838
34322
|
}));
|
|
33839
34323
|
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
34324
|
+
} else {
|
|
34325
|
+
const configured = String(config?.processor?.name ?? "");
|
|
34326
|
+
throw new Error(
|
|
34327
|
+
configured === "" ? "[EXULU] No document processor configured. Set processor.name to one of: mistral, liteparse, officeparser." : `[EXULU] Unknown document processor "${configured}". Supported processors are: mistral, liteparse, officeparser.` + (configured === "docling" ? ' The "docling" processor was removed: it depended on PyMuPDF, which is AGPL-licensed. Use "mistral" for PDF OCR.' : "")
|
|
34328
|
+
);
|
|
33840
34329
|
}
|
|
33841
34330
|
console.log(`[EXULU]
|
|
33842
34331
|
\u2713 Document processing completed successfully`);
|
|
33843
34332
|
console.log(`[EXULU] Total pages: ${json.length}`);
|
|
33844
34333
|
console.log(`[EXULU] Output file: ${paths.json}`);
|
|
33845
|
-
if (config?.vlm?.model) {
|
|
33846
|
-
console.error("[EXULU] VLM validation is only supported when docling is enabled, skipping validation.");
|
|
33847
|
-
}
|
|
33848
34334
|
const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
|
|
33849
34335
|
if (vlmModel && json.length > 0) {
|
|
33850
34336
|
json = await validateWithVLM(
|
|
@@ -33962,9 +34448,6 @@ async function documentProcessor({
|
|
|
33962
34448
|
} = await loadFile(file, name, tempDir);
|
|
33963
34449
|
let supportedTypes = [];
|
|
33964
34450
|
switch (config?.processor.name) {
|
|
33965
|
-
case "docling":
|
|
33966
|
-
supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
|
|
33967
|
-
break;
|
|
33968
34451
|
case "officeparser":
|
|
33969
34452
|
supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
|
|
33970
34453
|
break;
|