@exulu/backend 3.7.4 → 4.0.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 +566 -217
  6. package/dist/index.d.cts +3 -1
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.js +250 -180
  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
- }
4168
- }
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
- }
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 || "" });
4210
4220
  }
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")]
@@ -10744,13 +10867,7 @@ var processUiMessagesFlow = async ({
10744
10867
  messageMetadata: ({ part }) => {
10745
10868
  console.log("[EXULU] part", part.type);
10746
10869
  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
- };
10870
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
10754
10871
  }
10755
10872
  return void 0;
10756
10873
  },
@@ -16685,6 +16802,7 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
16685
16802
 
16686
16803
  ${customInstructions}` : agent.instructions;
16687
16804
  if (headers.session) markStreamActive(headers.session);
16805
+ const turnStartedAt = Date.now();
16688
16806
  let result;
16689
16807
  try {
16690
16808
  result = await generateStream({
@@ -16726,13 +16844,7 @@ ${customInstructions}` : agent.instructions;
16726
16844
  };
16727
16845
  }
16728
16846
  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
- };
16847
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
16736
16848
  }
16737
16849
  return void 0;
16738
16850
  },
@@ -24151,7 +24263,7 @@ function reconstructTableHeaders(document, validationResults, verbose = false) {
24151
24263
  }
24152
24264
  }
24153
24265
  async function validateWithVLM(document, model, verbose = false, concurrency = 10) {
24154
- console.log(`[EXULU] Starting VLM validation for docling output, ${document.length} pages...`);
24266
+ console.log(`[EXULU] Starting VLM validation for processor output, ${document.length} pages...`);
24155
24267
  console.log(`[EXULU] Concurrency limit: ${concurrency}`);
24156
24268
  const limit = pLimit(concurrency);
24157
24269
  const validationResults = /* @__PURE__ */ new Map();
@@ -24286,48 +24398,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
24286
24398
  async function processPdf(buffer, paths, config, verbose = false) {
24287
24399
  try {
24288
24400
  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") {
24401
+ if (config?.processor.name === "officeparser") {
24331
24402
  const text = await parseOfficeAsync2(buffer, {
24332
24403
  outputErrorToConsole: false,
24333
24404
  newlineDelimiter: "\n"
@@ -24418,14 +24489,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
24418
24489
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
24419
24490
  }));
24420
24491
  fs3.writeFileSync(paths.json, JSON.stringify(json, null, 2));
24492
+ } else {
24493
+ const configured = String(config?.processor?.name ?? "");
24494
+ throw new Error(
24495
+ 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.' : "")
24496
+ );
24421
24497
  }
24422
24498
  console.log(`[EXULU]
24423
24499
  \u2713 Document processing completed successfully`);
24424
24500
  console.log(`[EXULU] Total pages: ${json.length}`);
24425
24501
  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
24502
  const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
24430
24503
  if (vlmModel && json.length > 0) {
24431
24504
  json = await validateWithVLM(
@@ -24543,9 +24616,6 @@ async function documentProcessor({
24543
24616
  } = await loadFile(file, name, tempDir);
24544
24617
  let supportedTypes = [];
24545
24618
  switch (config?.processor.name) {
24546
- case "docling":
24547
- supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
24548
- break;
24549
24619
  case "officeparser":
24550
24620
  supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
24551
24621
  break;