@exulu/backend 3.7.1 → 3.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +249 -57
- package/dist/index.js +231 -48
- package/ee/workers.ts +3 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -10212,17 +10212,31 @@ function detectQueryLanguage(query, minLength = 10) {
|
|
|
10212
10212
|
}
|
|
10213
10213
|
function stemWord(word, languageCode) {
|
|
10214
10214
|
const stemmer = STEMMER_MAP[languageCode] || import_natural.default.PorterStemmer;
|
|
10215
|
-
const
|
|
10216
|
-
if (!
|
|
10215
|
+
const core = word.replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "").toLowerCase();
|
|
10216
|
+
if (!core) {
|
|
10217
10217
|
return word;
|
|
10218
10218
|
}
|
|
10219
|
+
if (!/^\p{L}+$/u.test(core)) {
|
|
10220
|
+
return core;
|
|
10221
|
+
}
|
|
10219
10222
|
try {
|
|
10220
|
-
return stemmer.stem(
|
|
10223
|
+
return stemmer.stem(core);
|
|
10221
10224
|
} catch (error) {
|
|
10222
10225
|
console.warn(`[EXULU] Error stemming word "${word}":`, error);
|
|
10223
|
-
return
|
|
10226
|
+
return core;
|
|
10224
10227
|
}
|
|
10225
10228
|
}
|
|
10229
|
+
function buildFullTextOrQuery(original, processed) {
|
|
10230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10231
|
+
const tokens = [];
|
|
10232
|
+
for (const raw of `${original} ${processed}`.split(/\s+/)) {
|
|
10233
|
+
const token = raw.replace(/["'`]/g, "").replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "").toLowerCase();
|
|
10234
|
+
if (!token || token === "or" || seen.has(token)) continue;
|
|
10235
|
+
seen.add(token);
|
|
10236
|
+
tokens.push(token);
|
|
10237
|
+
}
|
|
10238
|
+
return tokens.join(" or ");
|
|
10239
|
+
}
|
|
10226
10240
|
function preprocessQuery(query, options = {}) {
|
|
10227
10241
|
const {
|
|
10228
10242
|
enableStemming = true,
|
|
@@ -10257,6 +10271,18 @@ function preprocessQuery(query, options = {}) {
|
|
|
10257
10271
|
stemmed: true
|
|
10258
10272
|
};
|
|
10259
10273
|
}
|
|
10274
|
+
function resolveSearchQueryTexts(query) {
|
|
10275
|
+
const { processed } = preprocessQuery(query, {
|
|
10276
|
+
enableStemming: true,
|
|
10277
|
+
detectLanguage: true
|
|
10278
|
+
});
|
|
10279
|
+
const ftsText = processed || query;
|
|
10280
|
+
return {
|
|
10281
|
+
embedText: query,
|
|
10282
|
+
ftsText,
|
|
10283
|
+
hybridOrQuery: buildFullTextOrQuery(query, ftsText)
|
|
10284
|
+
};
|
|
10285
|
+
}
|
|
10260
10286
|
|
|
10261
10287
|
// src/graphql/resolvers/apply-sorting.ts
|
|
10262
10288
|
init_cjs_shims();
|
|
@@ -12735,17 +12761,15 @@ var vectorSearch = async ({
|
|
|
12735
12761
|
const _tBody = Date.now();
|
|
12736
12762
|
let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
|
|
12737
12763
|
let _embedSource = "none";
|
|
12764
|
+
let hybridOrQuery = "";
|
|
12738
12765
|
if (query) {
|
|
12739
12766
|
const _tp = Date.now();
|
|
12740
|
-
const
|
|
12741
|
-
enableStemming: true,
|
|
12742
|
-
detectLanguage: true
|
|
12743
|
-
});
|
|
12767
|
+
const texts = resolveSearchQueryTexts(query);
|
|
12744
12768
|
_preMs = Date.now() - _tp;
|
|
12745
|
-
console.log("[EXULU]
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12769
|
+
console.log("[EXULU] Search query texts:", texts);
|
|
12770
|
+
const embedText = texts.embedText;
|
|
12771
|
+
hybridOrQuery = texts.hybridOrQuery;
|
|
12772
|
+
query = texts.ftsText;
|
|
12749
12773
|
if (queryEmbedding && queryEmbedding.length) {
|
|
12750
12774
|
vector = queryEmbedding;
|
|
12751
12775
|
_embedSource = "reused";
|
|
@@ -12771,7 +12795,7 @@ var vectorSearch = async ({
|
|
|
12771
12795
|
});
|
|
12772
12796
|
_resolveMs = Date.now() - _tr;
|
|
12773
12797
|
const _te = Date.now();
|
|
12774
|
-
const [queryVector] = await resolved.embed([
|
|
12798
|
+
const [queryVector] = await resolved.embed([embedText], { inputType: "query" });
|
|
12775
12799
|
_embedMs = Date.now() - _te;
|
|
12776
12800
|
if (!queryVector?.length) {
|
|
12777
12801
|
throw new Error("No vector generated for query.");
|
|
@@ -12843,10 +12867,10 @@ var vectorSearch = async ({
|
|
|
12843
12867
|
const fullTextWeight = 2;
|
|
12844
12868
|
const semanticWeight = 1;
|
|
12845
12869
|
const rrfK = 50;
|
|
12846
|
-
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts,
|
|
12847
|
-
const ftRankParams = languages.map(() =>
|
|
12848
|
-
const ftMatchExpression = languages.map((lang) => `chunks.fts @@
|
|
12849
|
-
const ftMatchParams = languages.map(() =>
|
|
12870
|
+
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ");
|
|
12871
|
+
const ftRankParams = languages.map(() => hybridOrQuery);
|
|
12872
|
+
const ftMatchExpression = languages.map((lang) => `chunks.fts @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
|
|
12873
|
+
const ftMatchParams = languages.map(() => hybridOrQuery);
|
|
12850
12874
|
let fullTextQuery = db2(chunksTable + " as chunks").select([
|
|
12851
12875
|
"chunks.id",
|
|
12852
12876
|
"chunks.source",
|
|
@@ -12886,8 +12910,8 @@ var vectorSearch = async ({
|
|
|
12886
12910
|
db2.raw('items."updatedAt" as item_updated_at'),
|
|
12887
12911
|
db2.raw('items."createdAt" as item_created_at'),
|
|
12888
12912
|
db2.raw(
|
|
12889
|
-
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
12890
|
-
languages.map(() =>
|
|
12913
|
+
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
|
|
12914
|
+
languages.map(() => hybridOrQuery)
|
|
12891
12915
|
),
|
|
12892
12916
|
db2.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
|
|
12893
12917
|
db2.raw(
|
|
@@ -12912,8 +12936,8 @@ var vectorSearch = async ({
|
|
|
12912
12936
|
`,
|
|
12913
12937
|
[rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
|
|
12914
12938
|
).whereRaw(
|
|
12915
|
-
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
12916
|
-
[...languages.map(() =>
|
|
12939
|
+
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
|
|
12940
|
+
[...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
|
|
12917
12941
|
).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
|
|
12918
12942
|
cutoffs?.cosineDistance || 0
|
|
12919
12943
|
]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
|
|
@@ -16658,6 +16682,37 @@ var bullmq = {
|
|
|
16658
16682
|
}
|
|
16659
16683
|
};
|
|
16660
16684
|
|
|
16685
|
+
// src/utils/serialize-error.ts
|
|
16686
|
+
init_cjs_shims();
|
|
16687
|
+
var MAX_CAUSE_DEPTH = 3;
|
|
16688
|
+
function serializeError(err, depth = 0) {
|
|
16689
|
+
if (err instanceof Error) {
|
|
16690
|
+
const out = { name: err.name, message: err.message };
|
|
16691
|
+
if (err.stack) out.stack = err.stack;
|
|
16692
|
+
for (const [key, value] of Object.entries(err)) {
|
|
16693
|
+
if (key !== "cause") out[key] = value;
|
|
16694
|
+
}
|
|
16695
|
+
const cause = err.cause;
|
|
16696
|
+
if (cause !== void 0 && depth < MAX_CAUSE_DEPTH) {
|
|
16697
|
+
out.cause = serializeError(cause, depth + 1);
|
|
16698
|
+
}
|
|
16699
|
+
return out;
|
|
16700
|
+
}
|
|
16701
|
+
if (typeof err === "string") {
|
|
16702
|
+
return { message: err };
|
|
16703
|
+
}
|
|
16704
|
+
if (err !== null && typeof err === "object") {
|
|
16705
|
+
let message;
|
|
16706
|
+
try {
|
|
16707
|
+
message = JSON.stringify(err);
|
|
16708
|
+
} catch {
|
|
16709
|
+
message = "[unserializable error object]";
|
|
16710
|
+
}
|
|
16711
|
+
return { ...err, message };
|
|
16712
|
+
}
|
|
16713
|
+
return { message: String(err) };
|
|
16714
|
+
}
|
|
16715
|
+
|
|
16661
16716
|
// src/utils/enabled-tools.ts
|
|
16662
16717
|
init_cjs_shims();
|
|
16663
16718
|
init_pipeline();
|
|
@@ -16676,6 +16731,114 @@ init_statistics();
|
|
|
16676
16731
|
|
|
16677
16732
|
// src/exulu/generate-stream.ts
|
|
16678
16733
|
init_cjs_shims();
|
|
16734
|
+
|
|
16735
|
+
// src/exulu/file-part.ts
|
|
16736
|
+
init_cjs_shims();
|
|
16737
|
+
var IMAGE_EXTENSION_MIME = {
|
|
16738
|
+
".png": "image/png",
|
|
16739
|
+
".jpeg": "image/jpeg",
|
|
16740
|
+
".jpg": "image/jpeg",
|
|
16741
|
+
".gif": "image/gif",
|
|
16742
|
+
".webp": "image/webp",
|
|
16743
|
+
// Windows bitmaps arrive as email logos / signature images and camera exports.
|
|
16744
|
+
// Left out, they were routed to OfficeParser and replaced by an error text.
|
|
16745
|
+
".bmp": "image/bmp",
|
|
16746
|
+
".dib": "image/bmp"
|
|
16747
|
+
};
|
|
16748
|
+
var MEDIA_TYPE_ALIASES = {
|
|
16749
|
+
"image/jpg": "image/jpeg",
|
|
16750
|
+
"image/x-ms-bmp": "image/bmp",
|
|
16751
|
+
"image/x-bmp": "image/bmp",
|
|
16752
|
+
"image/x-windows-bmp": "image/bmp"
|
|
16753
|
+
};
|
|
16754
|
+
function fileNameFromUrl(url) {
|
|
16755
|
+
if (!url) return void 0;
|
|
16756
|
+
let path4;
|
|
16757
|
+
try {
|
|
16758
|
+
path4 = new URL(url).pathname;
|
|
16759
|
+
} catch {
|
|
16760
|
+
path4 = url.split("?")[0] ?? "";
|
|
16761
|
+
}
|
|
16762
|
+
const last = path4.split("/").pop() ?? "";
|
|
16763
|
+
try {
|
|
16764
|
+
return decodeURIComponent(last) || void 0;
|
|
16765
|
+
} catch {
|
|
16766
|
+
return last || void 0;
|
|
16767
|
+
}
|
|
16768
|
+
}
|
|
16769
|
+
function classifyFilePart(part) {
|
|
16770
|
+
const filename = part.filename ?? fileNameFromUrl(part.url);
|
|
16771
|
+
const lowerName = (filename ?? "").toLowerCase();
|
|
16772
|
+
const extension = Object.keys(IMAGE_EXTENSION_MIME).find((ext) => lowerName.endsWith(ext));
|
|
16773
|
+
const declared = typeof part.mediaType === "string" ? part.mediaType.trim().toLowerCase() : "";
|
|
16774
|
+
const declaredIsImage = declared.startsWith("image/");
|
|
16775
|
+
if (declaredIsImage || extension) {
|
|
16776
|
+
const mediaType = declaredIsImage ? MEDIA_TYPE_ALIASES[declared] ?? declared : IMAGE_EXTENSION_MIME[extension];
|
|
16777
|
+
return { kind: "image", mediaType, filename };
|
|
16778
|
+
}
|
|
16779
|
+
return { kind: "document", filename: filename ?? "attachment" };
|
|
16780
|
+
}
|
|
16781
|
+
|
|
16782
|
+
// src/exulu/stored-messages.ts
|
|
16783
|
+
init_cjs_shims();
|
|
16784
|
+
function dropEmptyMessages(messages) {
|
|
16785
|
+
const kept = messages.filter((m) => Array.isArray(m.parts) && m.parts.length > 0);
|
|
16786
|
+
return kept.length === messages.length ? messages : kept;
|
|
16787
|
+
}
|
|
16788
|
+
|
|
16789
|
+
// src/exulu/stored-file-url.ts
|
|
16790
|
+
init_cjs_shims();
|
|
16791
|
+
var stripTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
16792
|
+
function decodeKey(rawPath) {
|
|
16793
|
+
const key = rawPath.replace(/^\/+/, "");
|
|
16794
|
+
try {
|
|
16795
|
+
return decodeURIComponent(key);
|
|
16796
|
+
} catch {
|
|
16797
|
+
return key;
|
|
16798
|
+
}
|
|
16799
|
+
}
|
|
16800
|
+
function parseStoredFileUrl(url, store) {
|
|
16801
|
+
if (!url || !store.bucket) return void 0;
|
|
16802
|
+
let parsed;
|
|
16803
|
+
try {
|
|
16804
|
+
parsed = new URL(url);
|
|
16805
|
+
} catch {
|
|
16806
|
+
return void 0;
|
|
16807
|
+
}
|
|
16808
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
16809
|
+
if (store.endpoint) {
|
|
16810
|
+
let endpoint;
|
|
16811
|
+
try {
|
|
16812
|
+
endpoint = new URL(store.endpoint);
|
|
16813
|
+
} catch {
|
|
16814
|
+
return void 0;
|
|
16815
|
+
}
|
|
16816
|
+
if (parsed.host !== endpoint.host) return void 0;
|
|
16817
|
+
const base = stripTrailingSlash(endpoint.pathname);
|
|
16818
|
+
const prefix = `${base}/${store.bucket}/`;
|
|
16819
|
+
if (!parsed.pathname.startsWith(prefix)) return void 0;
|
|
16820
|
+
const key2 = decodeKey(parsed.pathname.slice(prefix.length));
|
|
16821
|
+
return key2 ? { bucket: store.bucket, key: key2 } : void 0;
|
|
16822
|
+
}
|
|
16823
|
+
if (!parsed.host.startsWith(`${store.bucket}.s3`) || !parsed.host.endsWith(".amazonaws.com")) {
|
|
16824
|
+
return void 0;
|
|
16825
|
+
}
|
|
16826
|
+
const key = decodeKey(parsed.pathname);
|
|
16827
|
+
return key ? { bucket: store.bucket, key } : void 0;
|
|
16828
|
+
}
|
|
16829
|
+
async function resolveFreshFileUrl(url, opts) {
|
|
16830
|
+
const location = parseStoredFileUrl(url, opts);
|
|
16831
|
+
if (!location) return url;
|
|
16832
|
+
try {
|
|
16833
|
+
return await opts.sign(location.bucket, location.key);
|
|
16834
|
+
} catch (err) {
|
|
16835
|
+
console.warn(`[EXULU] could not re-sign stored file URL for key "${location.key}":`, err);
|
|
16836
|
+
return url;
|
|
16837
|
+
}
|
|
16838
|
+
}
|
|
16839
|
+
|
|
16840
|
+
// src/exulu/generate-stream.ts
|
|
16841
|
+
init_uppy();
|
|
16679
16842
|
var import_ai6 = require("ai");
|
|
16680
16843
|
|
|
16681
16844
|
// src/exulu/context-guard.ts
|
|
@@ -16993,22 +17156,24 @@ var processFilePartsInMessages = async (messages, offloadCtx) => {
|
|
|
16993
17156
|
return part;
|
|
16994
17157
|
}
|
|
16995
17158
|
console.log(`[EXULU] Processing part`, part);
|
|
16996
|
-
const
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
);
|
|
17004
|
-
if (
|
|
17005
|
-
console.log(`[EXULU] Converting file part to image part: ${filename} `);
|
|
17159
|
+
const uploads = offloadCtx.exuluConfig?.fileUploads;
|
|
17160
|
+
const url = uploads?.s3Bucket ? await resolveFreshFileUrl(part.url, {
|
|
17161
|
+
endpoint: uploads.s3endpoint,
|
|
17162
|
+
bucket: uploads.s3Bucket,
|
|
17163
|
+
sign: (bucket, key) => getPresignedUrl(bucket, key, offloadCtx.exuluConfig)
|
|
17164
|
+
}) : part.url;
|
|
17165
|
+
const classified = classifyFilePart(part);
|
|
17166
|
+
console.log(`[EXULU] File part classified as ${classified.kind}: ${classified.filename}`);
|
|
17167
|
+
if (classified.kind === "image") {
|
|
17006
17168
|
return {
|
|
17007
17169
|
type: "file",
|
|
17008
|
-
mediaType:
|
|
17009
|
-
url
|
|
17170
|
+
mediaType: classified.mediaType,
|
|
17171
|
+
url,
|
|
17172
|
+
// Keep the name so later turns/steps still know the file.
|
|
17173
|
+
...classified.filename ? { filename: classified.filename } : {}
|
|
17010
17174
|
};
|
|
17011
17175
|
}
|
|
17176
|
+
const filename = classified.filename;
|
|
17012
17177
|
console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
|
|
17013
17178
|
try {
|
|
17014
17179
|
const response = await fetch(url);
|
|
@@ -17058,7 +17223,7 @@ var saveChat = async ({
|
|
|
17058
17223
|
model
|
|
17059
17224
|
}) => {
|
|
17060
17225
|
const { db: db2 } = await postgresClient();
|
|
17061
|
-
for (const message of messages) {
|
|
17226
|
+
for (const message of dropEmptyMessages(messages)) {
|
|
17062
17227
|
const mutation = db2.from("agent_messages").insert({
|
|
17063
17228
|
session,
|
|
17064
17229
|
user,
|
|
@@ -17145,7 +17310,7 @@ var generateSync = async ({
|
|
|
17145
17310
|
);
|
|
17146
17311
|
messages = await (0, import_ai6.validateUIMessages)({
|
|
17147
17312
|
// append the new message to the previous messages:
|
|
17148
|
-
messages: [...previousMessagesContent, ...messages]
|
|
17313
|
+
messages: dropEmptyMessages([...previousMessagesContent, ...messages])
|
|
17149
17314
|
});
|
|
17150
17315
|
const contextBudget = deriveContextBudget(contextWindow);
|
|
17151
17316
|
const occupancy = contextOccupancy(messages);
|
|
@@ -17487,7 +17652,7 @@ var generateStream = async ({
|
|
|
17487
17652
|
const model = languageModel;
|
|
17488
17653
|
messages = await (0, import_ai6.validateUIMessages)({
|
|
17489
17654
|
// append the new message to the previous messages:
|
|
17490
|
-
messages: [...previousMessagesContent, message]
|
|
17655
|
+
messages: dropEmptyMessages([...previousMessagesContent, message])
|
|
17491
17656
|
});
|
|
17492
17657
|
const query = message.parts?.[0]?.type === "text" ? message.parts[0].text : void 0;
|
|
17493
17658
|
if (session && query) {
|
|
@@ -19291,7 +19456,8 @@ var createWorkers = async (queues2, config, contexts, evals, tools, tracer) => {
|
|
|
19291
19456
|
JOB_STATUS_ENUM.completed
|
|
19292
19457
|
]).update({
|
|
19293
19458
|
state: JOB_STATUS_ENUM.failed,
|
|
19294
|
-
|
|
19459
|
+
// A raw Error serialises to "{}" in jsonb — keep message/stack/cause.
|
|
19460
|
+
error: serializeError(error)
|
|
19295
19461
|
});
|
|
19296
19462
|
void maybePruneJobResults(db2);
|
|
19297
19463
|
return;
|
|
@@ -23821,7 +23987,7 @@ var import_fs3 = __toESM(require("fs"), 1);
|
|
|
23821
23987
|
var import_node_crypto15 = require("crypto");
|
|
23822
23988
|
var import_api2 = require("@opentelemetry/api");
|
|
23823
23989
|
var import_jszip3 = __toESM(require("jszip"), 1);
|
|
23824
|
-
var
|
|
23990
|
+
var import_ai12 = require("ai");
|
|
23825
23991
|
var import_cookie_parser = __toESM(require("cookie-parser"), 1);
|
|
23826
23992
|
init_statistics2();
|
|
23827
23993
|
|
|
@@ -23980,7 +24146,9 @@ var compactSession = async ({
|
|
|
23980
24146
|
}) => {
|
|
23981
24147
|
const budget = deriveContextBudget(contextWindow);
|
|
23982
24148
|
const rows = await getAgentMessages({ session: sessionID, user: user.id });
|
|
23983
|
-
const all = await (0, import_ai10.validateUIMessages)({
|
|
24149
|
+
const all = await (0, import_ai10.validateUIMessages)({
|
|
24150
|
+
messages: dropEmptyMessages(rows.map((r) => JSON.parse(r.content)))
|
|
24151
|
+
});
|
|
23984
24152
|
const history = sliceHistoryAtCheckpoint(all);
|
|
23985
24153
|
const { head, tail } = splitTail(history, budget.compactionTailTokens);
|
|
23986
24154
|
if (head.length === 0) {
|
|
@@ -24034,6 +24202,25 @@ ${summary}` }],
|
|
|
24034
24202
|
return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
|
|
24035
24203
|
};
|
|
24036
24204
|
|
|
24205
|
+
// src/exulu/request-error.ts
|
|
24206
|
+
init_cjs_shims();
|
|
24207
|
+
var import_ai11 = require("ai");
|
|
24208
|
+
init_context_budget();
|
|
24209
|
+
function describeRequestError(err) {
|
|
24210
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
24211
|
+
return { status: 413, body: err.message };
|
|
24212
|
+
}
|
|
24213
|
+
if (import_ai11.TypeValidationError.isInstance(err)) {
|
|
24214
|
+
const cause = err.cause;
|
|
24215
|
+
const detail = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
|
|
24216
|
+
return {
|
|
24217
|
+
status: 422,
|
|
24218
|
+
body: `The stored conversation could not be loaded: ${detail.slice(0, 400) || "type validation failed"}`
|
|
24219
|
+
};
|
|
24220
|
+
}
|
|
24221
|
+
return { status: 500, body: err instanceof Error ? err.message : String(err) };
|
|
24222
|
+
}
|
|
24223
|
+
|
|
24037
24224
|
// src/exulu/transcribe.ts
|
|
24038
24225
|
init_cjs_shims();
|
|
24039
24226
|
init_env();
|
|
@@ -25639,11 +25826,13 @@ ${customInstructions}` : agent.instructions;
|
|
|
25639
25826
|
});
|
|
25640
25827
|
} catch (err) {
|
|
25641
25828
|
if (headers.session) clearStreamActive(headers.session);
|
|
25642
|
-
|
|
25643
|
-
|
|
25644
|
-
|
|
25645
|
-
|
|
25646
|
-
|
|
25829
|
+
console.error(
|
|
25830
|
+
"[EXULU] chat request failed before streaming.",
|
|
25831
|
+
err instanceof Error ? err.message.slice(0, 500) : err
|
|
25832
|
+
);
|
|
25833
|
+
const { status, body } = describeRequestError(err);
|
|
25834
|
+
res.status(status).send(body);
|
|
25835
|
+
return;
|
|
25647
25836
|
}
|
|
25648
25837
|
result.stream.consumeStream();
|
|
25649
25838
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
@@ -25678,7 +25867,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
25678
25867
|
else message2 = JSON.stringify(error);
|
|
25679
25868
|
return mapStreamErrorMessage(message2);
|
|
25680
25869
|
},
|
|
25681
|
-
generateMessageId: (0,
|
|
25870
|
+
generateMessageId: (0, import_ai12.createIdGenerator)({
|
|
25682
25871
|
prefix: "msg_",
|
|
25683
25872
|
size: 16
|
|
25684
25873
|
}),
|
|
@@ -25782,11 +25971,13 @@ ${customInstructions}` : agent.instructions;
|
|
|
25782
25971
|
}
|
|
25783
25972
|
});
|
|
25784
25973
|
} catch (err) {
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25974
|
+
console.error(
|
|
25975
|
+
"[EXULU] run request failed.",
|
|
25976
|
+
err instanceof Error ? err.message.slice(0, 500) : err
|
|
25977
|
+
);
|
|
25978
|
+
const { status, body } = describeRequestError(err);
|
|
25979
|
+
res.status(status).send(body);
|
|
25980
|
+
return;
|
|
25790
25981
|
}
|
|
25791
25982
|
res.status(200).json(response);
|
|
25792
25983
|
return;
|
|
@@ -28110,6 +28301,7 @@ ${style.markdown}` : params.prompt;
|
|
|
28110
28301
|
".jpeg": "image/jpeg",
|
|
28111
28302
|
".gif": "image/gif",
|
|
28112
28303
|
".webp": "image/webp",
|
|
28304
|
+
".bmp": "image/bmp",
|
|
28113
28305
|
".svg": "image/svg+xml",
|
|
28114
28306
|
".pdf": "application/pdf",
|
|
28115
28307
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -29136,7 +29328,7 @@ init_cjs_shims();
|
|
|
29136
29328
|
|
|
29137
29329
|
// src/exulu/evals.ts
|
|
29138
29330
|
init_cjs_shims();
|
|
29139
|
-
var
|
|
29331
|
+
var import_ai13 = require("ai");
|
|
29140
29332
|
var ExuluEval = class {
|
|
29141
29333
|
id;
|
|
29142
29334
|
name;
|
|
@@ -29176,7 +29368,7 @@ var ExuluEval = class {
|
|
|
29176
29368
|
init_resolve_model();
|
|
29177
29369
|
init_singleton();
|
|
29178
29370
|
var import_zod20 = require("zod");
|
|
29179
|
-
var
|
|
29371
|
+
var import_ai14 = require("ai");
|
|
29180
29372
|
var llmAsJudgeEval = () => {
|
|
29181
29373
|
if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
|
|
29182
29374
|
return new ExuluEval({
|
|
@@ -29219,13 +29411,13 @@ var llmAsJudgeEval = () => {
|
|
|
29219
29411
|
rbacBypass: true
|
|
29220
29412
|
});
|
|
29221
29413
|
console.log("[EXULU] prompt", prompt);
|
|
29222
|
-
const { output } = await (0,
|
|
29414
|
+
const { output } = await (0, import_ai14.generateText)({
|
|
29223
29415
|
temperature: 0,
|
|
29224
29416
|
model: resolved.languageModel,
|
|
29225
29417
|
system: "",
|
|
29226
29418
|
prompt,
|
|
29227
29419
|
maxRetries: 2,
|
|
29228
|
-
output:
|
|
29420
|
+
output: import_ai14.Output.object({
|
|
29229
29421
|
schema: import_zod20.z.object({
|
|
29230
29422
|
score: import_zod20.z.number().min(0).max(100).describe("The score between 0 and 100.")
|
|
29231
29423
|
})
|
|
@@ -32839,7 +33031,7 @@ var MarkdownChunker = class {
|
|
|
32839
33031
|
init_cjs_shims();
|
|
32840
33032
|
var fs4 = __toESM(require("fs"), 1);
|
|
32841
33033
|
var path3 = __toESM(require("path"), 1);
|
|
32842
|
-
var
|
|
33034
|
+
var import_ai15 = require("ai");
|
|
32843
33035
|
var import_zod27 = require("zod");
|
|
32844
33036
|
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
32845
33037
|
var import_crypto3 = require("crypto");
|
|
@@ -33295,9 +33487,9 @@ If the page contains a flow-chart, schematic, technical drawing or control board
|
|
|
33295
33487
|
|
|
33296
33488
|
### 7. Only populate \`corrected_text\` when \`needs_correction\` is true. If the OCR output is accurate, return \`needs_correction: false\` and \`corrected_content: null\`.
|
|
33297
33489
|
`;
|
|
33298
|
-
const result = await (0,
|
|
33490
|
+
const result = await (0, import_ai15.generateText)({
|
|
33299
33491
|
model,
|
|
33300
|
-
output:
|
|
33492
|
+
output: import_ai15.Output.object({
|
|
33301
33493
|
schema: import_zod27.z.object({
|
|
33302
33494
|
needs_correction: import_zod27.z.boolean(),
|
|
33303
33495
|
corrected_text: import_zod27.z.string().nullable(),
|
package/dist/index.js
CHANGED
|
@@ -1347,17 +1347,31 @@ function detectQueryLanguage(query, minLength = 10) {
|
|
|
1347
1347
|
}
|
|
1348
1348
|
function stemWord(word, languageCode) {
|
|
1349
1349
|
const stemmer = STEMMER_MAP[languageCode] || natural.PorterStemmer;
|
|
1350
|
-
const
|
|
1351
|
-
if (!
|
|
1350
|
+
const core = word.replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "").toLowerCase();
|
|
1351
|
+
if (!core) {
|
|
1352
1352
|
return word;
|
|
1353
1353
|
}
|
|
1354
|
+
if (!/^\p{L}+$/u.test(core)) {
|
|
1355
|
+
return core;
|
|
1356
|
+
}
|
|
1354
1357
|
try {
|
|
1355
|
-
return stemmer.stem(
|
|
1358
|
+
return stemmer.stem(core);
|
|
1356
1359
|
} catch (error) {
|
|
1357
1360
|
console.warn(`[EXULU] Error stemming word "${word}":`, error);
|
|
1358
|
-
return
|
|
1361
|
+
return core;
|
|
1359
1362
|
}
|
|
1360
1363
|
}
|
|
1364
|
+
function buildFullTextOrQuery(original, processed) {
|
|
1365
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1366
|
+
const tokens = [];
|
|
1367
|
+
for (const raw of `${original} ${processed}`.split(/\s+/)) {
|
|
1368
|
+
const token = raw.replace(/["'`]/g, "").replace(/^[^\p{L}\p{N}]+/u, "").replace(/[^\p{L}\p{N}]+$/u, "").toLowerCase();
|
|
1369
|
+
if (!token || token === "or" || seen.has(token)) continue;
|
|
1370
|
+
seen.add(token);
|
|
1371
|
+
tokens.push(token);
|
|
1372
|
+
}
|
|
1373
|
+
return tokens.join(" or ");
|
|
1374
|
+
}
|
|
1361
1375
|
function preprocessQuery(query, options = {}) {
|
|
1362
1376
|
const {
|
|
1363
1377
|
enableStemming = true,
|
|
@@ -1392,6 +1406,18 @@ function preprocessQuery(query, options = {}) {
|
|
|
1392
1406
|
stemmed: true
|
|
1393
1407
|
};
|
|
1394
1408
|
}
|
|
1409
|
+
function resolveSearchQueryTexts(query) {
|
|
1410
|
+
const { processed } = preprocessQuery(query, {
|
|
1411
|
+
enableStemming: true,
|
|
1412
|
+
detectLanguage: true
|
|
1413
|
+
});
|
|
1414
|
+
const ftsText = processed || query;
|
|
1415
|
+
return {
|
|
1416
|
+
embedText: query,
|
|
1417
|
+
ftsText,
|
|
1418
|
+
hybridOrQuery: buildFullTextOrQuery(query, ftsText)
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1395
1421
|
|
|
1396
1422
|
// src/graphql/resolvers/field-allow-list.ts
|
|
1397
1423
|
var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
|
|
@@ -3828,17 +3854,15 @@ var vectorSearch = async ({
|
|
|
3828
3854
|
const _tBody = Date.now();
|
|
3829
3855
|
let _preMs = 0, _statMs = 0, _resolveMs = 0, _embedMs = 0;
|
|
3830
3856
|
let _embedSource = "none";
|
|
3857
|
+
let hybridOrQuery = "";
|
|
3831
3858
|
if (query) {
|
|
3832
3859
|
const _tp = Date.now();
|
|
3833
|
-
const
|
|
3834
|
-
enableStemming: true,
|
|
3835
|
-
detectLanguage: true
|
|
3836
|
-
});
|
|
3860
|
+
const texts = resolveSearchQueryTexts(query);
|
|
3837
3861
|
_preMs = Date.now() - _tp;
|
|
3838
|
-
console.log("[EXULU]
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3862
|
+
console.log("[EXULU] Search query texts:", texts);
|
|
3863
|
+
const embedText = texts.embedText;
|
|
3864
|
+
hybridOrQuery = texts.hybridOrQuery;
|
|
3865
|
+
query = texts.ftsText;
|
|
3842
3866
|
if (queryEmbedding && queryEmbedding.length) {
|
|
3843
3867
|
vector = queryEmbedding;
|
|
3844
3868
|
_embedSource = "reused";
|
|
@@ -3864,7 +3888,7 @@ var vectorSearch = async ({
|
|
|
3864
3888
|
});
|
|
3865
3889
|
_resolveMs = Date.now() - _tr;
|
|
3866
3890
|
const _te = Date.now();
|
|
3867
|
-
const [queryVector] = await resolved.embed([
|
|
3891
|
+
const [queryVector] = await resolved.embed([embedText], { inputType: "query" });
|
|
3868
3892
|
_embedMs = Date.now() - _te;
|
|
3869
3893
|
if (!queryVector?.length) {
|
|
3870
3894
|
throw new Error("No vector generated for query.");
|
|
@@ -3936,10 +3960,10 @@ var vectorSearch = async ({
|
|
|
3936
3960
|
const fullTextWeight = 2;
|
|
3937
3961
|
const semanticWeight = 1;
|
|
3938
3962
|
const rrfK = 50;
|
|
3939
|
-
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts,
|
|
3940
|
-
const ftRankParams = languages.map(() =>
|
|
3941
|
-
const ftMatchExpression = languages.map((lang) => `chunks.fts @@
|
|
3942
|
-
const ftMatchParams = languages.map(() =>
|
|
3963
|
+
const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ");
|
|
3964
|
+
const ftRankParams = languages.map(() => hybridOrQuery);
|
|
3965
|
+
const ftMatchExpression = languages.map((lang) => `chunks.fts @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
|
|
3966
|
+
const ftMatchParams = languages.map(() => hybridOrQuery);
|
|
3943
3967
|
let fullTextQuery = db(chunksTable + " as chunks").select([
|
|
3944
3968
|
"chunks.id",
|
|
3945
3969
|
"chunks.source",
|
|
@@ -3979,8 +4003,8 @@ var vectorSearch = async ({
|
|
|
3979
4003
|
db.raw('items."updatedAt" as item_updated_at'),
|
|
3980
4004
|
db.raw('items."createdAt" as item_created_at'),
|
|
3981
4005
|
db.raw(
|
|
3982
|
-
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
3983
|
-
languages.map(() =>
|
|
4006
|
+
`GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
|
|
4007
|
+
languages.map(() => hybridOrQuery)
|
|
3984
4008
|
),
|
|
3985
4009
|
db.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
|
|
3986
4010
|
db.raw(
|
|
@@ -4005,8 +4029,8 @@ var vectorSearch = async ({
|
|
|
4005
4029
|
`,
|
|
4006
4030
|
[rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
|
|
4007
4031
|
).whereRaw(
|
|
4008
|
-
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts,
|
|
4009
|
-
[...languages.map(() =>
|
|
4032
|
+
`(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
|
|
4033
|
+
[...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
|
|
4010
4034
|
).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
|
|
4011
4035
|
cutoffs?.cosineDistance || 0
|
|
4012
4036
|
]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
|
|
@@ -7687,9 +7711,141 @@ var bullmq = {
|
|
|
7687
7711
|
}
|
|
7688
7712
|
};
|
|
7689
7713
|
|
|
7714
|
+
// src/utils/serialize-error.ts
|
|
7715
|
+
var MAX_CAUSE_DEPTH = 3;
|
|
7716
|
+
function serializeError(err, depth = 0) {
|
|
7717
|
+
if (err instanceof Error) {
|
|
7718
|
+
const out = { name: err.name, message: err.message };
|
|
7719
|
+
if (err.stack) out.stack = err.stack;
|
|
7720
|
+
for (const [key, value] of Object.entries(err)) {
|
|
7721
|
+
if (key !== "cause") out[key] = value;
|
|
7722
|
+
}
|
|
7723
|
+
const cause = err.cause;
|
|
7724
|
+
if (cause !== void 0 && depth < MAX_CAUSE_DEPTH) {
|
|
7725
|
+
out.cause = serializeError(cause, depth + 1);
|
|
7726
|
+
}
|
|
7727
|
+
return out;
|
|
7728
|
+
}
|
|
7729
|
+
if (typeof err === "string") {
|
|
7730
|
+
return { message: err };
|
|
7731
|
+
}
|
|
7732
|
+
if (err !== null && typeof err === "object") {
|
|
7733
|
+
let message;
|
|
7734
|
+
try {
|
|
7735
|
+
message = JSON.stringify(err);
|
|
7736
|
+
} catch {
|
|
7737
|
+
message = "[unserializable error object]";
|
|
7738
|
+
}
|
|
7739
|
+
return { ...err, message };
|
|
7740
|
+
}
|
|
7741
|
+
return { message: String(err) };
|
|
7742
|
+
}
|
|
7743
|
+
|
|
7690
7744
|
// src/exulu/agent-as-tool.ts
|
|
7691
7745
|
import { z as z2 } from "zod";
|
|
7692
7746
|
|
|
7747
|
+
// src/exulu/file-part.ts
|
|
7748
|
+
var IMAGE_EXTENSION_MIME = {
|
|
7749
|
+
".png": "image/png",
|
|
7750
|
+
".jpeg": "image/jpeg",
|
|
7751
|
+
".jpg": "image/jpeg",
|
|
7752
|
+
".gif": "image/gif",
|
|
7753
|
+
".webp": "image/webp",
|
|
7754
|
+
// Windows bitmaps arrive as email logos / signature images and camera exports.
|
|
7755
|
+
// Left out, they were routed to OfficeParser and replaced by an error text.
|
|
7756
|
+
".bmp": "image/bmp",
|
|
7757
|
+
".dib": "image/bmp"
|
|
7758
|
+
};
|
|
7759
|
+
var MEDIA_TYPE_ALIASES = {
|
|
7760
|
+
"image/jpg": "image/jpeg",
|
|
7761
|
+
"image/x-ms-bmp": "image/bmp",
|
|
7762
|
+
"image/x-bmp": "image/bmp",
|
|
7763
|
+
"image/x-windows-bmp": "image/bmp"
|
|
7764
|
+
};
|
|
7765
|
+
function fileNameFromUrl(url) {
|
|
7766
|
+
if (!url) return void 0;
|
|
7767
|
+
let path2;
|
|
7768
|
+
try {
|
|
7769
|
+
path2 = new URL(url).pathname;
|
|
7770
|
+
} catch {
|
|
7771
|
+
path2 = url.split("?")[0] ?? "";
|
|
7772
|
+
}
|
|
7773
|
+
const last = path2.split("/").pop() ?? "";
|
|
7774
|
+
try {
|
|
7775
|
+
return decodeURIComponent(last) || void 0;
|
|
7776
|
+
} catch {
|
|
7777
|
+
return last || void 0;
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
function classifyFilePart(part) {
|
|
7781
|
+
const filename = part.filename ?? fileNameFromUrl(part.url);
|
|
7782
|
+
const lowerName = (filename ?? "").toLowerCase();
|
|
7783
|
+
const extension = Object.keys(IMAGE_EXTENSION_MIME).find((ext) => lowerName.endsWith(ext));
|
|
7784
|
+
const declared = typeof part.mediaType === "string" ? part.mediaType.trim().toLowerCase() : "";
|
|
7785
|
+
const declaredIsImage = declared.startsWith("image/");
|
|
7786
|
+
if (declaredIsImage || extension) {
|
|
7787
|
+
const mediaType = declaredIsImage ? MEDIA_TYPE_ALIASES[declared] ?? declared : IMAGE_EXTENSION_MIME[extension];
|
|
7788
|
+
return { kind: "image", mediaType, filename };
|
|
7789
|
+
}
|
|
7790
|
+
return { kind: "document", filename: filename ?? "attachment" };
|
|
7791
|
+
}
|
|
7792
|
+
|
|
7793
|
+
// src/exulu/stored-messages.ts
|
|
7794
|
+
function dropEmptyMessages(messages) {
|
|
7795
|
+
const kept = messages.filter((m) => Array.isArray(m.parts) && m.parts.length > 0);
|
|
7796
|
+
return kept.length === messages.length ? messages : kept;
|
|
7797
|
+
}
|
|
7798
|
+
|
|
7799
|
+
// src/exulu/stored-file-url.ts
|
|
7800
|
+
var stripTrailingSlash = (s) => s.replace(/\/+$/, "");
|
|
7801
|
+
function decodeKey(rawPath) {
|
|
7802
|
+
const key = rawPath.replace(/^\/+/, "");
|
|
7803
|
+
try {
|
|
7804
|
+
return decodeURIComponent(key);
|
|
7805
|
+
} catch {
|
|
7806
|
+
return key;
|
|
7807
|
+
}
|
|
7808
|
+
}
|
|
7809
|
+
function parseStoredFileUrl(url, store) {
|
|
7810
|
+
if (!url || !store.bucket) return void 0;
|
|
7811
|
+
let parsed;
|
|
7812
|
+
try {
|
|
7813
|
+
parsed = new URL(url);
|
|
7814
|
+
} catch {
|
|
7815
|
+
return void 0;
|
|
7816
|
+
}
|
|
7817
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
7818
|
+
if (store.endpoint) {
|
|
7819
|
+
let endpoint;
|
|
7820
|
+
try {
|
|
7821
|
+
endpoint = new URL(store.endpoint);
|
|
7822
|
+
} catch {
|
|
7823
|
+
return void 0;
|
|
7824
|
+
}
|
|
7825
|
+
if (parsed.host !== endpoint.host) return void 0;
|
|
7826
|
+
const base = stripTrailingSlash(endpoint.pathname);
|
|
7827
|
+
const prefix = `${base}/${store.bucket}/`;
|
|
7828
|
+
if (!parsed.pathname.startsWith(prefix)) return void 0;
|
|
7829
|
+
const key2 = decodeKey(parsed.pathname.slice(prefix.length));
|
|
7830
|
+
return key2 ? { bucket: store.bucket, key: key2 } : void 0;
|
|
7831
|
+
}
|
|
7832
|
+
if (!parsed.host.startsWith(`${store.bucket}.s3`) || !parsed.host.endsWith(".amazonaws.com")) {
|
|
7833
|
+
return void 0;
|
|
7834
|
+
}
|
|
7835
|
+
const key = decodeKey(parsed.pathname);
|
|
7836
|
+
return key ? { bucket: store.bucket, key } : void 0;
|
|
7837
|
+
}
|
|
7838
|
+
async function resolveFreshFileUrl(url, opts) {
|
|
7839
|
+
const location = parseStoredFileUrl(url, opts);
|
|
7840
|
+
if (!location) return url;
|
|
7841
|
+
try {
|
|
7842
|
+
return await opts.sign(location.bucket, location.key);
|
|
7843
|
+
} catch (err) {
|
|
7844
|
+
console.warn(`[EXULU] could not re-sign stored file URL for key "${location.key}":`, err);
|
|
7845
|
+
return url;
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7693
7849
|
// src/exulu/generate-stream.ts
|
|
7694
7850
|
import {
|
|
7695
7851
|
convertToModelMessages,
|
|
@@ -7993,22 +8149,24 @@ var processFilePartsInMessages = async (messages, offloadCtx) => {
|
|
|
7993
8149
|
return part;
|
|
7994
8150
|
}
|
|
7995
8151
|
console.log(`[EXULU] Processing part`, part);
|
|
7996
|
-
const
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
);
|
|
8004
|
-
if (
|
|
8005
|
-
console.log(`[EXULU] Converting file part to image part: ${filename} `);
|
|
8152
|
+
const uploads = offloadCtx.exuluConfig?.fileUploads;
|
|
8153
|
+
const url = uploads?.s3Bucket ? await resolveFreshFileUrl(part.url, {
|
|
8154
|
+
endpoint: uploads.s3endpoint,
|
|
8155
|
+
bucket: uploads.s3Bucket,
|
|
8156
|
+
sign: (bucket, key) => getPresignedUrl(bucket, key, offloadCtx.exuluConfig)
|
|
8157
|
+
}) : part.url;
|
|
8158
|
+
const classified = classifyFilePart(part);
|
|
8159
|
+
console.log(`[EXULU] File part classified as ${classified.kind}: ${classified.filename}`);
|
|
8160
|
+
if (classified.kind === "image") {
|
|
8006
8161
|
return {
|
|
8007
8162
|
type: "file",
|
|
8008
|
-
mediaType:
|
|
8009
|
-
url
|
|
8163
|
+
mediaType: classified.mediaType,
|
|
8164
|
+
url,
|
|
8165
|
+
// Keep the name so later turns/steps still know the file.
|
|
8166
|
+
...classified.filename ? { filename: classified.filename } : {}
|
|
8010
8167
|
};
|
|
8011
8168
|
}
|
|
8169
|
+
const filename = classified.filename;
|
|
8012
8170
|
console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
|
|
8013
8171
|
try {
|
|
8014
8172
|
const response = await fetch(url);
|
|
@@ -8058,7 +8216,7 @@ var saveChat = async ({
|
|
|
8058
8216
|
model
|
|
8059
8217
|
}) => {
|
|
8060
8218
|
const { db } = await postgresClient();
|
|
8061
|
-
for (const message of messages) {
|
|
8219
|
+
for (const message of dropEmptyMessages(messages)) {
|
|
8062
8220
|
const mutation = db.from("agent_messages").insert({
|
|
8063
8221
|
session,
|
|
8064
8222
|
user,
|
|
@@ -8145,7 +8303,7 @@ var generateSync = async ({
|
|
|
8145
8303
|
);
|
|
8146
8304
|
messages = await validateUIMessages({
|
|
8147
8305
|
// append the new message to the previous messages:
|
|
8148
|
-
messages: [...previousMessagesContent, ...messages]
|
|
8306
|
+
messages: dropEmptyMessages([...previousMessagesContent, ...messages])
|
|
8149
8307
|
});
|
|
8150
8308
|
const contextBudget = deriveContextBudget(contextWindow);
|
|
8151
8309
|
const occupancy = contextOccupancy(messages);
|
|
@@ -8487,7 +8645,7 @@ var generateStream = async ({
|
|
|
8487
8645
|
const model = languageModel;
|
|
8488
8646
|
messages = await validateUIMessages({
|
|
8489
8647
|
// append the new message to the previous messages:
|
|
8490
|
-
messages: [...previousMessagesContent, message]
|
|
8648
|
+
messages: dropEmptyMessages([...previousMessagesContent, message])
|
|
8491
8649
|
});
|
|
8492
8650
|
const query = message.parts?.[0]?.type === "text" ? message.parts[0].text : void 0;
|
|
8493
8651
|
if (session && query) {
|
|
@@ -10270,7 +10428,8 @@ var createWorkers = async (queues2, config, contexts, evals, tools, tracer) => {
|
|
|
10270
10428
|
JOB_STATUS_ENUM.completed
|
|
10271
10429
|
]).update({
|
|
10272
10430
|
state: JOB_STATUS_ENUM.failed,
|
|
10273
|
-
|
|
10431
|
+
// A raw Error serialises to "{}" in jsonb — keep message/stack/cause.
|
|
10432
|
+
error: serializeError(error)
|
|
10274
10433
|
});
|
|
10275
10434
|
void maybePruneJobResults(db);
|
|
10276
10435
|
return;
|
|
@@ -14914,7 +15073,9 @@ var compactSession = async ({
|
|
|
14914
15073
|
}) => {
|
|
14915
15074
|
const budget = deriveContextBudget(contextWindow);
|
|
14916
15075
|
const rows = await getAgentMessages({ session: sessionID, user: user.id });
|
|
14917
|
-
const all = await validateUIMessages2({
|
|
15076
|
+
const all = await validateUIMessages2({
|
|
15077
|
+
messages: dropEmptyMessages(rows.map((r) => JSON.parse(r.content)))
|
|
15078
|
+
});
|
|
14918
15079
|
const history = sliceHistoryAtCheckpoint(all);
|
|
14919
15080
|
const { head, tail } = splitTail(history, budget.compactionTailTokens);
|
|
14920
15081
|
if (head.length === 0) {
|
|
@@ -14968,6 +15129,23 @@ ${summary}` }],
|
|
|
14968
15129
|
return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
|
|
14969
15130
|
};
|
|
14970
15131
|
|
|
15132
|
+
// src/exulu/request-error.ts
|
|
15133
|
+
import { TypeValidationError } from "ai";
|
|
15134
|
+
function describeRequestError(err) {
|
|
15135
|
+
if (err instanceof ContextCompactionRequiredError) {
|
|
15136
|
+
return { status: 413, body: err.message };
|
|
15137
|
+
}
|
|
15138
|
+
if (TypeValidationError.isInstance(err)) {
|
|
15139
|
+
const cause = err.cause;
|
|
15140
|
+
const detail = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
|
|
15141
|
+
return {
|
|
15142
|
+
status: 422,
|
|
15143
|
+
body: `The stored conversation could not be loaded: ${detail.slice(0, 400) || "type validation failed"}`
|
|
15144
|
+
};
|
|
15145
|
+
}
|
|
15146
|
+
return { status: 500, body: err instanceof Error ? err.message : String(err) };
|
|
15147
|
+
}
|
|
15148
|
+
|
|
14971
15149
|
// src/exulu/transcribe.ts
|
|
14972
15150
|
var TRANSCRIBE_SYSTEM_PROMPT = "You are a speech-to-text transcription engine. Detect the language actually spoken and transcribe it word-for-word in that same language. Never translate. Output only the transcript text \u2014 no quotes, labels, or commentary. If there is no intelligible speech, output nothing.";
|
|
14973
15151
|
function isGeminiChatTranscriptionModel(entry) {
|
|
@@ -16524,11 +16702,13 @@ ${customInstructions}` : agent.instructions;
|
|
|
16524
16702
|
});
|
|
16525
16703
|
} catch (err) {
|
|
16526
16704
|
if (headers.session) clearStreamActive(headers.session);
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16705
|
+
console.error(
|
|
16706
|
+
"[EXULU] chat request failed before streaming.",
|
|
16707
|
+
err instanceof Error ? err.message.slice(0, 500) : err
|
|
16708
|
+
);
|
|
16709
|
+
const { status, body } = describeRequestError(err);
|
|
16710
|
+
res.status(status).send(body);
|
|
16711
|
+
return;
|
|
16532
16712
|
}
|
|
16533
16713
|
result.stream.consumeStream();
|
|
16534
16714
|
result.stream.pipeUIMessageStreamToResponse(res, {
|
|
@@ -16667,11 +16847,13 @@ ${customInstructions}` : agent.instructions;
|
|
|
16667
16847
|
}
|
|
16668
16848
|
});
|
|
16669
16849
|
} catch (err) {
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
|
|
16674
|
-
|
|
16850
|
+
console.error(
|
|
16851
|
+
"[EXULU] run request failed.",
|
|
16852
|
+
err instanceof Error ? err.message.slice(0, 500) : err
|
|
16853
|
+
);
|
|
16854
|
+
const { status, body } = describeRequestError(err);
|
|
16855
|
+
res.status(status).send(body);
|
|
16856
|
+
return;
|
|
16675
16857
|
}
|
|
16676
16858
|
res.status(200).json(response);
|
|
16677
16859
|
return;
|
|
@@ -18995,6 +19177,7 @@ ${style.markdown}` : params.prompt;
|
|
|
18995
19177
|
".jpeg": "image/jpeg",
|
|
18996
19178
|
".gif": "image/gif",
|
|
18997
19179
|
".webp": "image/webp",
|
|
19180
|
+
".bmp": "image/bmp",
|
|
18998
19181
|
".svg": "image/svg+xml",
|
|
18999
19182
|
".pdf": "application/pdf",
|
|
19000
19183
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
package/ee/workers.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { redisServer } from "@EE/queues/server.ts";
|
|
|
3
3
|
import { guardRedisStartup, logRedisErrors } from "@EE/queues/redis-startup.ts";
|
|
4
4
|
import { Job, Worker, type JobState } from "bullmq";
|
|
5
5
|
import { bullmq } from "@SRC/validators/bullmq.ts";
|
|
6
|
+
import { serializeError } from "@SRC/utils/serialize-error.ts";
|
|
6
7
|
import { getEnabledTools } from "@SRC/utils/enabled-tools.ts";
|
|
7
8
|
import { ExuluStorage } from "@SRC/exulu/storage.ts";
|
|
8
9
|
import type { ExuluAgent } from "@EXULU_TYPES/models/agent.ts";
|
|
@@ -1212,7 +1213,8 @@ export const createWorkers = async (
|
|
|
1212
1213
|
])
|
|
1213
1214
|
.update({
|
|
1214
1215
|
state: JOB_STATUS_ENUM.failed,
|
|
1215
|
-
|
|
1216
|
+
// A raw Error serialises to "{}" in jsonb — keep message/stack/cause.
|
|
1217
|
+
error: serializeError(error),
|
|
1216
1218
|
});
|
|
1217
1219
|
|
|
1218
1220
|
// Cap the table as rows become terminal (every Nth, idempotent).
|