@exulu/backend 3.7.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{chunk-27K2CO47.js → chunk-QMN6MVHQ.js} +1 -1
  2. package/dist/{chunk-AWMU6QXB.js → chunk-RBEWHG7I.js} +297 -39
  3. package/dist/cli/start-whisper.js +1 -1
  4. package/dist/{convert-exulu-tools-to-ai-sdk-tools-XNQ6Q3X6.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
  5. package/dist/index.cjs +617 -223
  6. package/dist/index.d.cts +3 -1
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.js +300 -186
  9. package/dist/{python-setup-JZGHWQCG.js → python-setup-DRJ3QX5F.js} +1 -1
  10. package/ee/LICENSE.md +2 -2
  11. package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
  12. package/ee/agentic-retrieval/pipeline/config.ts +15 -0
  13. package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
  14. package/ee/agentic-retrieval/pipeline/index.ts +67 -13
  15. package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
  16. package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
  17. package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
  18. package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
  19. package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
  20. package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
  21. package/ee/agentic-retrieval/pipeline/search.ts +9 -6
  22. package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
  23. package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
  24. package/ee/agentic-retrieval/pipeline/types.ts +2 -0
  25. package/ee/python/documents/processing/README.md +2 -3
  26. package/ee/python/documents/processing/doc_processor.ts +21 -61
  27. package/ee/python/documents/processing/split_pdf.py +25 -30
  28. package/ee/python/documents/processing/tests/__init__.py +0 -0
  29. package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
  30. package/ee/python/requirements.txt +12 -2
  31. package/ee/python/setup.sh +40 -1
  32. package/ee/python/transcription/pipeline.py +109 -15
  33. package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
  34. package/ee/workers.ts +2 -7
  35. package/license.md +2 -2
  36. package/package.json +3 -4
  37. package/scripts/postinstall.cjs +52 -1
  38. package/ee/python/documents/processing/document_to_markdown.py +0 -413
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  isPythonEnvironmentSetup,
6
6
  setupPythonEnvironment,
7
7
  validatePythonEnvironment
8
- } from "./chunk-27K2CO47.js";
8
+ } from "./chunk-QMN6MVHQ.js";
9
9
  import {
10
10
  COMPACTION_INSUFFICIENT,
11
11
  ContextCompactionRequiredError,
@@ -62,6 +62,7 @@ import {
62
62
  imageAttachmentGuard,
63
63
  initAudit,
64
64
  invalidateBudgetCaches,
65
+ isIgnoredArtifactPath,
65
66
  isLiteLLMEnabled,
66
67
  listS3ObjectsByPrefix,
67
68
  listTagsByPrefix,
@@ -75,6 +76,7 @@ import {
75
76
  resolveReranker,
76
77
  sanitizeName,
77
78
  sanitizeToolName,
79
+ sessionFilePrefix,
78
80
  setBudgetSettings,
79
81
  setLiteLLMPackageRoot,
80
82
  sliceHistoryAtCheckpoint,
@@ -88,7 +90,7 @@ import {
88
90
  verifyCredentialNonce,
89
91
  waitForLiteLLMReady,
90
92
  withRetry
91
- } from "./chunk-AWMU6QXB.js";
93
+ } from "./chunk-RBEWHG7I.js";
92
94
  import {
93
95
  LiteLLMAdminError,
94
96
  findLiteLLMModel,
@@ -1418,6 +1420,67 @@ function resolveSearchQueryTexts(query) {
1418
1420
  hybridOrQuery: buildFullTextOrQuery(query, ftsText)
1419
1421
  };
1420
1422
  }
1423
+ function chooseFullTextQuery(opts) {
1424
+ if (opts.strictMatches || countOrTerms(opts.orText) > MAX_OR_TERMS) {
1425
+ return { fn: "plainto_tsquery", text: opts.strictText };
1426
+ }
1427
+ return { fn: "websearch_to_tsquery", text: opts.orText };
1428
+ }
1429
+ var MAX_OR_TERMS = 12;
1430
+ function countOrTerms(orText) {
1431
+ return orText.trim() ? orText.split(/\s+or\s+/i).length : 0;
1432
+ }
1433
+
1434
+ // src/graphql/resolvers/expand-neighbours.ts
1435
+ function planNeighbourFetch(results, expand) {
1436
+ const before = Math.max(0, expand.before ?? 0);
1437
+ const after = Math.max(0, expand.after ?? 0);
1438
+ const plan = /* @__PURE__ */ new Map();
1439
+ if (before === 0 && after === 0) return plan;
1440
+ const present = new Set(results.map((r) => `${r.item_id}-${r.chunk_index}`));
1441
+ for (const r of results) {
1442
+ for (let i = r.chunk_index - before; i <= r.chunk_index + after; i++) {
1443
+ if (i < 0 || i === r.chunk_index || present.has(`${r.item_id}-${i}`)) continue;
1444
+ if (!plan.has(r.item_id)) plan.set(r.item_id, /* @__PURE__ */ new Set());
1445
+ plan.get(r.item_id).add(i);
1446
+ }
1447
+ }
1448
+ return plan;
1449
+ }
1450
+ function mergeNeighbours(results, rows, plan, context) {
1451
+ const byItem = /* @__PURE__ */ new Map();
1452
+ for (const r of results) if (!byItem.has(r.item_id)) byItem.set(r.item_id, r);
1453
+ const merged = /* @__PURE__ */ new Map();
1454
+ for (const r of results) merged.set(`${r.item_id}-${r.chunk_index}`, r);
1455
+ for (const row of rows) {
1456
+ if (!plan.get(row.source)?.has(row.chunk_index)) continue;
1457
+ const key = `${row.source}-${row.chunk_index}`;
1458
+ if (merged.has(key)) continue;
1459
+ const parent = byItem.get(row.source);
1460
+ if (!parent) continue;
1461
+ merged.set(key, {
1462
+ chunk_content: row.content,
1463
+ chunk_index: row.chunk_index,
1464
+ chunk_id: row.id,
1465
+ chunk_source: row.source,
1466
+ chunk_metadata: row.metadata,
1467
+ chunk_created_at: row.createdAt,
1468
+ chunk_updated_at: row.updatedAt,
1469
+ item_updated_at: parent.item_updated_at,
1470
+ item_created_at: parent.item_created_at,
1471
+ item_id: parent.item_id,
1472
+ item_external_id: parent.item_external_id,
1473
+ item_name: parent.item_name,
1474
+ chunk_cosine_distance: 0,
1475
+ chunk_fts_rank: 0,
1476
+ chunk_hybrid_score: 0,
1477
+ context
1478
+ });
1479
+ }
1480
+ return Array.from(merged.values()).sort(
1481
+ (a, b) => a.item_id === b.item_id ? a.chunk_index - b.chunk_index : 0
1482
+ );
1483
+ }
1421
1484
 
1422
1485
  // src/graphql/resolvers/field-allow-list.ts
1423
1486
  var ALWAYS_ALLOWED = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt"]);
@@ -2385,6 +2448,14 @@ var agentsSchema = {
2385
2448
  name: "max_tool_steps",
2386
2449
  type: "number"
2387
2450
  },
2451
+ {
2452
+ // Thinking budget of the answer model, forwarded as LiteLLM's
2453
+ // reasoning_effort ("none" | "disable" | "minimal" | "low" | "medium" |
2454
+ // "high"). null = provider default. See resolve-reasoning-effort.ts.
2455
+ // Auto-ALTERed on boot.
2456
+ name: "reasoning_effort",
2457
+ type: "text"
2458
+ },
2388
2459
  {
2389
2460
  name: "guest_access",
2390
2461
  type: "boolean",
@@ -3099,6 +3170,14 @@ var convertContextToTableDefinition = (context) => {
3099
3170
  return addCoreFields(definition);
3100
3171
  };
3101
3172
 
3173
+ // src/graphql/resolvers/query-embedding-policy.ts
3174
+ function needsQueryEmbedding(method) {
3175
+ return method !== "tsvector";
3176
+ }
3177
+ function boostsWithQueryEntities(method) {
3178
+ return method !== "tsvector";
3179
+ }
3180
+
3102
3181
  // src/exulu/entities/config.ts
3103
3182
  var hydrateEntityTypes = async (context) => {
3104
3183
  const byName = /* @__PURE__ */ new Map();
@@ -3863,7 +3942,9 @@ var vectorSearch = async ({
3863
3942
  const embedText = texts.embedText;
3864
3943
  hybridOrQuery = texts.hybridOrQuery;
3865
3944
  query = texts.ftsText;
3866
- if (queryEmbedding && queryEmbedding.length) {
3945
+ if (!needsQueryEmbedding(method)) {
3946
+ _embedSource = "none";
3947
+ } else if (queryEmbedding && queryEmbedding.length) {
3867
3948
  vector = queryEmbedding;
3868
3949
  _embedSource = "reused";
3869
3950
  } else {
@@ -3896,8 +3977,10 @@ var vectorSearch = async ({
3896
3977
  vector = queryVector;
3897
3978
  _embedSource = "computed";
3898
3979
  }
3899
- vectorStr = `ARRAY[${vector.join(",")}]`;
3900
- vectorExpr = `${vectorStr}::vector`;
3980
+ if (vector.length) {
3981
+ vectorStr = `ARRAY[${vector.join(",")}]`;
3982
+ vectorExpr = `${vectorStr}::vector`;
3983
+ }
3901
3984
  }
3902
3985
  let keywordsQuery = [];
3903
3986
  if (keywords) {
@@ -3955,14 +4038,22 @@ var vectorSearch = async ({
3955
4038
  ]);
3956
4039
  resultChunks = await chunksQuery;
3957
4040
  break;
3958
- case "hybridSearch":
4041
+ case "hybridSearch": {
4042
+ let strictMatches = false;
4043
+ if (query && hybridOrQuery) {
4044
+ const probe = await db(chunksTable + " as chunks").select(db.raw("1")).whereRaw(`(${languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ")})`, languages.map(() => query)).first();
4045
+ strictMatches = Boolean(probe);
4046
+ }
4047
+ const fullText = chooseFullTextQuery({ strictMatches, strictText: query ?? "", orText: hybridOrQuery });
4048
+ const ftsFn = fullText.fn;
4049
+ hybridOrQuery = fullText.text;
3959
4050
  const matchCount = Math.min(limit * 2);
3960
4051
  const fullTextWeight = 2;
3961
4052
  const semanticWeight = 1;
3962
4053
  const rrfK = 50;
3963
- const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ");
4054
+ const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ");
3964
4055
  const ftRankParams = languages.map(() => hybridOrQuery);
3965
- const ftMatchExpression = languages.map((lang) => `chunks.fts @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
4056
+ const ftMatchExpression = languages.map((lang) => `chunks.fts @@ ${ftsFn}('${lang}', ?)`).join(" OR ");
3966
4057
  const ftMatchParams = languages.map(() => hybridOrQuery);
3967
4058
  let fullTextQuery = db(chunksTable + " as chunks").select([
3968
4059
  "chunks.id",
@@ -4003,7 +4094,7 @@ var vectorSearch = async ({
4003
4094
  db.raw('items."updatedAt" as item_updated_at'),
4004
4095
  db.raw('items."createdAt" as item_created_at'),
4005
4096
  db.raw(
4006
- `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
4097
+ `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) AS fts_rank`,
4007
4098
  languages.map(() => hybridOrQuery)
4008
4099
  ),
4009
4100
  db.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
@@ -4029,12 +4120,14 @@ var vectorSearch = async ({
4029
4120
  `,
4030
4121
  [rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
4031
4122
  ).whereRaw(
4032
- `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
4123
+ `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) > ?)`,
4033
4124
  [...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
4034
4125
  ).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
4035
4126
  cutoffs?.cosineDistance || 0
4036
4127
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
4037
4128
  resultChunks = await hybridQuery;
4129
+ break;
4130
+ }
4038
4131
  }
4039
4132
  if (process.env.EXULU_VS_TIMING) {
4040
4133
  console.log(
@@ -4086,7 +4179,7 @@ var vectorSearch = async ({
4086
4179
  }
4087
4180
  let queryEntities = [];
4088
4181
  let entityInsights;
4089
- if (entitiesOn && rawQuery) {
4182
+ if (entitiesOn && rawQuery && boostsWithQueryEntities(method)) {
4090
4183
  try {
4091
4184
  const types = await hydrateEntityTypes(context);
4092
4185
  const { mentions: queryMentions } = await extractEntitiesForItem({
@@ -4118,104 +4211,18 @@ var vectorSearch = async ({
4118
4211
  }
4119
4212
  results = results.slice(0, limit);
4120
4213
  if (expand?.before || expand?.after) {
4121
- const expandedMap = /* @__PURE__ */ new Map();
4122
- for (const chunk of results) {
4123
- expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
4124
- }
4125
- if (expand?.before) {
4126
- for (const chunk of results) {
4127
- const indicesToFetch = Array.from(
4128
- { length: expand.before },
4129
- (_, i) => chunk.chunk_index - expand.before + i
4130
- ).filter((index) => index >= 0);
4131
- await Promise.all(
4132
- indicesToFetch.map(async (index) => {
4133
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
4134
- return;
4135
- }
4136
- const expandedChunk = await db(chunksTable).where({
4137
- source: chunk.item_id,
4138
- chunk_index: index
4139
- }).first();
4140
- if (expandedChunk) {
4141
- if (expandedChunk) {
4142
- expandedMap.set(`${chunk.item_id}-${index}`, {
4143
- chunk_content: expandedChunk.content,
4144
- chunk_index: expandedChunk.chunk_index,
4145
- chunk_id: expandedChunk.id,
4146
- chunk_source: expandedChunk.source,
4147
- chunk_metadata: expandedChunk.metadata,
4148
- chunk_created_at: expandedChunk.createdAt,
4149
- chunk_updated_at: expandedChunk.updatedAt,
4150
- item_updated_at: chunk.item_updated_at,
4151
- item_created_at: chunk.item_created_at,
4152
- item_id: chunk.item_id,
4153
- item_external_id: chunk.item_external_id,
4154
- item_name: chunk.item_name,
4155
- chunk_cosine_distance: 0,
4156
- chunk_fts_rank: 0,
4157
- chunk_hybrid_score: 0,
4158
- context: {
4159
- name: table.name.singular,
4160
- id: table.id || ""
4161
- }
4162
- });
4163
- }
4164
- }
4165
- })
4166
- );
4167
- }
4214
+ const plan = planNeighbourFetch(results, expand);
4215
+ if (plan.size > 0) {
4216
+ const itemIds = Array.from(plan.keys());
4217
+ const indices = Array.from(new Set(Array.from(plan.values()).flatMap((s) => Array.from(s))));
4218
+ const rows = await db(chunksTable).select(["id", "source", "chunk_index", "content", "metadata", "createdAt", "updatedAt"]).whereIn("source", itemIds).whereIn("chunk_index", indices);
4219
+ results = mergeNeighbours(results, rows, plan, { name: table.name.singular, id: table.id || "" });
4168
4220
  }
4169
- if (expand?.after) {
4170
- for (const chunk of results) {
4171
- const indicesToFetch = Array.from(
4172
- { length: expand.after },
4173
- (_, i) => chunk.chunk_index + i + 1
4174
- );
4175
- await Promise.all(
4176
- indicesToFetch.map(async (index) => {
4177
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
4178
- return;
4179
- }
4180
- const expandedChunk = await db(chunksTable).where({
4181
- source: chunk.item_id,
4182
- chunk_index: index
4183
- }).first();
4184
- if (expandedChunk) {
4185
- expandedMap.set(`${chunk.item_id}-${index}`, {
4186
- chunk_content: expandedChunk.content,
4187
- chunk_index: expandedChunk.chunk_index,
4188
- chunk_id: expandedChunk.id,
4189
- chunk_source: expandedChunk.source,
4190
- chunk_metadata: expandedChunk.metadata,
4191
- chunk_created_at: expandedChunk.createdAt,
4192
- chunk_updated_at: expandedChunk.updatedAt,
4193
- item_updated_at: chunk.item_updated_at,
4194
- item_created_at: chunk.item_created_at,
4195
- item_id: chunk.item_id,
4196
- item_external_id: chunk.item_external_id,
4197
- item_name: chunk.item_name,
4198
- chunk_cosine_distance: 0,
4199
- chunk_fts_rank: 0,
4200
- chunk_hybrid_score: 0,
4201
- context: {
4202
- name: table.name.singular,
4203
- id: table.id || ""
4204
- }
4205
- });
4206
- }
4207
- })
4208
- );
4209
- }
4210
- }
4211
- results = Array.from(expandedMap.values());
4212
4221
  results = results.sort((a, b) => {
4213
4222
  if (a.item_id !== b.item_id) {
4214
4223
  return a.item_id.localeCompare(b.item_id);
4215
4224
  }
4216
- const aIndex = Number(a.chunk_index);
4217
- const bIndex = Number(b.chunk_index);
4218
- return aIndex - bIndex;
4225
+ return Number(a.chunk_index) - Number(b.chunk_index);
4219
4226
  });
4220
4227
  }
4221
4228
  if (entitiesOn) {
@@ -7741,6 +7748,19 @@ function serializeError(err, depth = 0) {
7741
7748
  return { message: String(err) };
7742
7749
  }
7743
7750
 
7751
+ // src/exulu/turn-metadata.ts
7752
+ function finishTurnMetadata(opts) {
7753
+ const now = opts.now ?? Date.now();
7754
+ return {
7755
+ totalTokens: opts.totalUsage.totalTokens,
7756
+ reasoningTokens: opts.totalUsage.reasoningTokens,
7757
+ inputTokens: opts.totalUsage.inputTokens,
7758
+ outputTokens: opts.totalUsage.outputTokens,
7759
+ cachedInputTokens: opts.totalUsage.cachedInputTokens,
7760
+ durationMs: Math.max(0, now - opts.startedAt)
7761
+ };
7762
+ }
7763
+
7744
7764
  // src/exulu/agent-as-tool.ts
7745
7765
  import { z as z2 } from "zod";
7746
7766
 
@@ -7846,6 +7866,54 @@ async function resolveFreshFileUrl(url, opts) {
7846
7866
  }
7847
7867
  }
7848
7868
 
7869
+ // src/exulu/session-file-listing.ts
7870
+ var DEFAULT_MAX = 25;
7871
+ function formatSize(bytes) {
7872
+ if (bytes < 1024) return `${bytes} B`;
7873
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
7874
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
7875
+ }
7876
+ function formatAge(then, now) {
7877
+ const minutes = Math.max(0, Math.round((now.getTime() - then.getTime()) / 6e4));
7878
+ if (minutes < 1) return "just now";
7879
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
7880
+ const hours = Math.round(minutes / 60);
7881
+ if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
7882
+ const days = Math.round(hours / 24);
7883
+ return `${days} day${days === 1 ? "" : "s"} ago`;
7884
+ }
7885
+ function describeSessionFiles(files, opts = {}) {
7886
+ const now = opts.now ?? /* @__PURE__ */ new Date();
7887
+ const max = opts.max ?? DEFAULT_MAX;
7888
+ const usable = files.filter((f) => f.name && !f.name.endsWith("/") && !isIgnoredArtifactPath(f.name)).sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
7889
+ if (usable.length === 0) return "";
7890
+ const shown = usable.slice(0, max);
7891
+ const lines = shown.map((f) => {
7892
+ const isNew = opts.lastTurnAt ? f.lastModified.getTime() > opts.lastTurnAt.getTime() : false;
7893
+ return `- ${f.name} (${formatSize(f.size)}, ${formatAge(f.lastModified, now)})${isNew ? " [NEW since your last answer]" : ""}`;
7894
+ });
7895
+ const omitted = usable.length - shown.length;
7896
+ if (omitted > 0) lines.push(`\u2026 and ${omitted} more file${omitted === 1 ? "" : "s"} (list them with \`ls\`).`);
7897
+ return "Files currently in this session (newest first):\n" + lines.join("\n") + '\nThese files are available to you. When the user refers to one of these files, to "the document" or "the attachment", or asks something only such a file can answer, read it with parse_document, view_document_page or read_session_file. Otherwise proceed as usual, e.g. with the knowledge bases.';
7898
+ }
7899
+ async function loadSessionFileListing(opts) {
7900
+ const uploads = opts.exuluConfig?.fileUploads;
7901
+ if (!uploads?.s3Bucket) return "";
7902
+ const prefix = sessionFilePrefix(opts.ownerId, opts.sessionID, uploads.s3prefix);
7903
+ try {
7904
+ const objects = await listS3ObjectsByPrefix(prefix, opts.exuluConfig);
7905
+ const files = objects.map((o) => ({
7906
+ name: o.key.slice(o.key.indexOf(prefix) + prefix.length),
7907
+ size: o.size,
7908
+ lastModified: new Date(o.lastModified)
7909
+ }));
7910
+ return describeSessionFiles(files, { lastTurnAt: opts.lastTurnAt });
7911
+ } catch (err) {
7912
+ console.warn(`[EXULU] could not list session files for prompt (session ${opts.sessionID}):`, err);
7913
+ return "";
7914
+ }
7915
+ }
7916
+
7849
7917
  // src/exulu/generate-stream.ts
7850
7918
  import {
7851
7919
  convertToModelMessages,
@@ -8085,11 +8153,12 @@ function resolveTurnStepBudget(maxStepCount, agent) {
8085
8153
  }
8086
8154
  return DEFAULT_MAX_STEPS;
8087
8155
  }
8156
+ var TOOL_INPUT_FLATTEN_CHARS = 6e3;
8088
8157
  function flattenPart(part) {
8089
8158
  const p = part;
8090
8159
  if (p?.type === "text") return p.text ?? "";
8091
8160
  if (p?.type === "tool-call") {
8092
- return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
8161
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, TOOL_INPUT_FLATTEN_CHARS)}`;
8093
8162
  }
8094
8163
  if (p?.type === "tool-result") {
8095
8164
  const out = p.output?.value ?? p.output;
@@ -8111,7 +8180,7 @@ function flattenToolHistory(messages) {
8111
8180
  return m;
8112
8181
  });
8113
8182
  }
8114
- var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
8183
+ var FINAL_ANSWER_INSTRUCTION = `This is your last step for this turn. Answer the user's original question now, in plain text, using only the information gathered above. Do not invent, estimate or "fill in" values that were not actually gathered: report only what the tools returned or what you wrote down, and name explicitly what is missing. If you could not finish the task, tell the user you reached the maximum number of tool steps, summarize what you found and did so far, and say what remains \u2014 they can ask you to continue. Do not attempt any further tool calls. Write your answer as normal prose for the user: do not output tool-call syntax, JSON commands, or bracketed lines such as "[called tool ...]" \u2014 describe anything you did or still plan to do in plain language.`;
8115
8184
  function finalAnswerGuard(maxSteps) {
8116
8185
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
8117
8186
  toolChoice: "none",
@@ -8136,6 +8205,35 @@ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
8136
8205
  };
8137
8206
  }
8138
8207
 
8208
+ // src/exulu/resolve-reasoning-effort.ts
8209
+ var REASONING_EFFORTS = ["none", "disable", "minimal", "low", "medium", "high"];
8210
+ function resolveReasoningEffort(agent) {
8211
+ const raw = agent?.reasoning_effort;
8212
+ if (typeof raw !== "string") return void 0;
8213
+ const normalized = raw.trim().toLowerCase();
8214
+ return REASONING_EFFORTS.includes(normalized) ? normalized : void 0;
8215
+ }
8216
+ function resolveProviderOptions(agent) {
8217
+ const effort = resolveReasoningEffort(agent);
8218
+ return {
8219
+ openai: { reasoningSummary: "auto" },
8220
+ ...effort ? { litellm: { reasoningEffort: effort } } : {}
8221
+ };
8222
+ }
8223
+
8224
+ // src/exulu/stream-error.ts
8225
+ function onChatStreamError({ error }) {
8226
+ const detail = error instanceof Error ? error.message : error === void 0 ? "unknown error" : safeStringify(error);
8227
+ console.error("[EXULU] chat stream error.", detail);
8228
+ }
8229
+ function safeStringify(value) {
8230
+ try {
8231
+ return JSON.stringify(value) ?? String(value);
8232
+ } catch {
8233
+ return String(value);
8234
+ }
8235
+ }
8236
+
8139
8237
  // src/exulu/generate-stream.ts
8140
8238
  var processFilePartsInMessages = async (messages, offloadCtx) => {
8141
8239
  const processedMessages = await Promise.all(
@@ -8229,6 +8327,15 @@ var saveChat = async ({
8229
8327
  await mutation;
8230
8328
  }
8231
8329
  };
8330
+ var lastMessageTime = (rows) => {
8331
+ let latest;
8332
+ for (const row of rows) {
8333
+ if (!row.createdAt) continue;
8334
+ const d = new Date(row.createdAt);
8335
+ if (!latest || d > latest) latest = d;
8336
+ }
8337
+ return latest;
8338
+ };
8232
8339
  var getAgentMessages = async ({
8233
8340
  session,
8234
8341
  user,
@@ -8286,6 +8393,7 @@ var generateSync = async ({
8286
8393
  let project;
8287
8394
  let sessionItems;
8288
8395
  let sessionOwnerId;
8396
+ let lastTurnAt;
8289
8397
  if (session) {
8290
8398
  const sessionData = await getSession({ sessionID: session });
8291
8399
  sessionItems = sessionData.session_items;
@@ -8300,6 +8408,7 @@ var generateSync = async ({
8300
8408
  session,
8301
8409
  user: user.id
8302
8410
  });
8411
+ lastTurnAt = lastMessageTime(previousMessages);
8303
8412
  const previousMessagesContent = previousMessages.map(
8304
8413
  (message) => JSON.parse(message.content)
8305
8414
  );
@@ -8462,6 +8571,15 @@ var generateSync = async ({
8462
8571
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
8463
8572
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
8464
8573
  `;
8574
+ if (session) {
8575
+ const listing = await loadSessionFileListing({
8576
+ sessionID: session,
8577
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
8578
+ exuluConfig,
8579
+ lastTurnAt
8580
+ });
8581
+ if (listing) system += "\n\n" + listing;
8582
+ }
8465
8583
  system += `
8466
8584
 
8467
8585
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -8631,6 +8749,7 @@ var generateStream = async ({
8631
8749
  let project;
8632
8750
  let sessionItems;
8633
8751
  let sessionOwnerId;
8752
+ let lastTurnAt;
8634
8753
  if (session) {
8635
8754
  const sessionData = await getSession({ sessionID: session });
8636
8755
  project = sessionData.project;
@@ -8646,6 +8765,7 @@ var generateStream = async ({
8646
8765
  includeAllUsers: isRunSessionMetadata(sessionData.metadata)
8647
8766
  });
8648
8767
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
8768
+ lastTurnAt = lastMessageTime(previousMessages2);
8649
8769
  }
8650
8770
  const model = languageModel;
8651
8771
  messages = await validateUIMessages({
@@ -8788,6 +8908,15 @@ ${skillsList}
8788
8908
  truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
8789
8909
  to page through it \u2014 do not ask the user to re-upload.
8790
8910
  `;
8911
+ if (session) {
8912
+ const listing = await loadSessionFileListing({
8913
+ sessionID: session,
8914
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
8915
+ exuluConfig,
8916
+ lastTurnAt
8917
+ });
8918
+ if (listing) system += "\n\n" + listing;
8919
+ }
8791
8920
  system += `
8792
8921
 
8793
8922
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -8890,18 +9019,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
8890
9019
  // for the first step or change other parameters.
8891
9020
  system,
8892
9021
  maxRetries: 2,
8893
- providerOptions: {
8894
- openai: {
8895
- reasoningSummary: "auto"
8896
- }
8897
- },
9022
+ // OpenAI reasoning summaries + the agent's optional thinking budget
9023
+ // (agents.reasoning_effort → LiteLLM reasoning_effort).
9024
+ providerOptions: resolveProviderOptions(agent),
8898
9025
  tools,
8899
- onError: (error) => {
8900
- console.error("[EXULU] chat stream error.", error);
8901
- throw new Error(
8902
- `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
8903
- );
8904
- },
9026
+ // Log only — throwing here crashed the process (see stream-error.ts).
9027
+ onError: onChatStreamError,
8905
9028
  // todo allow configuring the step budget per skill
8906
9029
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
8907
9030
  stopWhen: [stepCountIs(turnBudget), hasToolCall("image_generation")]
@@ -8913,6 +9036,44 @@ When a tool execution is not approved by the user, do not retry it unless explic
8913
9036
  };
8914
9037
  };
8915
9038
 
9039
+ // src/utils/agent-glossary.ts
9040
+ function extractGlossaryFromAgentTools(tools) {
9041
+ if (!Array.isArray(tools)) return [];
9042
+ const searchTool = tools.find((t) => t?.id === "agentic_context_search");
9043
+ const entry = searchTool?.config?.find((c) => c?.name === "vocabulary");
9044
+ if (entry === void 0 || entry.variable === void 0 || entry.variable === null || entry.variable === "") {
9045
+ return [];
9046
+ }
9047
+ try {
9048
+ const parsed = typeof entry.variable === "string" ? JSON.parse(entry.variable) : entry.variable;
9049
+ const glossary = parsed?.glossary;
9050
+ if (!Array.isArray(glossary)) return [];
9051
+ return glossary.filter(
9052
+ (g) => !!g && typeof g.term === "string" && typeof g.meaning === "string" && g.term.trim().length > 0
9053
+ );
9054
+ } catch (err) {
9055
+ console.warn(
9056
+ "[EXULU] Failed to parse the agentic_context_search vocabulary config while building agent instructions.",
9057
+ err
9058
+ );
9059
+ return [];
9060
+ }
9061
+ }
9062
+ function formatGlossaryBlock(glossary) {
9063
+ if (!glossary.length) return "";
9064
+ const lines = glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n");
9065
+ return `The organization's documents and internal terminology use the following abbreviations/terms:
9066
+ ${lines}`;
9067
+ }
9068
+ function withGlossary(baseInstructions, tools) {
9069
+ const instructions = baseInstructions ?? "";
9070
+ const block = formatGlossaryBlock(extractGlossaryFromAgentTools(tools));
9071
+ if (!block) return instructions;
9072
+ return instructions ? `${block}
9073
+
9074
+ ${instructions}` : block;
9075
+ }
9076
+
8916
9077
  // src/exulu/agent-as-tool.ts
8917
9078
  var createAgentTool = async (instance, contexts) => {
8918
9079
  const agent = await exuluApp.get().agent(instance);
@@ -8971,7 +9132,7 @@ var createAgentTool = async (instance, contexts) => {
8971
9132
  const response = await generateSync({
8972
9133
  agent,
8973
9134
  contexts,
8974
- instructions: agent.instructions,
9135
+ instructions: withGlossary(agent.instructions, agent.tools),
8975
9136
  prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
8976
9137
  languageModel: resolved.languageModel,
8977
9138
  user,
@@ -10744,13 +10905,7 @@ var processUiMessagesFlow = async ({
10744
10905
  messageMetadata: ({ part }) => {
10745
10906
  console.log("[EXULU] part", part.type);
10746
10907
  if (part.type === "finish") {
10747
- return {
10748
- totalTokens: part.totalUsage.totalTokens,
10749
- reasoningTokens: part.totalUsage.reasoningTokens,
10750
- inputTokens: part.totalUsage.inputTokens,
10751
- outputTokens: part.totalUsage.outputTokens,
10752
- cachedInputTokens: part.totalUsage.cachedInputTokens
10753
- };
10908
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
10754
10909
  }
10755
10910
  return void 0;
10756
10911
  },
@@ -11964,7 +12119,7 @@ var recallService = {
11964
12119
  );
11965
12120
  const { text } = await generateText4({
11966
12121
  model: resolved.languageModel,
11967
- system: agent.instructions || void 0,
12122
+ system: withGlossary(agent.instructions, agent.tools) || void 0,
11968
12123
  prompt: `${prompt.content}
11969
12124
 
11970
12125
  ---
@@ -16681,10 +16836,14 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
16681
16836
  }
16682
16837
  const approvedTools = req.body.approvedTools ? typeof req.body.approvedTools === "string" ? JSON.parse(req.body.approvedTools) : req.body.approvedTools : [];
16683
16838
  const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
16684
- const instructions = customInstructions ? `${agent.instructions}
16839
+ const instructions = withGlossary(
16840
+ customInstructions ? `${agent.instructions}
16685
16841
 
16686
- ${customInstructions}` : agent.instructions;
16842
+ ${customInstructions}` : agent.instructions,
16843
+ agent.tools
16844
+ );
16687
16845
  if (headers.session) markStreamActive(headers.session);
16846
+ const turnStartedAt = Date.now();
16688
16847
  let result;
16689
16848
  try {
16690
16849
  result = await generateStream({
@@ -16726,13 +16885,7 @@ ${customInstructions}` : agent.instructions;
16726
16885
  };
16727
16886
  }
16728
16887
  if (part.type === "finish") {
16729
- return {
16730
- totalTokens: part.totalUsage.totalTokens,
16731
- reasoningTokens: part.totalUsage.reasoningTokens,
16732
- inputTokens: part.totalUsage.inputTokens,
16733
- outputTokens: part.totalUsage.outputTokens,
16734
- cachedInputTokens: part.totalUsage.cachedInputTokens
16735
- };
16888
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
16736
16889
  }
16737
16890
  return void 0;
16738
16891
  },
@@ -16824,9 +16977,12 @@ ${customInstructions}` : agent.instructions;
16824
16977
  return;
16825
16978
  } else {
16826
16979
  const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
16827
- const instructions = customInstructions ? `${agent.instructions}
16980
+ const instructions = withGlossary(
16981
+ customInstructions ? `${agent.instructions}
16828
16982
 
16829
- ${customInstructions}` : agent.instructions;
16983
+ ${customInstructions}` : agent.instructions,
16984
+ agent.tools
16985
+ );
16830
16986
  let response;
16831
16987
  try {
16832
16988
  response = await generateSync({
@@ -24151,7 +24307,7 @@ function reconstructTableHeaders(document, validationResults, verbose = false) {
24151
24307
  }
24152
24308
  }
24153
24309
  async function validateWithVLM(document, model, verbose = false, concurrency = 10) {
24154
- console.log(`[EXULU] Starting VLM validation for docling output, ${document.length} pages...`);
24310
+ console.log(`[EXULU] Starting VLM validation for processor output, ${document.length} pages...`);
24155
24311
  console.log(`[EXULU] Concurrency limit: ${concurrency}`);
24156
24312
  const limit = pLimit(concurrency);
24157
24313
  const validationResults = /* @__PURE__ */ new Map();
@@ -24286,48 +24442,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
24286
24442
  async function processPdf(buffer, paths, config, verbose = false) {
24287
24443
  try {
24288
24444
  let json = [];
24289
- if (config?.processor.name === "docling") {
24290
- console.log(`[EXULU] Validating Python environment...`);
24291
- const validation = await validatePythonEnvironment(void 0, true);
24292
- if (!validation.valid) {
24293
- console.log(`[EXULU] Python environment not ready, setting up automatically...`);
24294
- console.log(`[EXULU] Reason: ${validation.message}`);
24295
- const setupResult = await setupPythonEnvironment({
24296
- verbose: true,
24297
- force: false
24298
- // Only setup if not already done
24299
- });
24300
- if (!setupResult.success) {
24301
- throw new Error(`Failed to setup Python environment: ${setupResult.message}
24302
-
24303
- ${setupResult.output || ""}`);
24304
- }
24305
- console.log(`[EXULU] Python environment setup completed successfully`);
24306
- } else {
24307
- console.log(`[EXULU] Python environment is valid`);
24308
- }
24309
- console.log(`[EXULU] Processing document with document_to_markdown.py`);
24310
- const result = await executePythonScript({
24311
- scriptPath: "ee/python/documents/processing/document_to_markdown.py",
24312
- args: [
24313
- paths.source,
24314
- "-o",
24315
- paths.json,
24316
- "--images-dir",
24317
- paths.images
24318
- ],
24319
- timeout: 30 * 60 * 1e3
24320
- // 30 minutes for large documents
24321
- });
24322
- if (result.stderr) {
24323
- console.log("Processing info:", result.stderr.trim());
24324
- }
24325
- if (!result.success) {
24326
- throw new Error(`Document processing failed: ${result.stderr}`);
24327
- }
24328
- const jsonContent = await fs3.promises.readFile(paths.json, "utf-8");
24329
- json = JSON.parse(jsonContent);
24330
- } else if (config?.processor.name === "officeparser") {
24445
+ if (config?.processor.name === "officeparser") {
24331
24446
  const text = await parseOfficeAsync2(buffer, {
24332
24447
  outputErrorToConsole: false,
24333
24448
  newlineDelimiter: "\n"
@@ -24418,14 +24533,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
24418
24533
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
24419
24534
  }));
24420
24535
  fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
24536
+ } else {
24537
+ const configured = String(config?.processor?.name ?? "");
24538
+ throw new Error(
24539
+ configured === "" ? "[EXULU] No document processor configured. Set processor.name to one of: mistral, liteparse, officeparser." : `[EXULU] Unknown document processor "${configured}". Supported processors are: mistral, liteparse, officeparser.` + (configured === "docling" ? ' The "docling" processor was removed: it depended on PyMuPDF, which is AGPL-licensed. Use "mistral" for PDF OCR.' : "")
24540
+ );
24421
24541
  }
24422
24542
  console.log(`[EXULU]
24423
24543
  \u2713 Document processing completed successfully`);
24424
24544
  console.log(`[EXULU] Total pages: ${json.length}`);
24425
24545
  console.log(`[EXULU] Output file: ${paths.json}`);
24426
- if (config?.vlm?.model) {
24427
- console.error("[EXULU] VLM validation is only supported when docling is enabled, skipping validation.");
24428
- }
24429
24546
  const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
24430
24547
  if (vlmModel && json.length > 0) {
24431
24548
  json = await validateWithVLM(
@@ -24543,9 +24660,6 @@ async function documentProcessor({
24543
24660
  } = await loadFile(file, name, tempDir);
24544
24661
  let supportedTypes = [];
24545
24662
  switch (config?.processor.name) {
24546
- case "docling":
24547
- supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
24548
- break;
24549
24663
  case "officeparser":
24550
24664
  supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
24551
24665
  break;