@exulu/backend 3.7.1 → 3.7.2

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 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 cleaned = word.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase();
10216
- if (!cleaned) {
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(cleaned);
10223
+ return stemmer.stem(core);
10221
10224
  } catch (error) {
10222
10225
  console.warn(`[EXULU] Error stemming word "${word}":`, error);
10223
- return cleaned;
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 { processed: stemmedQuery } = preprocessQuery(query, {
12741
- enableStemming: true,
12742
- detectLanguage: true
12743
- });
12767
+ const texts = resolveSearchQueryTexts(query);
12744
12768
  _preMs = Date.now() - _tp;
12745
- console.log("[EXULU] Stemmed query:", stemmedQuery);
12746
- if (stemmedQuery) {
12747
- query = stemmedQuery;
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([query], { inputType: "query" });
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, plainto_tsquery('${lang}', ?))`).join(", ");
12847
- const ftRankParams = languages.map(() => query);
12848
- const ftMatchExpression = languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ");
12849
- const ftMatchParams = languages.map(() => query);
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, plainto_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
12890
- languages.map(() => query)
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, plainto_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
12916
- [...languages.map(() => query), cutoffs?.tsvector || 0]
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,62 @@ 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/generate-stream.ts
16679
16790
  var import_ai6 = require("ai");
16680
16791
 
16681
16792
  // src/exulu/context-guard.ts
@@ -16993,22 +17104,19 @@ var processFilePartsInMessages = async (messages, offloadCtx) => {
16993
17104
  return part;
16994
17105
  }
16995
17106
  console.log(`[EXULU] Processing part`, part);
16996
- const { mediaType, url, filename } = part;
16997
- console.log(`[EXULU] Media type: ${mediaType}`);
16998
- console.log(`[EXULU] URL: ${url}`);
16999
- console.log(`[EXULU] Filename: ${filename}`);
17000
- const imageTypes = [".png", ".jpeg", ".jpg", ".gif", ".webp"];
17001
- const imageType = imageTypes.find(
17002
- (type) => filename.toLowerCase().includes(type.toLowerCase())
17003
- );
17004
- if (imageType) {
17005
- console.log(`[EXULU] Converting file part to image part: ${filename} `);
17107
+ const { url } = part;
17108
+ const classified = classifyFilePart(part);
17109
+ console.log(`[EXULU] File part classified as ${classified.kind}: ${classified.filename}`);
17110
+ if (classified.kind === "image") {
17006
17111
  return {
17007
17112
  type: "file",
17008
- mediaType: `image/${imageType.replace(".", "")}`,
17009
- url
17113
+ mediaType: classified.mediaType,
17114
+ url,
17115
+ // Keep the name so later turns/steps still know the file.
17116
+ ...classified.filename ? { filename: classified.filename } : {}
17010
17117
  };
17011
17118
  }
17119
+ const filename = classified.filename;
17012
17120
  console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
17013
17121
  try {
17014
17122
  const response = await fetch(url);
@@ -17058,7 +17166,7 @@ var saveChat = async ({
17058
17166
  model
17059
17167
  }) => {
17060
17168
  const { db: db2 } = await postgresClient();
17061
- for (const message of messages) {
17169
+ for (const message of dropEmptyMessages(messages)) {
17062
17170
  const mutation = db2.from("agent_messages").insert({
17063
17171
  session,
17064
17172
  user,
@@ -17145,7 +17253,7 @@ var generateSync = async ({
17145
17253
  );
17146
17254
  messages = await (0, import_ai6.validateUIMessages)({
17147
17255
  // append the new message to the previous messages:
17148
- messages: [...previousMessagesContent, ...messages]
17256
+ messages: dropEmptyMessages([...previousMessagesContent, ...messages])
17149
17257
  });
17150
17258
  const contextBudget = deriveContextBudget(contextWindow);
17151
17259
  const occupancy = contextOccupancy(messages);
@@ -17487,7 +17595,7 @@ var generateStream = async ({
17487
17595
  const model = languageModel;
17488
17596
  messages = await (0, import_ai6.validateUIMessages)({
17489
17597
  // append the new message to the previous messages:
17490
- messages: [...previousMessagesContent, message]
17598
+ messages: dropEmptyMessages([...previousMessagesContent, message])
17491
17599
  });
17492
17600
  const query = message.parts?.[0]?.type === "text" ? message.parts[0].text : void 0;
17493
17601
  if (session && query) {
@@ -19291,7 +19399,8 @@ var createWorkers = async (queues2, config, contexts, evals, tools, tracer) => {
19291
19399
  JOB_STATUS_ENUM.completed
19292
19400
  ]).update({
19293
19401
  state: JOB_STATUS_ENUM.failed,
19294
- error
19402
+ // A raw Error serialises to "{}" in jsonb — keep message/stack/cause.
19403
+ error: serializeError(error)
19295
19404
  });
19296
19405
  void maybePruneJobResults(db2);
19297
19406
  return;
@@ -23821,7 +23930,7 @@ var import_fs3 = __toESM(require("fs"), 1);
23821
23930
  var import_node_crypto15 = require("crypto");
23822
23931
  var import_api2 = require("@opentelemetry/api");
23823
23932
  var import_jszip3 = __toESM(require("jszip"), 1);
23824
- var import_ai11 = require("ai");
23933
+ var import_ai12 = require("ai");
23825
23934
  var import_cookie_parser = __toESM(require("cookie-parser"), 1);
23826
23935
  init_statistics2();
23827
23936
 
@@ -23980,7 +24089,9 @@ var compactSession = async ({
23980
24089
  }) => {
23981
24090
  const budget = deriveContextBudget(contextWindow);
23982
24091
  const rows = await getAgentMessages({ session: sessionID, user: user.id });
23983
- const all = await (0, import_ai10.validateUIMessages)({ messages: rows.map((r) => JSON.parse(r.content)) });
24092
+ const all = await (0, import_ai10.validateUIMessages)({
24093
+ messages: dropEmptyMessages(rows.map((r) => JSON.parse(r.content)))
24094
+ });
23984
24095
  const history = sliceHistoryAtCheckpoint(all);
23985
24096
  const { head, tail } = splitTail(history, budget.compactionTailTokens);
23986
24097
  if (head.length === 0) {
@@ -24034,6 +24145,25 @@ ${summary}` }],
24034
24145
  return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
24035
24146
  };
24036
24147
 
24148
+ // src/exulu/request-error.ts
24149
+ init_cjs_shims();
24150
+ var import_ai11 = require("ai");
24151
+ init_context_budget();
24152
+ function describeRequestError(err) {
24153
+ if (err instanceof ContextCompactionRequiredError) {
24154
+ return { status: 413, body: err.message };
24155
+ }
24156
+ if (import_ai11.TypeValidationError.isInstance(err)) {
24157
+ const cause = err.cause;
24158
+ const detail = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
24159
+ return {
24160
+ status: 422,
24161
+ body: `The stored conversation could not be loaded: ${detail.slice(0, 400) || "type validation failed"}`
24162
+ };
24163
+ }
24164
+ return { status: 500, body: err instanceof Error ? err.message : String(err) };
24165
+ }
24166
+
24037
24167
  // src/exulu/transcribe.ts
24038
24168
  init_cjs_shims();
24039
24169
  init_env();
@@ -25639,11 +25769,13 @@ ${customInstructions}` : agent.instructions;
25639
25769
  });
25640
25770
  } catch (err) {
25641
25771
  if (headers.session) clearStreamActive(headers.session);
25642
- if (err instanceof ContextCompactionRequiredError) {
25643
- res.status(413).send(err.message);
25644
- return;
25645
- }
25646
- throw err;
25772
+ console.error(
25773
+ "[EXULU] chat request failed before streaming.",
25774
+ err instanceof Error ? err.message.slice(0, 500) : err
25775
+ );
25776
+ const { status, body } = describeRequestError(err);
25777
+ res.status(status).send(body);
25778
+ return;
25647
25779
  }
25648
25780
  result.stream.consumeStream();
25649
25781
  result.stream.pipeUIMessageStreamToResponse(res, {
@@ -25678,7 +25810,7 @@ ${customInstructions}` : agent.instructions;
25678
25810
  else message2 = JSON.stringify(error);
25679
25811
  return mapStreamErrorMessage(message2);
25680
25812
  },
25681
- generateMessageId: (0, import_ai11.createIdGenerator)({
25813
+ generateMessageId: (0, import_ai12.createIdGenerator)({
25682
25814
  prefix: "msg_",
25683
25815
  size: 16
25684
25816
  }),
@@ -25782,11 +25914,13 @@ ${customInstructions}` : agent.instructions;
25782
25914
  }
25783
25915
  });
25784
25916
  } catch (err) {
25785
- if (err instanceof ContextCompactionRequiredError) {
25786
- res.status(413).send(err.message);
25787
- return;
25788
- }
25789
- throw err;
25917
+ console.error(
25918
+ "[EXULU] run request failed.",
25919
+ err instanceof Error ? err.message.slice(0, 500) : err
25920
+ );
25921
+ const { status, body } = describeRequestError(err);
25922
+ res.status(status).send(body);
25923
+ return;
25790
25924
  }
25791
25925
  res.status(200).json(response);
25792
25926
  return;
@@ -28110,6 +28244,7 @@ ${style.markdown}` : params.prompt;
28110
28244
  ".jpeg": "image/jpeg",
28111
28245
  ".gif": "image/gif",
28112
28246
  ".webp": "image/webp",
28247
+ ".bmp": "image/bmp",
28113
28248
  ".svg": "image/svg+xml",
28114
28249
  ".pdf": "application/pdf",
28115
28250
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -29136,7 +29271,7 @@ init_cjs_shims();
29136
29271
 
29137
29272
  // src/exulu/evals.ts
29138
29273
  init_cjs_shims();
29139
- var import_ai12 = require("ai");
29274
+ var import_ai13 = require("ai");
29140
29275
  var ExuluEval = class {
29141
29276
  id;
29142
29277
  name;
@@ -29176,7 +29311,7 @@ var ExuluEval = class {
29176
29311
  init_resolve_model();
29177
29312
  init_singleton();
29178
29313
  var import_zod20 = require("zod");
29179
- var import_ai13 = require("ai");
29314
+ var import_ai14 = require("ai");
29180
29315
  var llmAsJudgeEval = () => {
29181
29316
  if (process.env.REDIS_HOST?.length && process.env.REDIS_PORT?.length) {
29182
29317
  return new ExuluEval({
@@ -29219,13 +29354,13 @@ var llmAsJudgeEval = () => {
29219
29354
  rbacBypass: true
29220
29355
  });
29221
29356
  console.log("[EXULU] prompt", prompt);
29222
- const { output } = await (0, import_ai13.generateText)({
29357
+ const { output } = await (0, import_ai14.generateText)({
29223
29358
  temperature: 0,
29224
29359
  model: resolved.languageModel,
29225
29360
  system: "",
29226
29361
  prompt,
29227
29362
  maxRetries: 2,
29228
- output: import_ai13.Output.object({
29363
+ output: import_ai14.Output.object({
29229
29364
  schema: import_zod20.z.object({
29230
29365
  score: import_zod20.z.number().min(0).max(100).describe("The score between 0 and 100.")
29231
29366
  })
@@ -32839,7 +32974,7 @@ var MarkdownChunker = class {
32839
32974
  init_cjs_shims();
32840
32975
  var fs4 = __toESM(require("fs"), 1);
32841
32976
  var path3 = __toESM(require("path"), 1);
32842
- var import_ai14 = require("ai");
32977
+ var import_ai15 = require("ai");
32843
32978
  var import_zod27 = require("zod");
32844
32979
  var import_p_limit = __toESM(require("p-limit"), 1);
32845
32980
  var import_crypto3 = require("crypto");
@@ -33295,9 +33430,9 @@ If the page contains a flow-chart, schematic, technical drawing or control board
33295
33430
 
33296
33431
  ### 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
33432
  `;
33298
- const result = await (0, import_ai14.generateText)({
33433
+ const result = await (0, import_ai15.generateText)({
33299
33434
  model,
33300
- output: import_ai14.Output.object({
33435
+ output: import_ai15.Output.object({
33301
33436
  schema: import_zod27.z.object({
33302
33437
  needs_correction: import_zod27.z.boolean(),
33303
33438
  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 cleaned = word.replace(/[^\p{L}\p{N}]/gu, "").toLowerCase();
1351
- if (!cleaned) {
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(cleaned);
1358
+ return stemmer.stem(core);
1356
1359
  } catch (error) {
1357
1360
  console.warn(`[EXULU] Error stemming word "${word}":`, error);
1358
- return cleaned;
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 { processed: stemmedQuery } = preprocessQuery(query, {
3834
- enableStemming: true,
3835
- detectLanguage: true
3836
- });
3860
+ const texts = resolveSearchQueryTexts(query);
3837
3861
  _preMs = Date.now() - _tp;
3838
- console.log("[EXULU] Stemmed query:", stemmedQuery);
3839
- if (stemmedQuery) {
3840
- query = stemmedQuery;
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([query], { inputType: "query" });
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, plainto_tsquery('${lang}', ?))`).join(", ");
3940
- const ftRankParams = languages.map(() => query);
3941
- const ftMatchExpression = languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ");
3942
- const ftMatchParams = languages.map(() => query);
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, plainto_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
3983
- languages.map(() => query)
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, plainto_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
4009
- [...languages.map(() => query), cutoffs?.tsvector || 0]
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,91 @@ 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
+
7693
7799
  // src/exulu/generate-stream.ts
7694
7800
  import {
7695
7801
  convertToModelMessages,
@@ -7993,22 +8099,19 @@ var processFilePartsInMessages = async (messages, offloadCtx) => {
7993
8099
  return part;
7994
8100
  }
7995
8101
  console.log(`[EXULU] Processing part`, part);
7996
- const { mediaType, url, filename } = part;
7997
- console.log(`[EXULU] Media type: ${mediaType}`);
7998
- console.log(`[EXULU] URL: ${url}`);
7999
- console.log(`[EXULU] Filename: ${filename}`);
8000
- const imageTypes = [".png", ".jpeg", ".jpg", ".gif", ".webp"];
8001
- const imageType = imageTypes.find(
8002
- (type) => filename.toLowerCase().includes(type.toLowerCase())
8003
- );
8004
- if (imageType) {
8005
- console.log(`[EXULU] Converting file part to image part: ${filename} `);
8102
+ const { url } = part;
8103
+ const classified = classifyFilePart(part);
8104
+ console.log(`[EXULU] File part classified as ${classified.kind}: ${classified.filename}`);
8105
+ if (classified.kind === "image") {
8006
8106
  return {
8007
8107
  type: "file",
8008
- mediaType: `image/${imageType.replace(".", "")}`,
8009
- url
8108
+ mediaType: classified.mediaType,
8109
+ url,
8110
+ // Keep the name so later turns/steps still know the file.
8111
+ ...classified.filename ? { filename: classified.filename } : {}
8010
8112
  };
8011
8113
  }
8114
+ const filename = classified.filename;
8012
8115
  console.log(`[EXULU] Converting file part to text using officeparser: ${filename}`);
8013
8116
  try {
8014
8117
  const response = await fetch(url);
@@ -8058,7 +8161,7 @@ var saveChat = async ({
8058
8161
  model
8059
8162
  }) => {
8060
8163
  const { db } = await postgresClient();
8061
- for (const message of messages) {
8164
+ for (const message of dropEmptyMessages(messages)) {
8062
8165
  const mutation = db.from("agent_messages").insert({
8063
8166
  session,
8064
8167
  user,
@@ -8145,7 +8248,7 @@ var generateSync = async ({
8145
8248
  );
8146
8249
  messages = await validateUIMessages({
8147
8250
  // append the new message to the previous messages:
8148
- messages: [...previousMessagesContent, ...messages]
8251
+ messages: dropEmptyMessages([...previousMessagesContent, ...messages])
8149
8252
  });
8150
8253
  const contextBudget = deriveContextBudget(contextWindow);
8151
8254
  const occupancy = contextOccupancy(messages);
@@ -8487,7 +8590,7 @@ var generateStream = async ({
8487
8590
  const model = languageModel;
8488
8591
  messages = await validateUIMessages({
8489
8592
  // append the new message to the previous messages:
8490
- messages: [...previousMessagesContent, message]
8593
+ messages: dropEmptyMessages([...previousMessagesContent, message])
8491
8594
  });
8492
8595
  const query = message.parts?.[0]?.type === "text" ? message.parts[0].text : void 0;
8493
8596
  if (session && query) {
@@ -10270,7 +10373,8 @@ var createWorkers = async (queues2, config, contexts, evals, tools, tracer) => {
10270
10373
  JOB_STATUS_ENUM.completed
10271
10374
  ]).update({
10272
10375
  state: JOB_STATUS_ENUM.failed,
10273
- error
10376
+ // A raw Error serialises to "{}" in jsonb — keep message/stack/cause.
10377
+ error: serializeError(error)
10274
10378
  });
10275
10379
  void maybePruneJobResults(db);
10276
10380
  return;
@@ -14914,7 +15018,9 @@ var compactSession = async ({
14914
15018
  }) => {
14915
15019
  const budget = deriveContextBudget(contextWindow);
14916
15020
  const rows = await getAgentMessages({ session: sessionID, user: user.id });
14917
- const all = await validateUIMessages2({ messages: rows.map((r) => JSON.parse(r.content)) });
15021
+ const all = await validateUIMessages2({
15022
+ messages: dropEmptyMessages(rows.map((r) => JSON.parse(r.content)))
15023
+ });
14918
15024
  const history = sliceHistoryAtCheckpoint(all);
14919
15025
  const { head, tail } = splitTail(history, budget.compactionTailTokens);
14920
15026
  if (head.length === 0) {
@@ -14968,6 +15074,23 @@ ${summary}` }],
14968
15074
  return { checkpoint, occupancyEstimate, originalTokens, summaryTokens };
14969
15075
  };
14970
15076
 
15077
+ // src/exulu/request-error.ts
15078
+ import { TypeValidationError } from "ai";
15079
+ function describeRequestError(err) {
15080
+ if (err instanceof ContextCompactionRequiredError) {
15081
+ return { status: 413, body: err.message };
15082
+ }
15083
+ if (TypeValidationError.isInstance(err)) {
15084
+ const cause = err.cause;
15085
+ const detail = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
15086
+ return {
15087
+ status: 422,
15088
+ body: `The stored conversation could not be loaded: ${detail.slice(0, 400) || "type validation failed"}`
15089
+ };
15090
+ }
15091
+ return { status: 500, body: err instanceof Error ? err.message : String(err) };
15092
+ }
15093
+
14971
15094
  // src/exulu/transcribe.ts
14972
15095
  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
15096
  function isGeminiChatTranscriptionModel(entry) {
@@ -16524,11 +16647,13 @@ ${customInstructions}` : agent.instructions;
16524
16647
  });
16525
16648
  } catch (err) {
16526
16649
  if (headers.session) clearStreamActive(headers.session);
16527
- if (err instanceof ContextCompactionRequiredError) {
16528
- res.status(413).send(err.message);
16529
- return;
16530
- }
16531
- throw err;
16650
+ console.error(
16651
+ "[EXULU] chat request failed before streaming.",
16652
+ err instanceof Error ? err.message.slice(0, 500) : err
16653
+ );
16654
+ const { status, body } = describeRequestError(err);
16655
+ res.status(status).send(body);
16656
+ return;
16532
16657
  }
16533
16658
  result.stream.consumeStream();
16534
16659
  result.stream.pipeUIMessageStreamToResponse(res, {
@@ -16667,11 +16792,13 @@ ${customInstructions}` : agent.instructions;
16667
16792
  }
16668
16793
  });
16669
16794
  } catch (err) {
16670
- if (err instanceof ContextCompactionRequiredError) {
16671
- res.status(413).send(err.message);
16672
- return;
16673
- }
16674
- throw err;
16795
+ console.error(
16796
+ "[EXULU] run request failed.",
16797
+ err instanceof Error ? err.message.slice(0, 500) : err
16798
+ );
16799
+ const { status, body } = describeRequestError(err);
16800
+ res.status(status).send(body);
16801
+ return;
16675
16802
  }
16676
16803
  res.status(200).json(response);
16677
16804
  return;
@@ -18995,6 +19122,7 @@ ${style.markdown}` : params.prompt;
18995
19122
  ".jpeg": "image/jpeg",
18996
19123
  ".gif": "image/gif",
18997
19124
  ".webp": "image/webp",
19125
+ ".bmp": "image/bmp",
18998
19126
  ".svg": "image/svg+xml",
18999
19127
  ".pdf": "application/pdf",
19000
19128
  ".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
- error,
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).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@exulu/backend",
3
3
  "author": "Qventu Bv.",
4
- "version": "3.7.1",
4
+ "version": "3.7.2",
5
5
  "main": "./dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {