@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.
Files changed (41) hide show
  1. package/dist/{chunk-T6JVFT7L.js → chunk-QMN6MVHQ.js} +6 -1
  2. package/dist/{chunk-BNTL6LYY.js → chunk-RBEWHG7I.js} +404 -59
  3. package/dist/cli/start-whisper.js +1 -1
  4. package/dist/{convert-exulu-tools-to-ai-sdk-tools-UQSLJDXE.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
  5. package/dist/index.cjs +957 -474
  6. package/dist/index.d.cts +3 -6
  7. package/dist/index.d.ts +3 -6
  8. package/dist/index.js +258 -182
  9. package/dist/python-setup-DRJ3QX5F.js +17 -0
  10. package/ee/LICENSE.md +2 -2
  11. package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
  12. package/ee/agentic-retrieval/pipeline/config.ts +15 -0
  13. package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
  14. package/ee/agentic-retrieval/pipeline/index.ts +67 -13
  15. package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
  16. package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
  17. package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
  18. package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
  19. package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
  20. package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
  21. package/ee/agentic-retrieval/pipeline/search.ts +9 -6
  22. package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
  23. package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
  24. package/ee/agentic-retrieval/pipeline/types.ts +2 -0
  25. package/ee/invoke-skills/artifact-filter.test.ts +49 -0
  26. package/ee/invoke-skills/artifact-filter.ts +38 -0
  27. package/ee/invoke-skills/create-sandbox.ts +56 -4
  28. package/ee/python/documents/processing/README.md +2 -3
  29. package/ee/python/documents/processing/doc_processor.ts +21 -61
  30. package/ee/python/documents/processing/split_pdf.py +25 -30
  31. package/ee/python/documents/processing/tests/__init__.py +0 -0
  32. package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
  33. package/ee/python/requirements.txt +17 -2
  34. package/ee/python/setup.sh +40 -1
  35. package/ee/python/transcription/pipeline.py +109 -15
  36. package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
  37. package/ee/workers.ts +2 -7
  38. package/license.md +2 -2
  39. package/package.json +3 -4
  40. package/scripts/postinstall.cjs +52 -1
  41. 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 && !dirExisted) {
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 execAsync2(wrapped, {
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 withUrls = artifacts.filter((a) => a.url);
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, execAsync2, EXEC_MAX_BUFFER, sandboxProbePromise, SANDBOX_FALLBACK_INSTRUCTIONS, degradedModeLogged, sandboxCache;
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
- execAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
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 generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
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 generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
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 execAsync3(
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, execAsync3, CACHE_ROOT, CACHE_IN, CACHE_OUT, inFlight, PreviewRenderError;
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
- execAsync3 = (0, import_node_util4.promisify)(import_node_child_process5.exec);
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 generalPrefix = uploads.s3prefix ? `${uploads.s3prefix.replace(/\/$/, "")}/` : "";
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, import_path, normalizePrefix, hasAllS3Fields, resolveAuditConfig;
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
- import_path = __toESM(require("path"), 1);
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 ?? import_path.default.join(import_os.default.tmpdir(), "exulu-audit-spool"),
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, import_fs, import_path2, createFsSpoolStore, pad, AuditSink;
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
- import_fs = require("fs");
5474
- import_path2 = __toESM(require("path"), 1);
5798
+ import_fs2 = require("fs");
5799
+ import_path3 = __toESM(require("path"), 1);
5475
5800
  createFsSpoolStore = (dir) => ({
5476
5801
  write: async (name, body) => {
5477
- await import_fs.promises.mkdir(dir, { recursive: true });
5478
- await import_fs.promises.writeFile(import_path2.default.join(dir, name), body, "utf8");
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 import_fs.promises.readdir(dir)).filter((f) => f.endsWith(".ndjson"));
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) => import_fs.promises.readFile(import_path2.default.join(dir, name), "utf8"),
5812
+ read: async (name) => import_fs2.promises.readFile(import_path3.default.join(dir, name), "utf8"),
5488
5813
  remove: async (name) => {
5489
- await import_fs.promises.rm(import_path2.default.join(dir, name), { force: true });
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 [docPageRaw, explicitKBRaw] = await Promise.all([
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: "hybridSearch", cutoffs: void 0, limit: 50 },
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
- const { output: output_relevant_memory } = await microCall({
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 (ctxId) => {
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 [memResult, routResult] = await Promise.all([
8575
- runMemoryPhase({
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 resolveIdentifierPins({
8651
- question: updatedQuestion,
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 (queryEmbedding && queryEmbedding.length) {
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
- vectorStr = `ARRAY[${vector.join(",")}]`;
12807
- vectorExpr = `${vectorStr}::vector`;
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, websearch_to_tsquery('${lang}', ?))`).join(", ");
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 @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
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, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
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, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
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
- const aIndex = Number(a.chunk_index);
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, 300)}`;
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
- providerOptions: {
17895
- openai: {
17896
- reasoningSummary: "auto"
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
- onError: (error) => {
17901
- console.error("[EXULU] chat stream error.", error);
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 import_fs2 = require("fs");
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 import_fs3 = __toESM(require("fs"), 1);
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 = import_fs3.default.readFileSync(path4 + "/package.json", "utf8");
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 docling output, ${document2.length} pages...`);
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 === "docling") {
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;