@exulu/backend 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/setup-python.cjs +18 -14
- package/dist/{chunk-RBEWHG7I.js → chunk-D4PCOOL5.js} +65 -9
- package/dist/{chunk-QMN6MVHQ.js → chunk-JIZCKSJO.js} +9 -16
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js → convert-exulu-tools-to-ai-sdk-tools-AHRL4TN4.js} +1 -1
- package/dist/index.cjs +209 -46
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +123 -12
- package/dist/{python-setup-DRJ3QX5F.js → python-setup-UYSNAW5V.js} +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +13 -7
package/bin/setup-python.cjs
CHANGED
|
@@ -98,20 +98,24 @@ async function main() {
|
|
|
98
98
|
console.log(`${colors.blue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}`);
|
|
99
99
|
console.log('');
|
|
100
100
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
console.log(
|
|
101
|
+
// We deliberately do NOT skip running setup.sh just because the venv
|
|
102
|
+
// directory and a `python` binary already exist (pythonEnvironmentExists()
|
|
103
|
+
// only checks that much, not that requirements.txt is fully installed).
|
|
104
|
+
// ee/python/setup.sh is itself idempotent — it only skips *recreating* the
|
|
105
|
+
// venv, never skips `pip install -r requirements.txt` — so always invoking
|
|
106
|
+
// it is cheap when nothing is missing (a few seconds) and is the only way
|
|
107
|
+
// to guarantee a venv created before a new requirement was added (e.g.
|
|
108
|
+
// python-docx, 2026-09-11) actually gets it installed. This used to return
|
|
109
|
+
// early here whenever the venv folder was merely present, which silently
|
|
110
|
+
// left newly-added dependencies uninstalled across any deploy that reused
|
|
111
|
+
// an older venv — see the docx-manipulation skill incident on dx-algi,
|
|
112
|
+
// 2026-09-21.
|
|
113
|
+
if (pythonEnvironmentExists()) {
|
|
114
|
+
console.log(
|
|
115
|
+
force
|
|
116
|
+
? `${colors.yellow}⚠${colors.reset} Rebuilding Python environment (--force flag detected)`
|
|
117
|
+
: `${colors.blue}ℹ${colors.reset} Python environment directory already exists — verifying and installing any missing dependencies...`,
|
|
118
|
+
);
|
|
115
119
|
console.log('');
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -1716,7 +1716,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1716
1716
|
if (!agent) {
|
|
1717
1717
|
throw new Error("Agent not found.");
|
|
1718
1718
|
}
|
|
1719
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1719
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-AHRL4TN4.js");
|
|
1720
1720
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1721
1721
|
[this],
|
|
1722
1722
|
[],
|
|
@@ -6308,7 +6308,7 @@ async function downloadKeyIntoSandbox(opts) {
|
|
|
6308
6308
|
}
|
|
6309
6309
|
async function resolvePythonVenvPath() {
|
|
6310
6310
|
try {
|
|
6311
|
-
const { getPythonVenvPath } = await import("./python-setup-
|
|
6311
|
+
const { getPythonVenvPath } = await import("./python-setup-UYSNAW5V.js");
|
|
6312
6312
|
return getPythonVenvPath();
|
|
6313
6313
|
} catch (err) {
|
|
6314
6314
|
console.warn("[SKILLS] Could not resolve the Python venv for the session sandbox; skill scripts fall back to the system python.", err);
|
|
@@ -6961,11 +6961,50 @@ import { parseOfficeAsync } from "officeparser";
|
|
|
6961
6961
|
// src/templates/tools/document-render-helpers.ts
|
|
6962
6962
|
import { execFile } from "child_process";
|
|
6963
6963
|
import { promisify as promisify3 } from "util";
|
|
6964
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6964
6965
|
import { mkdtemp, readdir as readdir2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
6965
6966
|
import { tmpdir } from "os";
|
|
6966
6967
|
import { join as join3 } from "path";
|
|
6967
6968
|
var execFileAsync = promisify3(execFile);
|
|
6968
6969
|
var MAX_STDOUT_BYTES = 64 * 1024 * 1024;
|
|
6970
|
+
var LEGACY_OFFICE_CONVERSION_TARGETS = {
|
|
6971
|
+
".doc": "docx",
|
|
6972
|
+
".xls": "xlsx",
|
|
6973
|
+
".ppt": "pptx",
|
|
6974
|
+
".rtf": "docx"
|
|
6975
|
+
};
|
|
6976
|
+
function isLegacyOfficeFormat(ext) {
|
|
6977
|
+
return ext in LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
6978
|
+
}
|
|
6979
|
+
async function convertLegacyOfficeToModern(bytes, ext) {
|
|
6980
|
+
const target = LEGACY_OFFICE_CONVERSION_TARGETS[ext];
|
|
6981
|
+
if (!target) {
|
|
6982
|
+
throw new Error(`No modern-format conversion target registered for "${ext}"`);
|
|
6983
|
+
}
|
|
6984
|
+
const dir = await mkdtemp(join3(tmpdir(), "exulu-officeconv-"));
|
|
6985
|
+
try {
|
|
6986
|
+
const inputPath = join3(dir, `input${ext}`);
|
|
6987
|
+
await writeFile2(inputPath, bytes);
|
|
6988
|
+
try {
|
|
6989
|
+
await execFileAsync(
|
|
6990
|
+
"soffice",
|
|
6991
|
+
["--headless", "--convert-to", target, inputPath, "--outdir", dir],
|
|
6992
|
+
{ timeout: 6e4, maxBuffer: MAX_STDOUT_BYTES }
|
|
6993
|
+
);
|
|
6994
|
+
} catch (err) {
|
|
6995
|
+
throw new Error(
|
|
6996
|
+
`LibreOffice could not convert the legacy "${ext}" file to ${target}: ` + (err instanceof Error ? err.message : String(err))
|
|
6997
|
+
);
|
|
6998
|
+
}
|
|
6999
|
+
const outputPath = join3(dir, `input.${target}`);
|
|
7000
|
+
if (!existsSync4(outputPath)) {
|
|
7001
|
+
throw new Error(`LibreOffice produced no output converting "${ext}" to ${target}`);
|
|
7002
|
+
}
|
|
7003
|
+
return await readFile(outputPath);
|
|
7004
|
+
} finally {
|
|
7005
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
6969
7008
|
async function pdfToText(pdf) {
|
|
6970
7009
|
const dir = await mkdtemp(join3(tmpdir(), "exulu-parse-"));
|
|
6971
7010
|
try {
|
|
@@ -7032,6 +7071,18 @@ var OFFICE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
7032
7071
|
".rtf"
|
|
7033
7072
|
]);
|
|
7034
7073
|
var pagesPattern = /^(\d+)(?:-(\d+))?$/;
|
|
7074
|
+
var BINARY_DOCUMENT_EXTENSION_PATTERN = new RegExp(
|
|
7075
|
+
`([^\\s"'\`]+\\.(?:pdf|${[...OFFICE_EXTENSIONS].map((ext) => ext.slice(1)).join("|")}))\\b`,
|
|
7076
|
+
"i"
|
|
7077
|
+
);
|
|
7078
|
+
function binaryDocumentBashHint(command, stdout, stderr) {
|
|
7079
|
+
const match = command.match(BINARY_DOCUMENT_EXTENSION_PATTERN);
|
|
7080
|
+
if (!match) return void 0;
|
|
7081
|
+
const silent = !stdout.trim() && !stderr.trim();
|
|
7082
|
+
if (!silent && !looksLikeGarbledTextLayer(stdout)) return void 0;
|
|
7083
|
+
const [, file] = match;
|
|
7084
|
+
return `Note: "${file}" is a binary document \u2014 grep/cat/text tools cannot read the text inside it, so a silent or garbled result does not mean the content isn't there. Use parse_document to extract its text first, then search or read within that extracted text instead of the original file.`;
|
|
7085
|
+
}
|
|
7035
7086
|
var createParseDocumentTool = ({
|
|
7036
7087
|
sessionID,
|
|
7037
7088
|
user,
|
|
@@ -7101,7 +7152,8 @@ var createParseDocumentTool = ({
|
|
|
7101
7152
|
fullText = pageTexts.map((text, i) => ({ page: i + 1, text })).filter(({ page }) => page >= range[0] && page <= range[1]).map(({ page, text }) => `--- page ${page} ---
|
|
7102
7153
|
${text.trim()}`).join("\n");
|
|
7103
7154
|
} else {
|
|
7104
|
-
const
|
|
7155
|
+
const officeBytes = isLegacyOfficeFormat(ext) ? await convertLegacyOfficeToModern(bytes, ext) : bytes;
|
|
7156
|
+
const extracted = await parseOfficeAsync(officeBytes, {
|
|
7105
7157
|
outputErrorToConsole: false,
|
|
7106
7158
|
newlineDelimiter: "\n"
|
|
7107
7159
|
});
|
|
@@ -7156,7 +7208,7 @@ import { extname as extname3 } from "path";
|
|
|
7156
7208
|
|
|
7157
7209
|
// src/sessions/pdf-preview-cache.ts
|
|
7158
7210
|
import { exec as exec3 } from "child_process";
|
|
7159
|
-
import { existsSync as
|
|
7211
|
+
import { existsSync as existsSync5 } from "fs";
|
|
7160
7212
|
import { mkdir as mkdir2, readFile as readFile2, rename, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
7161
7213
|
import { extname as extname2, join as join4 } from "path";
|
|
7162
7214
|
import { promisify as promisify4 } from "util";
|
|
@@ -7181,7 +7233,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
7181
7233
|
throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
|
|
7182
7234
|
}
|
|
7183
7235
|
const cachedPath = join4(CACHE_ROOT, `${safeEtag}.pdf`);
|
|
7184
|
-
if (
|
|
7236
|
+
if (existsSync5(cachedPath)) {
|
|
7185
7237
|
return readFile2(cachedPath);
|
|
7186
7238
|
}
|
|
7187
7239
|
const existing = inFlight.get(safeEtag);
|
|
@@ -7206,7 +7258,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
7206
7258
|
`LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
|
|
7207
7259
|
);
|
|
7208
7260
|
}
|
|
7209
|
-
if (!
|
|
7261
|
+
if (!existsSync5(outputPath)) {
|
|
7210
7262
|
throw new PreviewRenderError(
|
|
7211
7263
|
`LibreOffice produced no output for ${sourceKey} (etag ${etag})`
|
|
7212
7264
|
);
|
|
@@ -8230,13 +8282,17 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
8230
8282
|
const origExecute = sharedSessionSandbox.tools.bash.execute;
|
|
8231
8283
|
if (!origExecute) throw new Error("bash execute is undefined");
|
|
8232
8284
|
const result = await origExecute(args, opts);
|
|
8285
|
+
const rawStdout = typeof result?.stdout === "string" ? result.stdout : "";
|
|
8286
|
+
const rawStderr = typeof result?.stderr === "string" ? result.stderr : "";
|
|
8287
|
+
const hint = binaryDocumentBashHint(String(args?.command ?? ""), rawStdout, rawStderr);
|
|
8288
|
+
const stderrWithHint = hint ? [rawStderr, hint].filter(Boolean).join("\n\n") : rawStderr;
|
|
8233
8289
|
return {
|
|
8234
8290
|
...result,
|
|
8235
8291
|
...typeof result?.stdout === "string" && {
|
|
8236
|
-
stdout: truncateToolOutput(
|
|
8292
|
+
stdout: truncateToolOutput(rawStdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
|
|
8237
8293
|
},
|
|
8238
|
-
...typeof result?.stderr === "string" && {
|
|
8239
|
-
stderr: truncateToolOutput(
|
|
8294
|
+
...(typeof result?.stderr === "string" || hint) && {
|
|
8295
|
+
stderr: truncateToolOutput(stderrWithHint, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
|
|
8240
8296
|
}
|
|
8241
8297
|
};
|
|
8242
8298
|
}
|
|
@@ -57,15 +57,11 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
57
57
|
timeout = 6e5
|
|
58
58
|
// 10 minutes
|
|
59
59
|
} = options;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
success: true,
|
|
66
|
-
message: "Python environment already exists",
|
|
67
|
-
alreadyExists: true
|
|
68
|
-
};
|
|
60
|
+
const alreadyExisted = isPythonEnvironmentSetup(packageRoot);
|
|
61
|
+
if (verbose) {
|
|
62
|
+
console.log(
|
|
63
|
+
alreadyExisted ? "Python environment already exists \u2014 verifying and installing any missing dependencies..." : "Setting up Python environment..."
|
|
64
|
+
);
|
|
69
65
|
}
|
|
70
66
|
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
71
67
|
if (!existsSync(setupScriptPath)) {
|
|
@@ -76,9 +72,6 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
76
72
|
};
|
|
77
73
|
}
|
|
78
74
|
try {
|
|
79
|
-
if (verbose) {
|
|
80
|
-
console.log("Setting up Python environment...");
|
|
81
|
-
}
|
|
82
75
|
const { stdout, stderr } = await execAsync(`bash "${setupScriptPath}"`, {
|
|
83
76
|
cwd: packageRoot,
|
|
84
77
|
timeout,
|
|
@@ -98,8 +91,8 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
98
91
|
}
|
|
99
92
|
return {
|
|
100
93
|
success: true,
|
|
101
|
-
message: "Python environment set up successfully",
|
|
102
|
-
alreadyExists:
|
|
94
|
+
message: alreadyExisted ? "Python environment verified (dependencies were already installed or have been updated)" : "Python environment set up successfully",
|
|
95
|
+
alreadyExists: alreadyExisted,
|
|
103
96
|
pythonVersion,
|
|
104
97
|
output
|
|
105
98
|
};
|
|
@@ -108,7 +101,7 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
108
101
|
return {
|
|
109
102
|
success: false,
|
|
110
103
|
message: `Setup failed: ${error.message}`,
|
|
111
|
-
alreadyExists:
|
|
104
|
+
alreadyExists: alreadyExisted,
|
|
112
105
|
output: errorOutput
|
|
113
106
|
};
|
|
114
107
|
}
|
|
@@ -171,7 +164,7 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
|
171
164
|
};
|
|
172
165
|
}
|
|
173
166
|
if (checkPackages) {
|
|
174
|
-
const criticalPackages = ["pypdf", "transformers"];
|
|
167
|
+
const criticalPackages = ["pypdf", "transformers", "docx"];
|
|
175
168
|
const missingPackages = [];
|
|
176
169
|
for (const pkg of criticalPackages) {
|
|
177
170
|
try {
|
package/dist/index.cjs
CHANGED
|
@@ -3950,15 +3950,11 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3950
3950
|
timeout = 6e5
|
|
3951
3951
|
// 10 minutes
|
|
3952
3952
|
} = options;
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
success: true,
|
|
3959
|
-
message: "Python environment already exists",
|
|
3960
|
-
alreadyExists: true
|
|
3961
|
-
};
|
|
3953
|
+
const alreadyExisted = isPythonEnvironmentSetup(packageRoot);
|
|
3954
|
+
if (verbose) {
|
|
3955
|
+
console.log(
|
|
3956
|
+
alreadyExisted ? "Python environment already exists \u2014 verifying and installing any missing dependencies..." : "Setting up Python environment..."
|
|
3957
|
+
);
|
|
3962
3958
|
}
|
|
3963
3959
|
const setupScriptPath = getSetupScriptPath(packageRoot);
|
|
3964
3960
|
if (!(0, import_fs.existsSync)(setupScriptPath)) {
|
|
@@ -3969,9 +3965,6 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3969
3965
|
};
|
|
3970
3966
|
}
|
|
3971
3967
|
try {
|
|
3972
|
-
if (verbose) {
|
|
3973
|
-
console.log("Setting up Python environment...");
|
|
3974
|
-
}
|
|
3975
3968
|
const { stdout, stderr } = await execAsync2(`bash "${setupScriptPath}"`, {
|
|
3976
3969
|
cwd: packageRoot,
|
|
3977
3970
|
timeout,
|
|
@@ -3991,8 +3984,8 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
3991
3984
|
}
|
|
3992
3985
|
return {
|
|
3993
3986
|
success: true,
|
|
3994
|
-
message: "Python environment set up successfully",
|
|
3995
|
-
alreadyExists:
|
|
3987
|
+
message: alreadyExisted ? "Python environment verified (dependencies were already installed or have been updated)" : "Python environment set up successfully",
|
|
3988
|
+
alreadyExists: alreadyExisted,
|
|
3996
3989
|
pythonVersion,
|
|
3997
3990
|
output
|
|
3998
3991
|
};
|
|
@@ -4001,7 +3994,7 @@ async function setupPythonEnvironment(options = {}) {
|
|
|
4001
3994
|
return {
|
|
4002
3995
|
success: false,
|
|
4003
3996
|
message: `Setup failed: ${error.message}`,
|
|
4004
|
-
alreadyExists:
|
|
3997
|
+
alreadyExists: alreadyExisted,
|
|
4005
3998
|
output: errorOutput
|
|
4006
3999
|
};
|
|
4007
4000
|
}
|
|
@@ -4064,7 +4057,7 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
|
4064
4057
|
};
|
|
4065
4058
|
}
|
|
4066
4059
|
if (checkPackages) {
|
|
4067
|
-
const criticalPackages = ["pypdf", "transformers"];
|
|
4060
|
+
const criticalPackages = ["pypdf", "transformers", "docx"];
|
|
4068
4061
|
const missingPackages = [];
|
|
4069
4062
|
for (const pkg of criticalPackages) {
|
|
4070
4063
|
try {
|
|
@@ -5013,6 +5006,38 @@ var init_session_file_read_tool = __esm({
|
|
|
5013
5006
|
});
|
|
5014
5007
|
|
|
5015
5008
|
// src/templates/tools/document-render-helpers.ts
|
|
5009
|
+
function isLegacyOfficeFormat(ext) {
|
|
5010
|
+
return ext in LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
5011
|
+
}
|
|
5012
|
+
async function convertLegacyOfficeToModern(bytes, ext) {
|
|
5013
|
+
const target = LEGACY_OFFICE_CONVERSION_TARGETS[ext];
|
|
5014
|
+
if (!target) {
|
|
5015
|
+
throw new Error(`No modern-format conversion target registered for "${ext}"`);
|
|
5016
|
+
}
|
|
5017
|
+
const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-officeconv-"));
|
|
5018
|
+
try {
|
|
5019
|
+
const inputPath = (0, import_node_path5.join)(dir, `input${ext}`);
|
|
5020
|
+
await (0, import_promises2.writeFile)(inputPath, bytes);
|
|
5021
|
+
try {
|
|
5022
|
+
await execFileAsync(
|
|
5023
|
+
"soffice",
|
|
5024
|
+
["--headless", "--convert-to", target, inputPath, "--outdir", dir],
|
|
5025
|
+
{ timeout: 6e4, maxBuffer: MAX_STDOUT_BYTES }
|
|
5026
|
+
);
|
|
5027
|
+
} catch (err) {
|
|
5028
|
+
throw new Error(
|
|
5029
|
+
`LibreOffice could not convert the legacy "${ext}" file to ${target}: ` + (err instanceof Error ? err.message : String(err))
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
const outputPath = (0, import_node_path5.join)(dir, `input.${target}`);
|
|
5033
|
+
if (!(0, import_node_fs6.existsSync)(outputPath)) {
|
|
5034
|
+
throw new Error(`LibreOffice produced no output converting "${ext}" to ${target}`);
|
|
5035
|
+
}
|
|
5036
|
+
return await (0, import_promises2.readFile)(outputPath);
|
|
5037
|
+
} finally {
|
|
5038
|
+
await (0, import_promises2.rm)(dir, { recursive: true, force: true });
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5016
5041
|
async function pdfToText(pdf) {
|
|
5017
5042
|
const dir = await (0, import_promises2.mkdtemp)((0, import_node_path5.join)((0, import_node_os.tmpdir)(), "exulu-parse-"));
|
|
5018
5043
|
try {
|
|
@@ -5049,18 +5074,25 @@ async function renderPdfPageToPng(pdf, page, scaleTo) {
|
|
|
5049
5074
|
await (0, import_promises2.rm)(dir, { recursive: true, force: true });
|
|
5050
5075
|
}
|
|
5051
5076
|
}
|
|
5052
|
-
var import_node_child_process4, import_node_util3, import_promises2, import_node_os, import_node_path5, execFileAsync, MAX_STDOUT_BYTES;
|
|
5077
|
+
var import_node_child_process4, import_node_util3, import_node_fs6, import_promises2, import_node_os, import_node_path5, execFileAsync, MAX_STDOUT_BYTES, LEGACY_OFFICE_CONVERSION_TARGETS;
|
|
5053
5078
|
var init_document_render_helpers = __esm({
|
|
5054
5079
|
"src/templates/tools/document-render-helpers.ts"() {
|
|
5055
5080
|
"use strict";
|
|
5056
5081
|
init_cjs_shims();
|
|
5057
5082
|
import_node_child_process4 = require("child_process");
|
|
5058
5083
|
import_node_util3 = require("util");
|
|
5084
|
+
import_node_fs6 = require("fs");
|
|
5059
5085
|
import_promises2 = require("fs/promises");
|
|
5060
5086
|
import_node_os = require("os");
|
|
5061
5087
|
import_node_path5 = require("path");
|
|
5062
5088
|
execFileAsync = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
|
|
5063
5089
|
MAX_STDOUT_BYTES = 64 * 1024 * 1024;
|
|
5090
|
+
LEGACY_OFFICE_CONVERSION_TARGETS = {
|
|
5091
|
+
".doc": "docx",
|
|
5092
|
+
".xls": "xlsx",
|
|
5093
|
+
".ppt": "pptx",
|
|
5094
|
+
".rtf": "docx"
|
|
5095
|
+
};
|
|
5064
5096
|
}
|
|
5065
5097
|
});
|
|
5066
5098
|
|
|
@@ -5077,7 +5109,15 @@ function looksLikeGarbledTextLayer(text) {
|
|
|
5077
5109
|
if (visible < 40) return false;
|
|
5078
5110
|
return control / visible > 0.01;
|
|
5079
5111
|
}
|
|
5080
|
-
|
|
5112
|
+
function binaryDocumentBashHint(command, stdout, stderr) {
|
|
5113
|
+
const match = command.match(BINARY_DOCUMENT_EXTENSION_PATTERN);
|
|
5114
|
+
if (!match) return void 0;
|
|
5115
|
+
const silent = !stdout.trim() && !stderr.trim();
|
|
5116
|
+
if (!silent && !looksLikeGarbledTextLayer(stdout)) return void 0;
|
|
5117
|
+
const [, file] = match;
|
|
5118
|
+
return `Note: "${file}" is a binary document \u2014 grep/cat/text tools cannot read the text inside it, so a silent or garbled result does not mean the content isn't there. Use parse_document to extract its text first, then search or read within that extracted text instead of the original file.`;
|
|
5119
|
+
}
|
|
5120
|
+
var import_zod8, import_node_path6, import_officeparser, DEFAULT_LIMIT2, MAX_CONTENT_CHARS2, MIN_CHARS_PER_PAGE, OFFICE_EXTENSIONS, pagesPattern, BINARY_DOCUMENT_EXTENSION_PATTERN, createParseDocumentTool;
|
|
5081
5121
|
var init_parse_document_tool = __esm({
|
|
5082
5122
|
"src/templates/tools/parse-document-tool.ts"() {
|
|
5083
5123
|
"use strict";
|
|
@@ -5105,6 +5145,10 @@ var init_parse_document_tool = __esm({
|
|
|
5105
5145
|
".rtf"
|
|
5106
5146
|
]);
|
|
5107
5147
|
pagesPattern = /^(\d+)(?:-(\d+))?$/;
|
|
5148
|
+
BINARY_DOCUMENT_EXTENSION_PATTERN = new RegExp(
|
|
5149
|
+
`([^\\s"'\`]+\\.(?:pdf|${[...OFFICE_EXTENSIONS].map((ext) => ext.slice(1)).join("|")}))\\b`,
|
|
5150
|
+
"i"
|
|
5151
|
+
);
|
|
5108
5152
|
createParseDocumentTool = ({
|
|
5109
5153
|
sessionID,
|
|
5110
5154
|
user,
|
|
@@ -5174,7 +5218,8 @@ var init_parse_document_tool = __esm({
|
|
|
5174
5218
|
fullText = pageTexts.map((text, i) => ({ page: i + 1, text })).filter(({ page }) => page >= range[0] && page <= range[1]).map(({ page, text }) => `--- page ${page} ---
|
|
5175
5219
|
${text.trim()}`).join("\n");
|
|
5176
5220
|
} else {
|
|
5177
|
-
const
|
|
5221
|
+
const officeBytes = isLegacyOfficeFormat(ext) ? await convertLegacyOfficeToModern(bytes, ext) : bytes;
|
|
5222
|
+
const extracted = await (0, import_officeparser.parseOfficeAsync)(officeBytes, {
|
|
5178
5223
|
outputErrorToConsole: false,
|
|
5179
5224
|
newlineDelimiter: "\n"
|
|
5180
5225
|
});
|
|
@@ -5236,7 +5281,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5236
5281
|
throw new PreviewRenderError(`Invalid ETag for ${sourceKey}`);
|
|
5237
5282
|
}
|
|
5238
5283
|
const cachedPath = (0, import_node_path7.join)(CACHE_ROOT, `${safeEtag}.pdf`);
|
|
5239
|
-
if ((0,
|
|
5284
|
+
if ((0, import_node_fs7.existsSync)(cachedPath)) {
|
|
5240
5285
|
return (0, import_promises3.readFile)(cachedPath);
|
|
5241
5286
|
}
|
|
5242
5287
|
const existing = inFlight.get(safeEtag);
|
|
@@ -5261,7 +5306,7 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5261
5306
|
`LibreOffice conversion failed for ${sourceKey} (etag ${etag}): ${err?.stderr ?? err?.message ?? "unknown error"}`
|
|
5262
5307
|
);
|
|
5263
5308
|
}
|
|
5264
|
-
if (!(0,
|
|
5309
|
+
if (!(0, import_node_fs7.existsSync)(outputPath)) {
|
|
5265
5310
|
throw new PreviewRenderError(
|
|
5266
5311
|
`LibreOffice produced no output for ${sourceKey} (etag ${etag})`
|
|
5267
5312
|
);
|
|
@@ -5278,13 +5323,13 @@ async function getPdfPreviewBytes(opts) {
|
|
|
5278
5323
|
inFlight.set(safeEtag, promise);
|
|
5279
5324
|
return promise;
|
|
5280
5325
|
}
|
|
5281
|
-
var import_node_child_process5,
|
|
5326
|
+
var import_node_child_process5, import_node_fs7, import_promises3, import_node_path7, import_node_util4, execAsync4, CACHE_ROOT, CACHE_IN, CACHE_OUT, inFlight, PreviewRenderError;
|
|
5282
5327
|
var init_pdf_preview_cache = __esm({
|
|
5283
5328
|
"src/sessions/pdf-preview-cache.ts"() {
|
|
5284
5329
|
"use strict";
|
|
5285
5330
|
init_cjs_shims();
|
|
5286
5331
|
import_node_child_process5 = require("child_process");
|
|
5287
|
-
|
|
5332
|
+
import_node_fs7 = require("fs");
|
|
5288
5333
|
import_promises3 = require("fs/promises");
|
|
5289
5334
|
import_node_path7 = require("path");
|
|
5290
5335
|
import_node_util4 = require("util");
|
|
@@ -6540,13 +6585,17 @@ var init_convert_exulu_tools_to_ai_sdk_tools = __esm({
|
|
|
6540
6585
|
const origExecute = sharedSessionSandbox.tools.bash.execute;
|
|
6541
6586
|
if (!origExecute) throw new Error("bash execute is undefined");
|
|
6542
6587
|
const result = await origExecute(args, opts);
|
|
6588
|
+
const rawStdout = typeof result?.stdout === "string" ? result.stdout : "";
|
|
6589
|
+
const rawStderr = typeof result?.stderr === "string" ? result.stderr : "";
|
|
6590
|
+
const hint = binaryDocumentBashHint(String(args?.command ?? ""), rawStdout, rawStderr);
|
|
6591
|
+
const stderrWithHint = hint ? [rawStderr, hint].filter(Boolean).join("\n\n") : rawStderr;
|
|
6543
6592
|
return {
|
|
6544
6593
|
...result,
|
|
6545
6594
|
...typeof result?.stdout === "string" && {
|
|
6546
|
-
stdout: truncateToolOutput(
|
|
6595
|
+
stdout: truncateToolOutput(rawStdout, budget.contextWindow, "bash", 0.1, toolOutputCharLimit)
|
|
6547
6596
|
},
|
|
6548
|
-
...typeof result?.stderr === "string" && {
|
|
6549
|
-
stderr: truncateToolOutput(
|
|
6597
|
+
...(typeof result?.stderr === "string" || hint) && {
|
|
6598
|
+
stderr: truncateToolOutput(stderrWithHint, budget.contextWindow, "bash stderr", 0.4, toolOutputCharLimit)
|
|
6550
6599
|
}
|
|
6551
6600
|
};
|
|
6552
6601
|
}
|
|
@@ -12365,7 +12414,12 @@ var transcriptionJobsSchema = {
|
|
|
12365
12414
|
// Selected per-meeting post-processing: [{ prompt_id, agent_id }].
|
|
12366
12415
|
{ name: "post_processing_prompts", type: "json" },
|
|
12367
12416
|
// Results: [{ prompt_id, agent_id, prompt_name, status, output, error, ran_at }].
|
|
12368
|
-
{ name: "post_processing_outputs", type: "json" }
|
|
12417
|
+
{ name: "post_processing_outputs", type: "json" },
|
|
12418
|
+
// Permanent local copy of the meeting's mixed MP4, downloaded from Recall
|
|
12419
|
+
// when RECALL_STORE_VIDEO_LOCALLY=true. Null when that flag is off (the
|
|
12420
|
+
// video then stays reachable only via ExuluRecall.getRecordingVideoUrl,
|
|
12421
|
+
// for as long as Recall itself retains it) or for Whisper upload jobs.
|
|
12422
|
+
{ name: "video", type: "file" }
|
|
12369
12423
|
]
|
|
12370
12424
|
};
|
|
12371
12425
|
var imageGenerationsSchema = {
|
|
@@ -18648,6 +18702,45 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
18648
18702
|
};
|
|
18649
18703
|
};
|
|
18650
18704
|
|
|
18705
|
+
// src/utils/agent-glossary.ts
|
|
18706
|
+
init_cjs_shims();
|
|
18707
|
+
function extractGlossaryFromAgentTools(tools) {
|
|
18708
|
+
if (!Array.isArray(tools)) return [];
|
|
18709
|
+
const searchTool = tools.find((t) => t?.id === "agentic_context_search");
|
|
18710
|
+
const entry = searchTool?.config?.find((c) => c?.name === "vocabulary");
|
|
18711
|
+
if (entry === void 0 || entry.variable === void 0 || entry.variable === null || entry.variable === "") {
|
|
18712
|
+
return [];
|
|
18713
|
+
}
|
|
18714
|
+
try {
|
|
18715
|
+
const parsed = typeof entry.variable === "string" ? JSON.parse(entry.variable) : entry.variable;
|
|
18716
|
+
const glossary = parsed?.glossary;
|
|
18717
|
+
if (!Array.isArray(glossary)) return [];
|
|
18718
|
+
return glossary.filter(
|
|
18719
|
+
(g) => !!g && typeof g.term === "string" && typeof g.meaning === "string" && g.term.trim().length > 0
|
|
18720
|
+
);
|
|
18721
|
+
} catch (err) {
|
|
18722
|
+
console.warn(
|
|
18723
|
+
"[EXULU] Failed to parse the agentic_context_search vocabulary config while building agent instructions.",
|
|
18724
|
+
err
|
|
18725
|
+
);
|
|
18726
|
+
return [];
|
|
18727
|
+
}
|
|
18728
|
+
}
|
|
18729
|
+
function formatGlossaryBlock(glossary) {
|
|
18730
|
+
if (!glossary.length) return "";
|
|
18731
|
+
const lines = glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n");
|
|
18732
|
+
return `The organization's documents and internal terminology use the following abbreviations/terms:
|
|
18733
|
+
${lines}`;
|
|
18734
|
+
}
|
|
18735
|
+
function withGlossary(baseInstructions, tools) {
|
|
18736
|
+
const instructions = baseInstructions ?? "";
|
|
18737
|
+
const block = formatGlossaryBlock(extractGlossaryFromAgentTools(tools));
|
|
18738
|
+
if (!block) return instructions;
|
|
18739
|
+
return instructions ? `${block}
|
|
18740
|
+
|
|
18741
|
+
${instructions}` : block;
|
|
18742
|
+
}
|
|
18743
|
+
|
|
18651
18744
|
// src/exulu/agent-as-tool.ts
|
|
18652
18745
|
var createAgentTool = async (instance2, contexts) => {
|
|
18653
18746
|
const agent = await exuluApp.get().agent(instance2);
|
|
@@ -18706,7 +18799,7 @@ var createAgentTool = async (instance2, contexts) => {
|
|
|
18706
18799
|
const response = await generateSync({
|
|
18707
18800
|
agent,
|
|
18708
18801
|
contexts,
|
|
18709
|
-
instructions: agent.instructions,
|
|
18802
|
+
instructions: withGlossary(agent.instructions, agent.tools),
|
|
18710
18803
|
prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
|
|
18711
18804
|
languageModel: resolved.languageModel,
|
|
18712
18805
|
user,
|
|
@@ -20808,6 +20901,9 @@ var buildTranscriptItemInput = ({
|
|
|
20808
20901
|
post_processing: row.post_processing_outputs ?? void 0,
|
|
20809
20902
|
// Handle for the meeting video; null for Whisper uploads.
|
|
20810
20903
|
recall_recording_id: row.recall_recording_id ?? void 0,
|
|
20904
|
+
// Permanent local copy of the video, only present when
|
|
20905
|
+
// RECALL_STORE_VIDEO_LOCALLY was on at recording time.
|
|
20906
|
+
video_s3key: row.video_s3key ?? void 0,
|
|
20811
20907
|
rights_mode: rightsMode,
|
|
20812
20908
|
created_by: row.created_by
|
|
20813
20909
|
});
|
|
@@ -21143,6 +21239,12 @@ var recordingMonthlyLimitSeconds = () => {
|
|
|
21143
21239
|
const minutes = Number(raw);
|
|
21144
21240
|
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 : null;
|
|
21145
21241
|
};
|
|
21242
|
+
var RECALL_RECORDING_RETENTION_DEFAULT_HOURS = 2160;
|
|
21243
|
+
var recallRecordingRetentionHours = () => {
|
|
21244
|
+
const raw = Number(process.env.RECALL_RECORDING_RETENTION_HOURS);
|
|
21245
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : RECALL_RECORDING_RETENTION_DEFAULT_HOURS;
|
|
21246
|
+
};
|
|
21247
|
+
var recallStoreVideoLocally = () => process.env.RECALL_STORE_VIDEO_LOCALLY === "true";
|
|
21146
21248
|
var recallWebhookUrl = () => {
|
|
21147
21249
|
const base = recallPublicBaseUrl();
|
|
21148
21250
|
return base ? `${base}/recall/webhooks` : null;
|
|
@@ -21257,7 +21359,6 @@ var recordingDurationSeconds = (rec) => {
|
|
|
21257
21359
|
}
|
|
21258
21360
|
return null;
|
|
21259
21361
|
};
|
|
21260
|
-
var RECORDING_RETENTION_HOURS = 2160;
|
|
21261
21362
|
var buildCreateBotPayload = (input) => ({
|
|
21262
21363
|
meeting_url: input.meeting_url,
|
|
21263
21364
|
join_at: input.join_at,
|
|
@@ -21267,7 +21368,7 @@ var buildCreateBotPayload = (input) => ({
|
|
|
21267
21368
|
video_mixed_layout: "speaker_view",
|
|
21268
21369
|
participant_events: {},
|
|
21269
21370
|
meeting_metadata: {},
|
|
21270
|
-
retention: { type: "timed", hours:
|
|
21371
|
+
retention: { type: "timed", hours: recallRecordingRetentionHours() }
|
|
21271
21372
|
},
|
|
21272
21373
|
...input.notifyChat ? {
|
|
21273
21374
|
chat: {
|
|
@@ -21329,6 +21430,31 @@ var recallClient = {
|
|
|
21329
21430
|
}
|
|
21330
21431
|
};
|
|
21331
21432
|
|
|
21433
|
+
// src/exulu/recall/video-storage.ts
|
|
21434
|
+
init_cjs_shims();
|
|
21435
|
+
init_uppy();
|
|
21436
|
+
async function downloadAndStoreRecordingVideo(recording, jobId, config) {
|
|
21437
|
+
const video = recording.media_shortcuts?.video_mixed;
|
|
21438
|
+
const url = video?.data?.download_url;
|
|
21439
|
+
if (!url) return null;
|
|
21440
|
+
if (video.status?.code && video.status.code !== "done") return null;
|
|
21441
|
+
const res = await fetch(url);
|
|
21442
|
+
if (!res.ok) {
|
|
21443
|
+
throw new Error(`video download failed for job ${jobId}: HTTP ${res.status}`);
|
|
21444
|
+
}
|
|
21445
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
21446
|
+
return uploadFile(
|
|
21447
|
+
bytes,
|
|
21448
|
+
`recall-videos/${jobId}.mp4`,
|
|
21449
|
+
config,
|
|
21450
|
+
{ contentType: "video/mp4" },
|
|
21451
|
+
void 0,
|
|
21452
|
+
void 0,
|
|
21453
|
+
true
|
|
21454
|
+
// global: this is a system-stored artifact, not scoped to an uploading user.
|
|
21455
|
+
);
|
|
21456
|
+
}
|
|
21457
|
+
|
|
21332
21458
|
// src/exulu/recall/transcript-map.ts
|
|
21333
21459
|
init_cjs_shims();
|
|
21334
21460
|
var speakerLabel = (participant) => {
|
|
@@ -21566,12 +21692,24 @@ var recallService = {
|
|
|
21566
21692
|
const raw = await recallClient.downloadTranscript(url);
|
|
21567
21693
|
const segments = mapRecallTranscript(raw);
|
|
21568
21694
|
let duration = durationFromSegments(segments);
|
|
21695
|
+
let videoS3Key = null;
|
|
21569
21696
|
const recId = recordingId ?? job.recall_recording_id;
|
|
21570
21697
|
if (recId) {
|
|
21571
21698
|
try {
|
|
21572
21699
|
const rec = await recallClient.retrieveRecording(recId);
|
|
21573
21700
|
const recDuration = recordingDurationSeconds(rec);
|
|
21574
21701
|
if (recDuration != null) duration = recDuration;
|
|
21702
|
+
if (recallStoreVideoLocally()) {
|
|
21703
|
+
try {
|
|
21704
|
+
videoS3Key = await downloadAndStoreRecordingVideo(
|
|
21705
|
+
rec,
|
|
21706
|
+
jobId,
|
|
21707
|
+
exuluApp.get().config
|
|
21708
|
+
);
|
|
21709
|
+
} catch (err) {
|
|
21710
|
+
log4(`could not store video locally for job ${jobId}: ${err.message}`);
|
|
21711
|
+
}
|
|
21712
|
+
}
|
|
21575
21713
|
} catch (err) {
|
|
21576
21714
|
log4(`could not fetch recording duration for job ${jobId}: ${err.message}`);
|
|
21577
21715
|
}
|
|
@@ -21580,6 +21718,7 @@ var recallService = {
|
|
|
21580
21718
|
recall_transcript_id: transcriptId ?? job.recall_transcript_id ?? null,
|
|
21581
21719
|
raw_segments: JSON.stringify(segments),
|
|
21582
21720
|
duration_seconds: duration,
|
|
21721
|
+
...videoS3Key ? { video_s3key: videoS3Key } : {},
|
|
21583
21722
|
status: "awaiting_review"
|
|
21584
21723
|
});
|
|
21585
21724
|
} catch (err) {
|
|
@@ -21734,7 +21873,7 @@ var recallService = {
|
|
|
21734
21873
|
);
|
|
21735
21874
|
const { text } = await (0, import_ai8.generateText)({
|
|
21736
21875
|
model: resolved.languageModel,
|
|
21737
|
-
system: agent.instructions || void 0,
|
|
21876
|
+
system: withGlossary(agent.instructions, agent.tools) || void 0,
|
|
21738
21877
|
prompt: `${prompt.content}
|
|
21739
21878
|
|
|
21740
21879
|
---
|
|
@@ -22909,6 +23048,9 @@ type PageInfo {
|
|
|
22909
23048
|
typeDefs += `
|
|
22910
23049
|
meetingRecordingUsage: MeetingRecordingUsage
|
|
22911
23050
|
`;
|
|
23051
|
+
typeDefs += `
|
|
23052
|
+
recordingVideoUrl(job_id: ID!): String
|
|
23053
|
+
`;
|
|
22912
23054
|
typeDefs += `
|
|
22913
23055
|
workflowTriggers(workflow: ID!): [WorkflowTrigger!]!
|
|
22914
23056
|
`;
|
|
@@ -23855,6 +23997,13 @@ type LiteLLMModel {
|
|
|
23855
23997
|
if (!context.user) throw new Error("Authentication required");
|
|
23856
23998
|
return recallService.getUsage();
|
|
23857
23999
|
};
|
|
24000
|
+
resolvers.Query["recordingVideoUrl"] = async (_, args, context) => {
|
|
24001
|
+
await assertOwnsTranscriptionJob(args.job_id, context);
|
|
24002
|
+
const { db: db2 } = context;
|
|
24003
|
+
const row = await db2.from("transcription_jobs").select(["recall_recording_id"]).where({ id: args.job_id }).first();
|
|
24004
|
+
if (!row?.recall_recording_id) return null;
|
|
24005
|
+
return recallService.getRecordingVideoUrl(row.recall_recording_id);
|
|
24006
|
+
};
|
|
23858
24007
|
resolvers.Query["evals"] = async (_, args, context, info) => {
|
|
23859
24008
|
const requestedFields = getRequestedFields(info);
|
|
23860
24009
|
return {
|
|
@@ -25215,7 +25364,7 @@ async function editImage(args) {
|
|
|
25215
25364
|
|
|
25216
25365
|
// src/exulu/litellm/parse-image-models.ts
|
|
25217
25366
|
init_cjs_shims();
|
|
25218
|
-
var
|
|
25367
|
+
var import_node_fs8 = require("fs");
|
|
25219
25368
|
var stripComment3 = (line) => {
|
|
25220
25369
|
const idx = line.indexOf("#");
|
|
25221
25370
|
return idx >= 0 ? line.slice(0, idx) : line;
|
|
@@ -25238,8 +25387,8 @@ var parseInt103 = (raw) => {
|
|
|
25238
25387
|
return Number.isInteger(n) ? n : void 0;
|
|
25239
25388
|
};
|
|
25240
25389
|
var parseImageGenerationModels = (configPath) => {
|
|
25241
|
-
if (!(0,
|
|
25242
|
-
const text = (0,
|
|
25390
|
+
if (!(0, import_node_fs8.existsSync)(configPath)) return [];
|
|
25391
|
+
const text = (0, import_node_fs8.readFileSync)(configPath, "utf8");
|
|
25243
25392
|
const lines = text.split("\n");
|
|
25244
25393
|
const entries = [];
|
|
25245
25394
|
let current;
|
|
@@ -26527,9 +26676,12 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
|
|
|
26527
26676
|
}
|
|
26528
26677
|
const approvedTools = req.body.approvedTools ? typeof req.body.approvedTools === "string" ? JSON.parse(req.body.approvedTools) : req.body.approvedTools : [];
|
|
26529
26678
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
26530
|
-
const instructions =
|
|
26679
|
+
const instructions = withGlossary(
|
|
26680
|
+
customInstructions ? `${agent.instructions}
|
|
26531
26681
|
|
|
26532
|
-
${customInstructions}` : agent.instructions
|
|
26682
|
+
${customInstructions}` : agent.instructions,
|
|
26683
|
+
agent.tools
|
|
26684
|
+
);
|
|
26533
26685
|
if (headers.session) markStreamActive(headers.session);
|
|
26534
26686
|
const turnStartedAt = Date.now();
|
|
26535
26687
|
let result;
|
|
@@ -26665,9 +26817,12 @@ ${customInstructions}` : agent.instructions;
|
|
|
26665
26817
|
return;
|
|
26666
26818
|
} else {
|
|
26667
26819
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
26668
|
-
const instructions =
|
|
26820
|
+
const instructions = withGlossary(
|
|
26821
|
+
customInstructions ? `${agent.instructions}
|
|
26669
26822
|
|
|
26670
|
-
${customInstructions}` : agent.instructions
|
|
26823
|
+
${customInstructions}` : agent.instructions,
|
|
26824
|
+
agent.tools
|
|
26825
|
+
);
|
|
26671
26826
|
let response;
|
|
26672
26827
|
try {
|
|
26673
26828
|
response = await generateSync({
|
|
@@ -31049,7 +31204,10 @@ var transcriptionsContext = new ExuluContext2({
|
|
|
31049
31204
|
// Link back to the Recall recording so the mixed video stays reachable
|
|
31050
31205
|
// (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
|
|
31051
31206
|
// after six hours). Null for Whisper uploads.
|
|
31052
|
-
{ name: "recall_recording_id", type: "text" }
|
|
31207
|
+
{ name: "recall_recording_id", type: "text" },
|
|
31208
|
+
// Permanent local copy of the mixed video, present only when
|
|
31209
|
+
// RECALL_STORE_VIDEO_LOCALLY=true was set at recording time.
|
|
31210
|
+
{ name: "video", type: "file" }
|
|
31053
31211
|
],
|
|
31054
31212
|
sources: [],
|
|
31055
31213
|
active: true,
|
|
@@ -31482,6 +31640,11 @@ var ExuluApp = class {
|
|
|
31482
31640
|
get audit() {
|
|
31483
31641
|
return this._audit ?? getAuditLogger(this._config ?? {});
|
|
31484
31642
|
}
|
|
31643
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
31644
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
31645
|
+
get config() {
|
|
31646
|
+
return this._config ?? {};
|
|
31647
|
+
}
|
|
31485
31648
|
embeddings = {
|
|
31486
31649
|
generate: {
|
|
31487
31650
|
one: async ({ context: contextId, item: itemId }) => {
|
|
@@ -32737,17 +32900,17 @@ init_cjs_shims();
|
|
|
32737
32900
|
|
|
32738
32901
|
// src/exulu/litellm/db-init.ts
|
|
32739
32902
|
init_cjs_shims();
|
|
32740
|
-
var
|
|
32903
|
+
var import_node_fs10 = require("fs");
|
|
32741
32904
|
var import_node_path11 = require("path");
|
|
32742
32905
|
var import_node_child_process6 = require("child_process");
|
|
32743
32906
|
var import_pg = require("pg");
|
|
32744
32907
|
|
|
32745
32908
|
// src/exulu/litellm/db-setup-check.ts
|
|
32746
32909
|
init_cjs_shims();
|
|
32747
|
-
var
|
|
32910
|
+
var import_node_fs9 = require("fs");
|
|
32748
32911
|
var readLiteLLMDatabaseUrl = (configPath) => {
|
|
32749
|
-
if (!(0,
|
|
32750
|
-
const text = (0,
|
|
32912
|
+
if (!(0, import_node_fs9.existsSync)(configPath)) return void 0;
|
|
32913
|
+
const text = (0, import_node_fs9.readFileSync)(configPath, "utf8");
|
|
32751
32914
|
const match = text.match(
|
|
32752
32915
|
/^\s*database_url:\s*["']?([^"'\n#]+?)["']?\s*(#.*)?$/m
|
|
32753
32916
|
);
|
|
@@ -32949,7 +33112,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
32949
33112
|
const venvBin = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/bin");
|
|
32950
33113
|
const prismaCli = (0, import_node_path11.resolve)(venvBin, "prisma");
|
|
32951
33114
|
const venvLibDir = (0, import_node_path11.resolve)(packageRoot, "ee/python/.venv/lib");
|
|
32952
|
-
const pythonVersionDir = (0,
|
|
33115
|
+
const pythonVersionDir = (0, import_node_fs10.existsSync)(venvLibDir) ? (0, import_node_fs10.readdirSync)(venvLibDir).find((entry) => /^python3\.\d+$/.test(entry)) : void 0;
|
|
32953
33116
|
if (!pythonVersionDir) {
|
|
32954
33117
|
warn2([
|
|
32955
33118
|
`Could not find a python3.* directory under ${venvLibDir}.`,
|
|
@@ -32964,7 +33127,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
32964
33127
|
"site-packages/litellm/proxy"
|
|
32965
33128
|
);
|
|
32966
33129
|
const schemaPath = (0, import_node_path11.resolve)(litellmProxyDir, "schema.prisma");
|
|
32967
|
-
if (!(0,
|
|
33130
|
+
if (!(0, import_node_fs10.existsSync)(prismaCli)) {
|
|
32968
33131
|
warn2([
|
|
32969
33132
|
`Prisma CLI not found at ${prismaCli}.`,
|
|
32970
33133
|
`Run \`npm run python:setup\` to create the venv and install prisma.`,
|
|
@@ -32972,7 +33135,7 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
32972
33135
|
]);
|
|
32973
33136
|
return;
|
|
32974
33137
|
}
|
|
32975
|
-
if (!(0,
|
|
33138
|
+
if (!(0, import_node_fs10.existsSync)(schemaPath)) {
|
|
32976
33139
|
warn2([
|
|
32977
33140
|
`LiteLLM Prisma schema not found at ${schemaPath}.`,
|
|
32978
33141
|
`Re-run \`npm run python:setup\`. Skipping LiteLLM database setup.`
|
package/dist/index.d.cts
CHANGED
|
@@ -1167,6 +1167,9 @@ declare class ExuluApp {
|
|
|
1167
1167
|
context(id: string): ExuluContext | undefined;
|
|
1168
1168
|
get contexts(): ExuluContext[];
|
|
1169
1169
|
get audit(): AuditLogger;
|
|
1170
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
1171
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
1172
|
+
get config(): ExuluConfig;
|
|
1170
1173
|
embeddings: {
|
|
1171
1174
|
generate: {
|
|
1172
1175
|
one: ({ context: contextId, item: itemId }: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1167,6 +1167,9 @@ declare class ExuluApp {
|
|
|
1167
1167
|
context(id: string): ExuluContext | undefined;
|
|
1168
1168
|
get contexts(): ExuluContext[];
|
|
1169
1169
|
get audit(): AuditLogger;
|
|
1170
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
1171
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
1172
|
+
get config(): ExuluConfig;
|
|
1170
1173
|
embeddings: {
|
|
1171
1174
|
generate: {
|
|
1172
1175
|
one: ({ context: contextId, item: itemId }: {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isPythonEnvironmentSetup,
|
|
6
6
|
setupPythonEnvironment,
|
|
7
7
|
validatePythonEnvironment
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-JIZCKSJO.js";
|
|
9
9
|
import {
|
|
10
10
|
COMPACTION_INSUFFICIENT,
|
|
11
11
|
ContextCompactionRequiredError,
|
|
@@ -90,7 +90,7 @@ import {
|
|
|
90
90
|
verifyCredentialNonce,
|
|
91
91
|
waitForLiteLLMReady,
|
|
92
92
|
withRetry
|
|
93
|
-
} from "./chunk-
|
|
93
|
+
} from "./chunk-D4PCOOL5.js";
|
|
94
94
|
import {
|
|
95
95
|
LiteLLMAdminError,
|
|
96
96
|
findLiteLLMModel,
|
|
@@ -2880,7 +2880,12 @@ var transcriptionJobsSchema = {
|
|
|
2880
2880
|
// Selected per-meeting post-processing: [{ prompt_id, agent_id }].
|
|
2881
2881
|
{ name: "post_processing_prompts", type: "json" },
|
|
2882
2882
|
// Results: [{ prompt_id, agent_id, prompt_name, status, output, error, ran_at }].
|
|
2883
|
-
{ name: "post_processing_outputs", type: "json" }
|
|
2883
|
+
{ name: "post_processing_outputs", type: "json" },
|
|
2884
|
+
// Permanent local copy of the meeting's mixed MP4, downloaded from Recall
|
|
2885
|
+
// when RECALL_STORE_VIDEO_LOCALLY=true. Null when that flag is off (the
|
|
2886
|
+
// video then stays reachable only via ExuluRecall.getRecordingVideoUrl,
|
|
2887
|
+
// for as long as Recall itself retains it) or for Whisper upload jobs.
|
|
2888
|
+
{ name: "video", type: "file" }
|
|
2884
2889
|
]
|
|
2885
2890
|
};
|
|
2886
2891
|
var imageGenerationsSchema = {
|
|
@@ -9036,6 +9041,44 @@ When a tool execution is not approved by the user, do not retry it unless explic
|
|
|
9036
9041
|
};
|
|
9037
9042
|
};
|
|
9038
9043
|
|
|
9044
|
+
// src/utils/agent-glossary.ts
|
|
9045
|
+
function extractGlossaryFromAgentTools(tools) {
|
|
9046
|
+
if (!Array.isArray(tools)) return [];
|
|
9047
|
+
const searchTool = tools.find((t) => t?.id === "agentic_context_search");
|
|
9048
|
+
const entry = searchTool?.config?.find((c) => c?.name === "vocabulary");
|
|
9049
|
+
if (entry === void 0 || entry.variable === void 0 || entry.variable === null || entry.variable === "") {
|
|
9050
|
+
return [];
|
|
9051
|
+
}
|
|
9052
|
+
try {
|
|
9053
|
+
const parsed = typeof entry.variable === "string" ? JSON.parse(entry.variable) : entry.variable;
|
|
9054
|
+
const glossary = parsed?.glossary;
|
|
9055
|
+
if (!Array.isArray(glossary)) return [];
|
|
9056
|
+
return glossary.filter(
|
|
9057
|
+
(g) => !!g && typeof g.term === "string" && typeof g.meaning === "string" && g.term.trim().length > 0
|
|
9058
|
+
);
|
|
9059
|
+
} catch (err) {
|
|
9060
|
+
console.warn(
|
|
9061
|
+
"[EXULU] Failed to parse the agentic_context_search vocabulary config while building agent instructions.",
|
|
9062
|
+
err
|
|
9063
|
+
);
|
|
9064
|
+
return [];
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
9067
|
+
function formatGlossaryBlock(glossary) {
|
|
9068
|
+
if (!glossary.length) return "";
|
|
9069
|
+
const lines = glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n");
|
|
9070
|
+
return `The organization's documents and internal terminology use the following abbreviations/terms:
|
|
9071
|
+
${lines}`;
|
|
9072
|
+
}
|
|
9073
|
+
function withGlossary(baseInstructions, tools) {
|
|
9074
|
+
const instructions = baseInstructions ?? "";
|
|
9075
|
+
const block = formatGlossaryBlock(extractGlossaryFromAgentTools(tools));
|
|
9076
|
+
if (!block) return instructions;
|
|
9077
|
+
return instructions ? `${block}
|
|
9078
|
+
|
|
9079
|
+
${instructions}` : block;
|
|
9080
|
+
}
|
|
9081
|
+
|
|
9039
9082
|
// src/exulu/agent-as-tool.ts
|
|
9040
9083
|
var createAgentTool = async (instance, contexts) => {
|
|
9041
9084
|
const agent = await exuluApp.get().agent(instance);
|
|
@@ -9094,7 +9137,7 @@ var createAgentTool = async (instance, contexts) => {
|
|
|
9094
9137
|
const response = await generateSync({
|
|
9095
9138
|
agent,
|
|
9096
9139
|
contexts,
|
|
9097
|
-
instructions: agent.instructions,
|
|
9140
|
+
instructions: withGlossary(agent.instructions, agent.tools),
|
|
9098
9141
|
prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
|
|
9099
9142
|
languageModel: resolved.languageModel,
|
|
9100
9143
|
user,
|
|
@@ -11164,6 +11207,9 @@ var buildTranscriptItemInput = ({
|
|
|
11164
11207
|
post_processing: row.post_processing_outputs ?? void 0,
|
|
11165
11208
|
// Handle for the meeting video; null for Whisper uploads.
|
|
11166
11209
|
recall_recording_id: row.recall_recording_id ?? void 0,
|
|
11210
|
+
// Permanent local copy of the video, only present when
|
|
11211
|
+
// RECALL_STORE_VIDEO_LOCALLY was on at recording time.
|
|
11212
|
+
video_s3key: row.video_s3key ?? void 0,
|
|
11167
11213
|
rights_mode: rightsMode,
|
|
11168
11214
|
created_by: row.created_by
|
|
11169
11215
|
});
|
|
@@ -11491,6 +11537,12 @@ var recordingMonthlyLimitSeconds = () => {
|
|
|
11491
11537
|
const minutes = Number(raw);
|
|
11492
11538
|
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 : null;
|
|
11493
11539
|
};
|
|
11540
|
+
var RECALL_RECORDING_RETENTION_DEFAULT_HOURS = 2160;
|
|
11541
|
+
var recallRecordingRetentionHours = () => {
|
|
11542
|
+
const raw = Number(process.env.RECALL_RECORDING_RETENTION_HOURS);
|
|
11543
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : RECALL_RECORDING_RETENTION_DEFAULT_HOURS;
|
|
11544
|
+
};
|
|
11545
|
+
var recallStoreVideoLocally = () => process.env.RECALL_STORE_VIDEO_LOCALLY === "true";
|
|
11494
11546
|
var recallWebhookUrl = () => {
|
|
11495
11547
|
const base = recallPublicBaseUrl();
|
|
11496
11548
|
return base ? `${base}/recall/webhooks` : null;
|
|
@@ -11605,7 +11657,6 @@ var recordingDurationSeconds = (rec) => {
|
|
|
11605
11657
|
}
|
|
11606
11658
|
return null;
|
|
11607
11659
|
};
|
|
11608
|
-
var RECORDING_RETENTION_HOURS = 2160;
|
|
11609
11660
|
var buildCreateBotPayload = (input) => ({
|
|
11610
11661
|
meeting_url: input.meeting_url,
|
|
11611
11662
|
join_at: input.join_at,
|
|
@@ -11615,7 +11666,7 @@ var buildCreateBotPayload = (input) => ({
|
|
|
11615
11666
|
video_mixed_layout: "speaker_view",
|
|
11616
11667
|
participant_events: {},
|
|
11617
11668
|
meeting_metadata: {},
|
|
11618
|
-
retention: { type: "timed", hours:
|
|
11669
|
+
retention: { type: "timed", hours: recallRecordingRetentionHours() }
|
|
11619
11670
|
},
|
|
11620
11671
|
...input.notifyChat ? {
|
|
11621
11672
|
chat: {
|
|
@@ -11677,6 +11728,29 @@ var recallClient = {
|
|
|
11677
11728
|
}
|
|
11678
11729
|
};
|
|
11679
11730
|
|
|
11731
|
+
// src/exulu/recall/video-storage.ts
|
|
11732
|
+
async function downloadAndStoreRecordingVideo(recording, jobId, config) {
|
|
11733
|
+
const video = recording.media_shortcuts?.video_mixed;
|
|
11734
|
+
const url = video?.data?.download_url;
|
|
11735
|
+
if (!url) return null;
|
|
11736
|
+
if (video.status?.code && video.status.code !== "done") return null;
|
|
11737
|
+
const res = await fetch(url);
|
|
11738
|
+
if (!res.ok) {
|
|
11739
|
+
throw new Error(`video download failed for job ${jobId}: HTTP ${res.status}`);
|
|
11740
|
+
}
|
|
11741
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
11742
|
+
return uploadFile(
|
|
11743
|
+
bytes,
|
|
11744
|
+
`recall-videos/${jobId}.mp4`,
|
|
11745
|
+
config,
|
|
11746
|
+
{ contentType: "video/mp4" },
|
|
11747
|
+
void 0,
|
|
11748
|
+
void 0,
|
|
11749
|
+
true
|
|
11750
|
+
// global: this is a system-stored artifact, not scoped to an uploading user.
|
|
11751
|
+
);
|
|
11752
|
+
}
|
|
11753
|
+
|
|
11680
11754
|
// src/exulu/recall/transcript-map.ts
|
|
11681
11755
|
var speakerLabel = (participant) => {
|
|
11682
11756
|
if (!participant) return "unknown";
|
|
@@ -11913,12 +11987,24 @@ var recallService = {
|
|
|
11913
11987
|
const raw = await recallClient.downloadTranscript(url);
|
|
11914
11988
|
const segments = mapRecallTranscript(raw);
|
|
11915
11989
|
let duration = durationFromSegments(segments);
|
|
11990
|
+
let videoS3Key = null;
|
|
11916
11991
|
const recId = recordingId ?? job.recall_recording_id;
|
|
11917
11992
|
if (recId) {
|
|
11918
11993
|
try {
|
|
11919
11994
|
const rec = await recallClient.retrieveRecording(recId);
|
|
11920
11995
|
const recDuration = recordingDurationSeconds(rec);
|
|
11921
11996
|
if (recDuration != null) duration = recDuration;
|
|
11997
|
+
if (recallStoreVideoLocally()) {
|
|
11998
|
+
try {
|
|
11999
|
+
videoS3Key = await downloadAndStoreRecordingVideo(
|
|
12000
|
+
rec,
|
|
12001
|
+
jobId,
|
|
12002
|
+
exuluApp.get().config
|
|
12003
|
+
);
|
|
12004
|
+
} catch (err) {
|
|
12005
|
+
log3(`could not store video locally for job ${jobId}: ${err.message}`);
|
|
12006
|
+
}
|
|
12007
|
+
}
|
|
11922
12008
|
} catch (err) {
|
|
11923
12009
|
log3(`could not fetch recording duration for job ${jobId}: ${err.message}`);
|
|
11924
12010
|
}
|
|
@@ -11927,6 +12013,7 @@ var recallService = {
|
|
|
11927
12013
|
recall_transcript_id: transcriptId ?? job.recall_transcript_id ?? null,
|
|
11928
12014
|
raw_segments: JSON.stringify(segments),
|
|
11929
12015
|
duration_seconds: duration,
|
|
12016
|
+
...videoS3Key ? { video_s3key: videoS3Key } : {},
|
|
11930
12017
|
status: "awaiting_review"
|
|
11931
12018
|
});
|
|
11932
12019
|
} catch (err) {
|
|
@@ -12081,7 +12168,7 @@ var recallService = {
|
|
|
12081
12168
|
);
|
|
12082
12169
|
const { text } = await generateText4({
|
|
12083
12170
|
model: resolved.languageModel,
|
|
12084
|
-
system: agent.instructions || void 0,
|
|
12171
|
+
system: withGlossary(agent.instructions, agent.tools) || void 0,
|
|
12085
12172
|
prompt: `${prompt.content}
|
|
12086
12173
|
|
|
12087
12174
|
---
|
|
@@ -13248,6 +13335,9 @@ type PageInfo {
|
|
|
13248
13335
|
typeDefs += `
|
|
13249
13336
|
meetingRecordingUsage: MeetingRecordingUsage
|
|
13250
13337
|
`;
|
|
13338
|
+
typeDefs += `
|
|
13339
|
+
recordingVideoUrl(job_id: ID!): String
|
|
13340
|
+
`;
|
|
13251
13341
|
typeDefs += `
|
|
13252
13342
|
workflowTriggers(workflow: ID!): [WorkflowTrigger!]!
|
|
13253
13343
|
`;
|
|
@@ -14194,6 +14284,13 @@ type LiteLLMModel {
|
|
|
14194
14284
|
if (!context.user) throw new Error("Authentication required");
|
|
14195
14285
|
return recallService.getUsage();
|
|
14196
14286
|
};
|
|
14287
|
+
resolvers.Query["recordingVideoUrl"] = async (_, args, context) => {
|
|
14288
|
+
await assertOwnsTranscriptionJob(args.job_id, context);
|
|
14289
|
+
const { db } = context;
|
|
14290
|
+
const row = await db.from("transcription_jobs").select(["recall_recording_id"]).where({ id: args.job_id }).first();
|
|
14291
|
+
if (!row?.recall_recording_id) return null;
|
|
14292
|
+
return recallService.getRecordingVideoUrl(row.recall_recording_id);
|
|
14293
|
+
};
|
|
14197
14294
|
resolvers.Query["evals"] = async (_, args, context, info) => {
|
|
14198
14295
|
const requestedFields = getRequestedFields(info);
|
|
14199
14296
|
return {
|
|
@@ -16798,9 +16895,12 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
|
|
|
16798
16895
|
}
|
|
16799
16896
|
const approvedTools = req.body.approvedTools ? typeof req.body.approvedTools === "string" ? JSON.parse(req.body.approvedTools) : req.body.approvedTools : [];
|
|
16800
16897
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
16801
|
-
const instructions =
|
|
16898
|
+
const instructions = withGlossary(
|
|
16899
|
+
customInstructions ? `${agent.instructions}
|
|
16802
16900
|
|
|
16803
|
-
${customInstructions}` : agent.instructions
|
|
16901
|
+
${customInstructions}` : agent.instructions,
|
|
16902
|
+
agent.tools
|
|
16903
|
+
);
|
|
16804
16904
|
if (headers.session) markStreamActive(headers.session);
|
|
16805
16905
|
const turnStartedAt = Date.now();
|
|
16806
16906
|
let result;
|
|
@@ -16936,9 +17036,12 @@ ${customInstructions}` : agent.instructions;
|
|
|
16936
17036
|
return;
|
|
16937
17037
|
} else {
|
|
16938
17038
|
const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
|
|
16939
|
-
const instructions =
|
|
17039
|
+
const instructions = withGlossary(
|
|
17040
|
+
customInstructions ? `${agent.instructions}
|
|
16940
17041
|
|
|
16941
|
-
${customInstructions}` : agent.instructions
|
|
17042
|
+
${customInstructions}` : agent.instructions,
|
|
17043
|
+
agent.tools
|
|
17044
|
+
);
|
|
16942
17045
|
let response;
|
|
16943
17046
|
try {
|
|
16944
17047
|
response = await generateSync({
|
|
@@ -21270,7 +21373,10 @@ var transcriptionsContext = new ExuluContext2({
|
|
|
21270
21373
|
// Link back to the Recall recording so the mixed video stays reachable
|
|
21271
21374
|
// (resolve a fresh URL via ExuluRecall.getRecordingVideoUrl — it expires
|
|
21272
21375
|
// after six hours). Null for Whisper uploads.
|
|
21273
|
-
{ name: "recall_recording_id", type: "text" }
|
|
21376
|
+
{ name: "recall_recording_id", type: "text" },
|
|
21377
|
+
// Permanent local copy of the mixed video, present only when
|
|
21378
|
+
// RECALL_STORE_VIDEO_LOCALLY=true was set at recording time.
|
|
21379
|
+
{ name: "video", type: "file" }
|
|
21274
21380
|
],
|
|
21275
21381
|
sources: [],
|
|
21276
21382
|
active: true,
|
|
@@ -21697,6 +21803,11 @@ var ExuluApp = class {
|
|
|
21697
21803
|
get audit() {
|
|
21698
21804
|
return this._audit ?? getAuditLogger(this._config ?? {});
|
|
21699
21805
|
}
|
|
21806
|
+
/** The deployment's ExuluConfig, for background/webhook code paths that
|
|
21807
|
+
* have no request to read it off (e.g. the Recall webhook handler). */
|
|
21808
|
+
get config() {
|
|
21809
|
+
return this._config ?? {};
|
|
21810
|
+
}
|
|
21700
21811
|
embeddings = {
|
|
21701
21812
|
generate: {
|
|
21702
21813
|
one: async ({ context: contextId, item: itemId }) => {
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -156,15 +156,21 @@ async function main() {
|
|
|
156
156
|
return;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// We deliberately do NOT skip running setup.sh just because the venv
|
|
160
|
+
// directory and a `python` binary already exist (pythonEnvironmentExists()
|
|
161
|
+
// only checks that much, not that requirements.txt is fully installed).
|
|
162
|
+
// ee/python/setup.sh is itself idempotent — it only skips *recreating* the
|
|
163
|
+
// venv, never skips `pip install -r requirements.txt` — so always invoking
|
|
164
|
+
// it is cheap when nothing is missing (a few seconds) and is the only way
|
|
165
|
+
// to guarantee a venv created before a new requirement was added (e.g.
|
|
166
|
+
// python-docx, 2026-09-11) actually gets it installed. This used to return
|
|
167
|
+
// early here whenever the venv folder was merely present, which silently
|
|
168
|
+
// left newly-added dependencies uninstalled across any install that reused
|
|
169
|
+
// an older venv — see the docx-manipulation skill incident on dx-algi,
|
|
170
|
+
// 2026-09-21.
|
|
160
171
|
if (pythonEnvironmentExists()) {
|
|
161
|
-
console.log(`${colors.
|
|
172
|
+
console.log(`${colors.blue}ℹ${colors.reset} Python environment directory already exists — verifying and installing any missing dependencies...`);
|
|
162
173
|
console.log('');
|
|
163
|
-
console.log('To rebuild, run:');
|
|
164
|
-
console.log(` ${colors.green}import { setupPythonEnvironment } from '@exulu/backend';${colors.reset}`);
|
|
165
|
-
console.log(` ${colors.green}await setupPythonEnvironment({ force: true });${colors.reset}`);
|
|
166
|
-
console.log('');
|
|
167
|
-
return;
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
// Run setup
|