@exulu/backend 1.59.0 → 1.60.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.
|
@@ -262,7 +262,8 @@ var spawnLiteLLM = (cfg) => {
|
|
|
262
262
|
log(
|
|
263
263
|
`Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
|
|
264
264
|
);
|
|
265
|
-
const { DEBUG: _debug, ...
|
|
265
|
+
const { DEBUG: _debug, ...rest } = process.env;
|
|
266
|
+
const childEnv = { ...rest, DEBUG: "false" };
|
|
266
267
|
const child = spawn(
|
|
267
268
|
cfg.litellmBin,
|
|
268
269
|
[
|
|
@@ -275,7 +276,7 @@ var spawnLiteLLM = (cfg) => {
|
|
|
275
276
|
],
|
|
276
277
|
{
|
|
277
278
|
stdio: ["ignore", "pipe", "pipe"],
|
|
278
|
-
env:
|
|
279
|
+
env: childEnv
|
|
279
280
|
}
|
|
280
281
|
);
|
|
281
282
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -743,7 +744,7 @@ var ExuluTool = class {
|
|
|
743
744
|
});
|
|
744
745
|
providerapikey = resolved.apiKey;
|
|
745
746
|
}
|
|
746
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
747
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-PLLM2CJL.js");
|
|
747
748
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
748
749
|
[this],
|
|
749
750
|
[],
|
package/dist/index.cjs
CHANGED
|
@@ -429,7 +429,8 @@ var init_supervisor = __esm({
|
|
|
429
429
|
log(
|
|
430
430
|
`Spawning LiteLLM: ${cfg.litellmBin} --config ${cfg.configPath} --port ${cfg.port} --host ${cfg.host}`
|
|
431
431
|
);
|
|
432
|
-
const { DEBUG: _debug, ...
|
|
432
|
+
const { DEBUG: _debug, ...rest } = process.env;
|
|
433
|
+
const childEnv = { ...rest, DEBUG: "false" };
|
|
433
434
|
const child = (0, import_node_child_process.spawn)(
|
|
434
435
|
cfg.litellmBin,
|
|
435
436
|
[
|
|
@@ -442,7 +443,7 @@ var init_supervisor = __esm({
|
|
|
442
443
|
],
|
|
443
444
|
{
|
|
444
445
|
stdio: ["ignore", "pipe", "pipe"],
|
|
445
|
-
env:
|
|
446
|
+
env: childEnv
|
|
446
447
|
}
|
|
447
448
|
);
|
|
448
449
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -18790,6 +18791,217 @@ var emailTool = new ExuluTool({
|
|
|
18790
18791
|
}
|
|
18791
18792
|
});
|
|
18792
18793
|
|
|
18794
|
+
// src/templates/tools/transcribe.ts
|
|
18795
|
+
init_cjs_shims();
|
|
18796
|
+
init_tool();
|
|
18797
|
+
init_supervisor();
|
|
18798
|
+
init_uppy();
|
|
18799
|
+
var import_node_crypto8 = require("crypto");
|
|
18800
|
+
var import_promises3 = require("fs/promises");
|
|
18801
|
+
var import_node_path5 = require("path");
|
|
18802
|
+
var import_zod20 = require("zod");
|
|
18803
|
+
var SANDBOX_ROOT = "/tmp/exulu-sessions";
|
|
18804
|
+
var parseSandboxPath = (input) => {
|
|
18805
|
+
const stripped = input.startsWith("file://") ? input.slice("file://".length) : input;
|
|
18806
|
+
if (!stripped.startsWith(`${SANDBOX_ROOT}/`)) return null;
|
|
18807
|
+
const tail = stripped.slice(SANDBOX_ROOT.length + 1);
|
|
18808
|
+
const slash = tail.indexOf("/");
|
|
18809
|
+
if (slash < 1) return null;
|
|
18810
|
+
const sessionId = tail.slice(0, slash);
|
|
18811
|
+
const relPath = tail.slice(slash + 1);
|
|
18812
|
+
if (!relPath) return null;
|
|
18813
|
+
return { sessionId, relPath };
|
|
18814
|
+
};
|
|
18815
|
+
var audioMimetypeFromExtension = (filename) => {
|
|
18816
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
18817
|
+
switch (ext) {
|
|
18818
|
+
case "mp3":
|
|
18819
|
+
return "audio/mpeg";
|
|
18820
|
+
case "m4a":
|
|
18821
|
+
case "mp4":
|
|
18822
|
+
return "audio/mp4";
|
|
18823
|
+
case "wav":
|
|
18824
|
+
return "audio/wav";
|
|
18825
|
+
case "ogg":
|
|
18826
|
+
case "oga":
|
|
18827
|
+
return "audio/ogg";
|
|
18828
|
+
case "flac":
|
|
18829
|
+
return "audio/flac";
|
|
18830
|
+
case "webm":
|
|
18831
|
+
return "audio/webm";
|
|
18832
|
+
case "aac":
|
|
18833
|
+
return "audio/aac";
|
|
18834
|
+
case "mpga":
|
|
18835
|
+
case "mpeg":
|
|
18836
|
+
return "audio/mpeg";
|
|
18837
|
+
default:
|
|
18838
|
+
throw new Error(
|
|
18839
|
+
`Unable to infer an audio mimetype from filename "${filename}". Supported extensions: mp3, m4a, mp4, wav, ogg, flac, webm, aac, mpga.`
|
|
18840
|
+
);
|
|
18841
|
+
}
|
|
18842
|
+
};
|
|
18843
|
+
var transcribeTool = new ExuluTool({
|
|
18844
|
+
id: "transcribe_audio",
|
|
18845
|
+
name: "Transcribe Audio",
|
|
18846
|
+
description: "Transcribe an audio file (mp3, wav, m4a, etc.) from a URL to text using the configured speech-to-text model. The transcript is stored as a .txt file on S3 and the URL is returned; use this for clips that may be too long to inline in the conversation.",
|
|
18847
|
+
inputSchema: import_zod20.z.object({
|
|
18848
|
+
audio_url: import_zod20.z.string().describe(
|
|
18849
|
+
"Location of the audio file to transcribe. Accepts a publicly fetchable URL (https URL or presigned S3 URL), or a sandbox path such as '/tmp/exulu-sessions/<sessionId>/<file>' or 'file:///tmp/exulu-sessions/<sessionId>/<file>' \u2014 sandbox paths are resolved to their persisted S3 copy."
|
|
18850
|
+
),
|
|
18851
|
+
language: import_zod20.z.string().optional().describe(
|
|
18852
|
+
"ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect."
|
|
18853
|
+
)
|
|
18854
|
+
}),
|
|
18855
|
+
type: "function",
|
|
18856
|
+
config: [{
|
|
18857
|
+
name: "default_language",
|
|
18858
|
+
description: "ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect.",
|
|
18859
|
+
type: "string",
|
|
18860
|
+
default: void 0
|
|
18861
|
+
}],
|
|
18862
|
+
execute: async ({ audio_url, language, user, exuluConfig, sessionID }) => {
|
|
18863
|
+
if (!language && exuluConfig?.default_language) {
|
|
18864
|
+
language = exuluConfig?.default_language;
|
|
18865
|
+
} else {
|
|
18866
|
+
language = "en";
|
|
18867
|
+
}
|
|
18868
|
+
language = exuluConfig?.default_language;
|
|
18869
|
+
console.log("[EXULU] Exulu config", exuluConfig);
|
|
18870
|
+
if (!isLiteLLMEnabled()) {
|
|
18871
|
+
console.error("[EXULU] Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true').");
|
|
18872
|
+
throw new Error(
|
|
18873
|
+
"Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true')."
|
|
18874
|
+
);
|
|
18875
|
+
}
|
|
18876
|
+
if (!process.env.TRANSCRIPTION_MODEL) {
|
|
18877
|
+
console.error("[EXULU] TRANSCRIPTION_MODEL env var is not set.");
|
|
18878
|
+
throw new Error("TRANSCRIPTION_MODEL env var is not set.");
|
|
18879
|
+
}
|
|
18880
|
+
if (!exuluConfig?.fileUploads) {
|
|
18881
|
+
console.error("[EXULU] File uploads are not configured; the transcribe tool requires S3 to store transcripts.");
|
|
18882
|
+
throw new Error(
|
|
18883
|
+
"File uploads are not configured; the transcribe tool requires S3 to store transcripts."
|
|
18884
|
+
);
|
|
18885
|
+
}
|
|
18886
|
+
const sandboxPath = parseSandboxPath(audio_url);
|
|
18887
|
+
let buffer;
|
|
18888
|
+
let mimetype;
|
|
18889
|
+
let originalname;
|
|
18890
|
+
if (sandboxPath) {
|
|
18891
|
+
if (!user?.id) {
|
|
18892
|
+
throw new Error(
|
|
18893
|
+
"Sandbox audio paths require an authenticated user; got no user on the tool call."
|
|
18894
|
+
);
|
|
18895
|
+
}
|
|
18896
|
+
if (sessionID && sandboxPath.sessionId !== sessionID) {
|
|
18897
|
+
throw new Error(
|
|
18898
|
+
`Refusing to transcribe an audio file from a different session's sandbox (path session=${sandboxPath.sessionId}, current session=${sessionID}).`
|
|
18899
|
+
);
|
|
18900
|
+
}
|
|
18901
|
+
const rawKey = `user_${user.id}/sessions/${sandboxPath.sessionId}/${sandboxPath.relPath}`;
|
|
18902
|
+
console.log("[EXULU] Transcribing audio from sandbox path", {
|
|
18903
|
+
rawKey
|
|
18904
|
+
});
|
|
18905
|
+
const matches = await listS3ObjectsByPrefix(rawKey, exuluConfig);
|
|
18906
|
+
const found = matches.find((m) => m.key.endsWith(rawKey));
|
|
18907
|
+
if (!found) {
|
|
18908
|
+
console.error("[EXULU] Sandbox audio file not found in S3 storage at", {
|
|
18909
|
+
rawKey,
|
|
18910
|
+
matches
|
|
18911
|
+
});
|
|
18912
|
+
throw new Error(
|
|
18913
|
+
`Sandbox audio file not found in S3 storage at "${rawKey}". The file may not have been persisted yet \u2014 try again after the sandbox flushes it.`
|
|
18914
|
+
);
|
|
18915
|
+
}
|
|
18916
|
+
buffer = await getS3ObjectBytes(found.key, exuluConfig);
|
|
18917
|
+
originalname = decodeURIComponent(
|
|
18918
|
+
sandboxPath.relPath.split("/").pop() || "audio"
|
|
18919
|
+
);
|
|
18920
|
+
mimetype = audioMimetypeFromExtension(originalname);
|
|
18921
|
+
} else {
|
|
18922
|
+
console.log("[EXULU] Fetching audio from URL", {
|
|
18923
|
+
audio_url
|
|
18924
|
+
});
|
|
18925
|
+
const upstream = await fetch(audio_url);
|
|
18926
|
+
if (!upstream.ok) {
|
|
18927
|
+
console.error("[EXULU] Failed to fetch audio from", {
|
|
18928
|
+
audio_url,
|
|
18929
|
+
upstream
|
|
18930
|
+
});
|
|
18931
|
+
throw new Error(
|
|
18932
|
+
`Failed to fetch audio from ${audio_url}: ${upstream.status} ${upstream.statusText}`
|
|
18933
|
+
);
|
|
18934
|
+
}
|
|
18935
|
+
mimetype = upstream.headers.get("content-type") || "audio/mpeg";
|
|
18936
|
+
if (!mimetype.startsWith("audio/")) {
|
|
18937
|
+
throw new Error(
|
|
18938
|
+
`URL did not return an audio file (content-type: ${mimetype}).`
|
|
18939
|
+
);
|
|
18940
|
+
}
|
|
18941
|
+
buffer = Buffer.from(await upstream.arrayBuffer());
|
|
18942
|
+
originalname = "audio";
|
|
18943
|
+
try {
|
|
18944
|
+
const pathname = new URL(audio_url).pathname;
|
|
18945
|
+
const last = pathname.split("/").pop();
|
|
18946
|
+
if (last) originalname = decodeURIComponent(last);
|
|
18947
|
+
} catch {
|
|
18948
|
+
}
|
|
18949
|
+
}
|
|
18950
|
+
const { text } = await transcribeAudio({
|
|
18951
|
+
file: { buffer, originalname, mimetype },
|
|
18952
|
+
language
|
|
18953
|
+
});
|
|
18954
|
+
const transcriptBuffer = Buffer.from(text, "utf-8");
|
|
18955
|
+
const transcriptFilename = `${(0, import_node_crypto8.randomUUID)()}.txt`;
|
|
18956
|
+
const transcriptKey = sessionID ? `sessions/${sessionID}/transcripts/${transcriptFilename}` : `transcripts/${transcriptFilename}`;
|
|
18957
|
+
console.log("[EXULU] Uploading transcript to S3", {
|
|
18958
|
+
transcriptFilename,
|
|
18959
|
+
transcriptKey
|
|
18960
|
+
});
|
|
18961
|
+
const url = await uploadFile(
|
|
18962
|
+
transcriptBuffer,
|
|
18963
|
+
transcriptKey,
|
|
18964
|
+
exuluConfig,
|
|
18965
|
+
{ contentType: "text/plain" },
|
|
18966
|
+
user?.id
|
|
18967
|
+
);
|
|
18968
|
+
console.log("[EXULU] Uploaded transcript to S3", {
|
|
18969
|
+
url
|
|
18970
|
+
});
|
|
18971
|
+
let sandboxLocalPath;
|
|
18972
|
+
if (sessionID) {
|
|
18973
|
+
sandboxLocalPath = (0, import_node_path5.join)(
|
|
18974
|
+
SANDBOX_ROOT,
|
|
18975
|
+
sessionID,
|
|
18976
|
+
"transcripts",
|
|
18977
|
+
transcriptFilename
|
|
18978
|
+
);
|
|
18979
|
+
console.log("[EXULU] Mirroring transcript into session sandbox", {
|
|
18980
|
+
sandboxLocalPath
|
|
18981
|
+
});
|
|
18982
|
+
try {
|
|
18983
|
+
await (0, import_promises3.mkdir)((0, import_node_path5.dirname)(sandboxLocalPath), { recursive: true });
|
|
18984
|
+
await (0, import_promises3.writeFile)(sandboxLocalPath, transcriptBuffer);
|
|
18985
|
+
} catch (err) {
|
|
18986
|
+
console.error(
|
|
18987
|
+
`[EXULU] Failed to write transcript to sandbox dir ${sandboxLocalPath}; S3 copy is unaffected.`,
|
|
18988
|
+
err
|
|
18989
|
+
);
|
|
18990
|
+
sandboxLocalPath = void 0;
|
|
18991
|
+
}
|
|
18992
|
+
}
|
|
18993
|
+
console.log("[EXULU] Transcribed audio successfully", {
|
|
18994
|
+
text,
|
|
18995
|
+
url,
|
|
18996
|
+
sandboxLocalPath,
|
|
18997
|
+
length: text.length
|
|
18998
|
+
});
|
|
18999
|
+
return {
|
|
19000
|
+
result: sandboxLocalPath ? `Transcript stored at: ${url} (also available in the session sandbox at ${sandboxLocalPath}, ${text.length} characters).` : `${url}`
|
|
19001
|
+
};
|
|
19002
|
+
}
|
|
19003
|
+
});
|
|
19004
|
+
|
|
18793
19005
|
// src/validators/postgres-name.ts
|
|
18794
19006
|
init_cjs_shims();
|
|
18795
19007
|
var isValidPostgresName = (id) => {
|
|
@@ -19102,12 +19314,17 @@ var ExuluApp = class {
|
|
|
19102
19314
|
...providers ?? []
|
|
19103
19315
|
];
|
|
19104
19316
|
this._config = config;
|
|
19317
|
+
const transcriptionTools = [];
|
|
19318
|
+
if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
|
|
19319
|
+
transcriptionTools.push(transcribeTool);
|
|
19320
|
+
}
|
|
19105
19321
|
this._tools = [
|
|
19106
19322
|
...tools ?? [],
|
|
19107
19323
|
...todoTools,
|
|
19108
19324
|
...questionTools,
|
|
19109
19325
|
...perplexityTools,
|
|
19110
|
-
emailTool
|
|
19326
|
+
emailTool,
|
|
19327
|
+
...transcriptionTools
|
|
19111
19328
|
// Because agents are stored in the database, we add those as tools
|
|
19112
19329
|
// at request time, not during ExuluApp initialization. We add them
|
|
19113
19330
|
// in the grahql tools resolver.
|
|
@@ -21274,7 +21491,7 @@ init_cjs_shims();
|
|
|
21274
21491
|
// src/exulu/litellm/db-init.ts
|
|
21275
21492
|
init_cjs_shims();
|
|
21276
21493
|
var import_node_fs6 = require("fs");
|
|
21277
|
-
var
|
|
21494
|
+
var import_node_path6 = require("path");
|
|
21278
21495
|
var import_node_child_process5 = require("child_process");
|
|
21279
21496
|
var import_pg = require("pg");
|
|
21280
21497
|
|
|
@@ -21344,7 +21561,7 @@ ${WARNING_BANNER}`);
|
|
|
21344
21561
|
};
|
|
21345
21562
|
var log2 = (line) => console.log(`[EXULU-LITELLM] ${line}`);
|
|
21346
21563
|
var initLiteLLMDatabase = async (packageRoot) => {
|
|
21347
|
-
const configPath = process.env.LITELLM_CONFIG_PATH ?? (0,
|
|
21564
|
+
const configPath = process.env.LITELLM_CONFIG_PATH ?? (0, import_node_path6.resolve)(packageRoot, "./config.litellm.yaml");
|
|
21348
21565
|
const safety = checkLiteLLMDatabaseSafety(configPath);
|
|
21349
21566
|
if (safety.ok && safety.reason === "no-litellm-db-mode") return;
|
|
21350
21567
|
if (!safety.ok && safety.reason === "unparseable-url") {
|
|
@@ -21482,13 +21699,13 @@ var initLiteLLMDatabase = async (packageRoot) => {
|
|
|
21482
21699
|
]);
|
|
21483
21700
|
return;
|
|
21484
21701
|
}
|
|
21485
|
-
const venvBin = (0,
|
|
21486
|
-
const prismaCli = (0,
|
|
21487
|
-
const litellmProxyDir = (0,
|
|
21702
|
+
const venvBin = (0, import_node_path6.resolve)(packageRoot, "ee/python/.venv/bin");
|
|
21703
|
+
const prismaCli = (0, import_node_path6.resolve)(venvBin, "prisma");
|
|
21704
|
+
const litellmProxyDir = (0, import_node_path6.resolve)(
|
|
21488
21705
|
packageRoot,
|
|
21489
21706
|
"ee/python/.venv/lib/python3.12/site-packages/litellm/proxy"
|
|
21490
21707
|
);
|
|
21491
|
-
const schemaPath = (0,
|
|
21708
|
+
const schemaPath = (0, import_node_path6.resolve)(litellmProxyDir, "schema.prisma");
|
|
21492
21709
|
if (!(0, import_node_fs6.existsSync)(prismaCli)) {
|
|
21493
21710
|
warn([
|
|
21494
21711
|
`Prisma CLI not found at ${prismaCli}.`,
|
|
@@ -22078,7 +22295,7 @@ init_cjs_shims();
|
|
|
22078
22295
|
var fs5 = __toESM(require("fs"), 1);
|
|
22079
22296
|
var path2 = __toESM(require("path"), 1);
|
|
22080
22297
|
var import_ai14 = require("ai");
|
|
22081
|
-
var
|
|
22298
|
+
var import_zod21 = require("zod");
|
|
22082
22299
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
22083
22300
|
var import_crypto = require("crypto");
|
|
22084
22301
|
init_with_retry();
|
|
@@ -22433,15 +22650,15 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
22433
22650
|
const result = await (0, import_ai14.generateText)({
|
|
22434
22651
|
model,
|
|
22435
22652
|
output: import_ai14.Output.object({
|
|
22436
|
-
schema:
|
|
22437
|
-
needs_correction:
|
|
22438
|
-
corrected_text:
|
|
22439
|
-
current_page_table:
|
|
22440
|
-
headers:
|
|
22441
|
-
is_continuation:
|
|
22653
|
+
schema: import_zod21.z.object({
|
|
22654
|
+
needs_correction: import_zod21.z.boolean(),
|
|
22655
|
+
corrected_text: import_zod21.z.string().nullable(),
|
|
22656
|
+
current_page_table: import_zod21.z.object({
|
|
22657
|
+
headers: import_zod21.z.array(import_zod21.z.string()),
|
|
22658
|
+
is_continuation: import_zod21.z.boolean()
|
|
22442
22659
|
}).nullable(),
|
|
22443
|
-
confidence:
|
|
22444
|
-
reasoning:
|
|
22660
|
+
confidence: import_zod21.z.enum(["high", "medium", "low"]),
|
|
22661
|
+
reasoning: import_zod21.z.string()
|
|
22445
22662
|
})
|
|
22446
22663
|
}),
|
|
22447
22664
|
messages: [
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
vectorSearch,
|
|
47
47
|
waitForLiteLLMReady,
|
|
48
48
|
withRetry
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-23YNGK3V.js";
|
|
50
50
|
import {
|
|
51
51
|
findLiteLLMModel
|
|
52
52
|
} from "./chunk-YS27XOXI.js";
|
|
@@ -11004,6 +11004,213 @@ var emailTool = new ExuluTool({
|
|
|
11004
11004
|
}
|
|
11005
11005
|
});
|
|
11006
11006
|
|
|
11007
|
+
// src/templates/tools/transcribe.ts
|
|
11008
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
11009
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
11010
|
+
import { dirname, join as join2 } from "path";
|
|
11011
|
+
import { z as z10 } from "zod";
|
|
11012
|
+
var SANDBOX_ROOT = "/tmp/exulu-sessions";
|
|
11013
|
+
var parseSandboxPath = (input) => {
|
|
11014
|
+
const stripped = input.startsWith("file://") ? input.slice("file://".length) : input;
|
|
11015
|
+
if (!stripped.startsWith(`${SANDBOX_ROOT}/`)) return null;
|
|
11016
|
+
const tail = stripped.slice(SANDBOX_ROOT.length + 1);
|
|
11017
|
+
const slash = tail.indexOf("/");
|
|
11018
|
+
if (slash < 1) return null;
|
|
11019
|
+
const sessionId = tail.slice(0, slash);
|
|
11020
|
+
const relPath = tail.slice(slash + 1);
|
|
11021
|
+
if (!relPath) return null;
|
|
11022
|
+
return { sessionId, relPath };
|
|
11023
|
+
};
|
|
11024
|
+
var audioMimetypeFromExtension = (filename) => {
|
|
11025
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
11026
|
+
switch (ext) {
|
|
11027
|
+
case "mp3":
|
|
11028
|
+
return "audio/mpeg";
|
|
11029
|
+
case "m4a":
|
|
11030
|
+
case "mp4":
|
|
11031
|
+
return "audio/mp4";
|
|
11032
|
+
case "wav":
|
|
11033
|
+
return "audio/wav";
|
|
11034
|
+
case "ogg":
|
|
11035
|
+
case "oga":
|
|
11036
|
+
return "audio/ogg";
|
|
11037
|
+
case "flac":
|
|
11038
|
+
return "audio/flac";
|
|
11039
|
+
case "webm":
|
|
11040
|
+
return "audio/webm";
|
|
11041
|
+
case "aac":
|
|
11042
|
+
return "audio/aac";
|
|
11043
|
+
case "mpga":
|
|
11044
|
+
case "mpeg":
|
|
11045
|
+
return "audio/mpeg";
|
|
11046
|
+
default:
|
|
11047
|
+
throw new Error(
|
|
11048
|
+
`Unable to infer an audio mimetype from filename "${filename}". Supported extensions: mp3, m4a, mp4, wav, ogg, flac, webm, aac, mpga.`
|
|
11049
|
+
);
|
|
11050
|
+
}
|
|
11051
|
+
};
|
|
11052
|
+
var transcribeTool = new ExuluTool({
|
|
11053
|
+
id: "transcribe_audio",
|
|
11054
|
+
name: "Transcribe Audio",
|
|
11055
|
+
description: "Transcribe an audio file (mp3, wav, m4a, etc.) from a URL to text using the configured speech-to-text model. The transcript is stored as a .txt file on S3 and the URL is returned; use this for clips that may be too long to inline in the conversation.",
|
|
11056
|
+
inputSchema: z10.object({
|
|
11057
|
+
audio_url: z10.string().describe(
|
|
11058
|
+
"Location of the audio file to transcribe. Accepts a publicly fetchable URL (https URL or presigned S3 URL), or a sandbox path such as '/tmp/exulu-sessions/<sessionId>/<file>' or 'file:///tmp/exulu-sessions/<sessionId>/<file>' \u2014 sandbox paths are resolved to their persisted S3 copy."
|
|
11059
|
+
),
|
|
11060
|
+
language: z10.string().optional().describe(
|
|
11061
|
+
"ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect."
|
|
11062
|
+
)
|
|
11063
|
+
}),
|
|
11064
|
+
type: "function",
|
|
11065
|
+
config: [{
|
|
11066
|
+
name: "default_language",
|
|
11067
|
+
description: "ISO-639-1 language code of the audio, e.g. 'en' or 'de'. Omit to let the model auto-detect.",
|
|
11068
|
+
type: "string",
|
|
11069
|
+
default: void 0
|
|
11070
|
+
}],
|
|
11071
|
+
execute: async ({ audio_url, language, user, exuluConfig, sessionID }) => {
|
|
11072
|
+
if (!language && exuluConfig?.default_language) {
|
|
11073
|
+
language = exuluConfig?.default_language;
|
|
11074
|
+
} else {
|
|
11075
|
+
language = "en";
|
|
11076
|
+
}
|
|
11077
|
+
language = exuluConfig?.default_language;
|
|
11078
|
+
console.log("[EXULU] Exulu config", exuluConfig);
|
|
11079
|
+
if (!isLiteLLMEnabled()) {
|
|
11080
|
+
console.error("[EXULU] Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true').");
|
|
11081
|
+
throw new Error(
|
|
11082
|
+
"Speech-to-text is not enabled on this deployment (EXULU_USE_LITELLM is not 'true')."
|
|
11083
|
+
);
|
|
11084
|
+
}
|
|
11085
|
+
if (!process.env.TRANSCRIPTION_MODEL) {
|
|
11086
|
+
console.error("[EXULU] TRANSCRIPTION_MODEL env var is not set.");
|
|
11087
|
+
throw new Error("TRANSCRIPTION_MODEL env var is not set.");
|
|
11088
|
+
}
|
|
11089
|
+
if (!exuluConfig?.fileUploads) {
|
|
11090
|
+
console.error("[EXULU] File uploads are not configured; the transcribe tool requires S3 to store transcripts.");
|
|
11091
|
+
throw new Error(
|
|
11092
|
+
"File uploads are not configured; the transcribe tool requires S3 to store transcripts."
|
|
11093
|
+
);
|
|
11094
|
+
}
|
|
11095
|
+
const sandboxPath = parseSandboxPath(audio_url);
|
|
11096
|
+
let buffer;
|
|
11097
|
+
let mimetype;
|
|
11098
|
+
let originalname;
|
|
11099
|
+
if (sandboxPath) {
|
|
11100
|
+
if (!user?.id) {
|
|
11101
|
+
throw new Error(
|
|
11102
|
+
"Sandbox audio paths require an authenticated user; got no user on the tool call."
|
|
11103
|
+
);
|
|
11104
|
+
}
|
|
11105
|
+
if (sessionID && sandboxPath.sessionId !== sessionID) {
|
|
11106
|
+
throw new Error(
|
|
11107
|
+
`Refusing to transcribe an audio file from a different session's sandbox (path session=${sandboxPath.sessionId}, current session=${sessionID}).`
|
|
11108
|
+
);
|
|
11109
|
+
}
|
|
11110
|
+
const rawKey = `user_${user.id}/sessions/${sandboxPath.sessionId}/${sandboxPath.relPath}`;
|
|
11111
|
+
console.log("[EXULU] Transcribing audio from sandbox path", {
|
|
11112
|
+
rawKey
|
|
11113
|
+
});
|
|
11114
|
+
const matches = await listS3ObjectsByPrefix(rawKey, exuluConfig);
|
|
11115
|
+
const found = matches.find((m) => m.key.endsWith(rawKey));
|
|
11116
|
+
if (!found) {
|
|
11117
|
+
console.error("[EXULU] Sandbox audio file not found in S3 storage at", {
|
|
11118
|
+
rawKey,
|
|
11119
|
+
matches
|
|
11120
|
+
});
|
|
11121
|
+
throw new Error(
|
|
11122
|
+
`Sandbox audio file not found in S3 storage at "${rawKey}". The file may not have been persisted yet \u2014 try again after the sandbox flushes it.`
|
|
11123
|
+
);
|
|
11124
|
+
}
|
|
11125
|
+
buffer = await getS3ObjectBytes(found.key, exuluConfig);
|
|
11126
|
+
originalname = decodeURIComponent(
|
|
11127
|
+
sandboxPath.relPath.split("/").pop() || "audio"
|
|
11128
|
+
);
|
|
11129
|
+
mimetype = audioMimetypeFromExtension(originalname);
|
|
11130
|
+
} else {
|
|
11131
|
+
console.log("[EXULU] Fetching audio from URL", {
|
|
11132
|
+
audio_url
|
|
11133
|
+
});
|
|
11134
|
+
const upstream = await fetch(audio_url);
|
|
11135
|
+
if (!upstream.ok) {
|
|
11136
|
+
console.error("[EXULU] Failed to fetch audio from", {
|
|
11137
|
+
audio_url,
|
|
11138
|
+
upstream
|
|
11139
|
+
});
|
|
11140
|
+
throw new Error(
|
|
11141
|
+
`Failed to fetch audio from ${audio_url}: ${upstream.status} ${upstream.statusText}`
|
|
11142
|
+
);
|
|
11143
|
+
}
|
|
11144
|
+
mimetype = upstream.headers.get("content-type") || "audio/mpeg";
|
|
11145
|
+
if (!mimetype.startsWith("audio/")) {
|
|
11146
|
+
throw new Error(
|
|
11147
|
+
`URL did not return an audio file (content-type: ${mimetype}).`
|
|
11148
|
+
);
|
|
11149
|
+
}
|
|
11150
|
+
buffer = Buffer.from(await upstream.arrayBuffer());
|
|
11151
|
+
originalname = "audio";
|
|
11152
|
+
try {
|
|
11153
|
+
const pathname = new URL(audio_url).pathname;
|
|
11154
|
+
const last = pathname.split("/").pop();
|
|
11155
|
+
if (last) originalname = decodeURIComponent(last);
|
|
11156
|
+
} catch {
|
|
11157
|
+
}
|
|
11158
|
+
}
|
|
11159
|
+
const { text } = await transcribeAudio({
|
|
11160
|
+
file: { buffer, originalname, mimetype },
|
|
11161
|
+
language
|
|
11162
|
+
});
|
|
11163
|
+
const transcriptBuffer = Buffer.from(text, "utf-8");
|
|
11164
|
+
const transcriptFilename = `${randomUUID5()}.txt`;
|
|
11165
|
+
const transcriptKey = sessionID ? `sessions/${sessionID}/transcripts/${transcriptFilename}` : `transcripts/${transcriptFilename}`;
|
|
11166
|
+
console.log("[EXULU] Uploading transcript to S3", {
|
|
11167
|
+
transcriptFilename,
|
|
11168
|
+
transcriptKey
|
|
11169
|
+
});
|
|
11170
|
+
const url = await uploadFile(
|
|
11171
|
+
transcriptBuffer,
|
|
11172
|
+
transcriptKey,
|
|
11173
|
+
exuluConfig,
|
|
11174
|
+
{ contentType: "text/plain" },
|
|
11175
|
+
user?.id
|
|
11176
|
+
);
|
|
11177
|
+
console.log("[EXULU] Uploaded transcript to S3", {
|
|
11178
|
+
url
|
|
11179
|
+
});
|
|
11180
|
+
let sandboxLocalPath;
|
|
11181
|
+
if (sessionID) {
|
|
11182
|
+
sandboxLocalPath = join2(
|
|
11183
|
+
SANDBOX_ROOT,
|
|
11184
|
+
sessionID,
|
|
11185
|
+
"transcripts",
|
|
11186
|
+
transcriptFilename
|
|
11187
|
+
);
|
|
11188
|
+
console.log("[EXULU] Mirroring transcript into session sandbox", {
|
|
11189
|
+
sandboxLocalPath
|
|
11190
|
+
});
|
|
11191
|
+
try {
|
|
11192
|
+
await mkdir2(dirname(sandboxLocalPath), { recursive: true });
|
|
11193
|
+
await writeFile2(sandboxLocalPath, transcriptBuffer);
|
|
11194
|
+
} catch (err) {
|
|
11195
|
+
console.error(
|
|
11196
|
+
`[EXULU] Failed to write transcript to sandbox dir ${sandboxLocalPath}; S3 copy is unaffected.`,
|
|
11197
|
+
err
|
|
11198
|
+
);
|
|
11199
|
+
sandboxLocalPath = void 0;
|
|
11200
|
+
}
|
|
11201
|
+
}
|
|
11202
|
+
console.log("[EXULU] Transcribed audio successfully", {
|
|
11203
|
+
text,
|
|
11204
|
+
url,
|
|
11205
|
+
sandboxLocalPath,
|
|
11206
|
+
length: text.length
|
|
11207
|
+
});
|
|
11208
|
+
return {
|
|
11209
|
+
result: sandboxLocalPath ? `Transcript stored at: ${url} (also available in the session sandbox at ${sandboxLocalPath}, ${text.length} characters).` : `${url}`
|
|
11210
|
+
};
|
|
11211
|
+
}
|
|
11212
|
+
});
|
|
11213
|
+
|
|
11007
11214
|
// src/validators/postgres-name.ts
|
|
11008
11215
|
var isValidPostgresName = (id) => {
|
|
11009
11216
|
const regex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
@@ -11015,17 +11222,17 @@ var isValidPostgresName = (id) => {
|
|
|
11015
11222
|
// src/utils/python-setup.ts
|
|
11016
11223
|
import { exec as exec2 } from "child_process";
|
|
11017
11224
|
import { promisify as promisify2 } from "util";
|
|
11018
|
-
import { resolve, join as
|
|
11225
|
+
import { resolve, join as join3, dirname as dirname2 } from "path";
|
|
11019
11226
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
11020
11227
|
import { fileURLToPath } from "url";
|
|
11021
11228
|
var execAsync2 = promisify2(exec2);
|
|
11022
11229
|
function getPackageRoot() {
|
|
11023
11230
|
const currentFile = fileURLToPath(import.meta.url);
|
|
11024
|
-
let currentDir =
|
|
11231
|
+
let currentDir = dirname2(currentFile);
|
|
11025
11232
|
let attempts = 0;
|
|
11026
11233
|
const maxAttempts = 10;
|
|
11027
11234
|
while (attempts < maxAttempts) {
|
|
11028
|
-
const packageJsonPath =
|
|
11235
|
+
const packageJsonPath = join3(currentDir, "package.json");
|
|
11029
11236
|
if (existsSync2(packageJsonPath)) {
|
|
11030
11237
|
try {
|
|
11031
11238
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
@@ -11042,7 +11249,7 @@ function getPackageRoot() {
|
|
|
11042
11249
|
currentDir = parentDir;
|
|
11043
11250
|
attempts++;
|
|
11044
11251
|
}
|
|
11045
|
-
const fallback = resolve(
|
|
11252
|
+
const fallback = resolve(dirname2(fileURLToPath(import.meta.url)), "../..");
|
|
11046
11253
|
return fallback;
|
|
11047
11254
|
}
|
|
11048
11255
|
function getSetupScriptPath(packageRoot) {
|
|
@@ -11054,7 +11261,7 @@ function getVenvPath(packageRoot) {
|
|
|
11054
11261
|
function isPythonEnvironmentSetup(packageRoot) {
|
|
11055
11262
|
const root = packageRoot ?? getPackageRoot();
|
|
11056
11263
|
const venvPath = getVenvPath(root);
|
|
11057
|
-
const pythonPath =
|
|
11264
|
+
const pythonPath = join3(venvPath, "bin", "python");
|
|
11058
11265
|
return existsSync2(venvPath) && existsSync2(pythonPath);
|
|
11059
11266
|
}
|
|
11060
11267
|
async function setupPythonEnvironment(options = {}) {
|
|
@@ -11157,7 +11364,7 @@ Note: In Docker containers, ensure you install all three components:
|
|
|
11157
11364
|
async function validatePythonEnvironment(packageRoot, checkPackages = true) {
|
|
11158
11365
|
const root = packageRoot ?? getPackageRoot();
|
|
11159
11366
|
const venvPath = getVenvPath(root);
|
|
11160
|
-
const pythonPath =
|
|
11367
|
+
const pythonPath = join3(venvPath, "bin", "python");
|
|
11161
11368
|
if (!existsSync2(venvPath)) {
|
|
11162
11369
|
return {
|
|
11163
11370
|
valid: false,
|
|
@@ -11307,12 +11514,17 @@ var ExuluApp = class {
|
|
|
11307
11514
|
...providers ?? []
|
|
11308
11515
|
];
|
|
11309
11516
|
this._config = config;
|
|
11517
|
+
const transcriptionTools = [];
|
|
11518
|
+
if (process.env.TRANSCRIPTION_MODEL && config?.fileUploads && config?.fileUploads?.s3region && config?.fileUploads?.s3key && config?.fileUploads?.s3secret && config?.fileUploads?.s3Bucket) {
|
|
11519
|
+
transcriptionTools.push(transcribeTool);
|
|
11520
|
+
}
|
|
11310
11521
|
this._tools = [
|
|
11311
11522
|
...tools ?? [],
|
|
11312
11523
|
...todoTools,
|
|
11313
11524
|
...questionTools,
|
|
11314
11525
|
...perplexityTools,
|
|
11315
|
-
emailTool
|
|
11526
|
+
emailTool,
|
|
11527
|
+
...transcriptionTools
|
|
11316
11528
|
// Because agents are stored in the database, we add those as tools
|
|
11317
11529
|
// at request time, not during ExuluApp initialization. We add them
|
|
11318
11530
|
// in the grahql tools resolver.
|
|
@@ -14237,9 +14449,9 @@ var MarkdownChunker = class {
|
|
|
14237
14449
|
import * as fs4 from "fs";
|
|
14238
14450
|
import * as path from "path";
|
|
14239
14451
|
import { generateText as generateText5, Output as Output2 } from "ai";
|
|
14240
|
-
import { z as
|
|
14452
|
+
import { z as z11 } from "zod";
|
|
14241
14453
|
import pLimit from "p-limit";
|
|
14242
|
-
import { randomUUID as
|
|
14454
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
14243
14455
|
import * as mammoth from "mammoth";
|
|
14244
14456
|
import TurndownService from "turndown";
|
|
14245
14457
|
import WordExtractor from "word-extractor";
|
|
@@ -14248,17 +14460,17 @@ import { parseOfficeAsync as parseOfficeAsync2 } from "officeparser";
|
|
|
14248
14460
|
// src/utils/python-executor.ts
|
|
14249
14461
|
import { exec as exec3 } from "child_process";
|
|
14250
14462
|
import { promisify as promisify3 } from "util";
|
|
14251
|
-
import { resolve as resolve3, join as
|
|
14463
|
+
import { resolve as resolve3, join as join4, dirname as dirname3 } from "path";
|
|
14252
14464
|
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
14253
14465
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14254
14466
|
var execAsync3 = promisify3(exec3);
|
|
14255
14467
|
function getPackageRoot2() {
|
|
14256
14468
|
const currentFile = fileURLToPath2(import.meta.url);
|
|
14257
|
-
let currentDir =
|
|
14469
|
+
let currentDir = dirname3(currentFile);
|
|
14258
14470
|
let attempts = 0;
|
|
14259
14471
|
const maxAttempts = 10;
|
|
14260
14472
|
while (attempts < maxAttempts) {
|
|
14261
|
-
const packageJsonPath =
|
|
14473
|
+
const packageJsonPath = join4(currentDir, "package.json");
|
|
14262
14474
|
if (existsSync5(packageJsonPath)) {
|
|
14263
14475
|
try {
|
|
14264
14476
|
const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
|
|
@@ -14275,7 +14487,7 @@ function getPackageRoot2() {
|
|
|
14275
14487
|
currentDir = parentDir;
|
|
14276
14488
|
attempts++;
|
|
14277
14489
|
}
|
|
14278
|
-
return resolve3(
|
|
14490
|
+
return resolve3(dirname3(fileURLToPath2(import.meta.url)), "../..");
|
|
14279
14491
|
}
|
|
14280
14492
|
var PythonEnvironmentError = class extends Error {
|
|
14281
14493
|
constructor(message) {
|
|
@@ -14300,7 +14512,7 @@ function getVenvPath2(packageRoot) {
|
|
|
14300
14512
|
}
|
|
14301
14513
|
function getPythonExecutable(packageRoot) {
|
|
14302
14514
|
const venvPath = getVenvPath2(packageRoot);
|
|
14303
|
-
return
|
|
14515
|
+
return join4(venvPath, "bin", "python");
|
|
14304
14516
|
}
|
|
14305
14517
|
async function validatePythonEnvironmentForExecution(packageRoot) {
|
|
14306
14518
|
const validation = await validatePythonEnvironment(packageRoot);
|
|
@@ -14589,15 +14801,15 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
14589
14801
|
const result = await generateText5({
|
|
14590
14802
|
model,
|
|
14591
14803
|
output: Output2.object({
|
|
14592
|
-
schema:
|
|
14593
|
-
needs_correction:
|
|
14594
|
-
corrected_text:
|
|
14595
|
-
current_page_table:
|
|
14596
|
-
headers:
|
|
14597
|
-
is_continuation:
|
|
14804
|
+
schema: z11.object({
|
|
14805
|
+
needs_correction: z11.boolean(),
|
|
14806
|
+
corrected_text: z11.string().nullable(),
|
|
14807
|
+
current_page_table: z11.object({
|
|
14808
|
+
headers: z11.array(z11.string()),
|
|
14809
|
+
is_continuation: z11.boolean()
|
|
14598
14810
|
}).nullable(),
|
|
14599
|
-
confidence:
|
|
14600
|
-
reasoning:
|
|
14811
|
+
confidence: z11.enum(["high", "medium", "low"]),
|
|
14812
|
+
reasoning: z11.string()
|
|
14601
14813
|
})
|
|
14602
14814
|
}),
|
|
14603
14815
|
messages: [
|
|
@@ -14997,7 +15209,7 @@ var loadFile = async (file, name, tempDir) => {
|
|
|
14997
15209
|
if (!fileType) {
|
|
14998
15210
|
throw new Error("[EXULU] File name does not include extension, extension is required for document processing.");
|
|
14999
15211
|
}
|
|
15000
|
-
const UUID =
|
|
15212
|
+
const UUID = randomUUID6();
|
|
15001
15213
|
let buffer;
|
|
15002
15214
|
if (Buffer.isBuffer(file)) {
|
|
15003
15215
|
filePath = path.join(tempDir, `${UUID}.${fileType}`);
|
|
@@ -15027,7 +15239,7 @@ async function documentProcessor({
|
|
|
15027
15239
|
if (!license["advanced-document-processing"]) {
|
|
15028
15240
|
throw new Error("Advanced document processing is an enterprise feature, please add a valid Exulu enterprise license key to use it.");
|
|
15029
15241
|
}
|
|
15030
|
-
const uuid =
|
|
15242
|
+
const uuid = randomUUID6();
|
|
15031
15243
|
const tempDir = path.join(process.cwd(), "temp", uuid);
|
|
15032
15244
|
const localFilesAndFoldersToDelete = [tempDir];
|
|
15033
15245
|
console.log(`[EXULU] Temporary directory for processing document ${name}: ${tempDir}`);
|