@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.cjs CHANGED
@@ -4064,7 +4064,7 @@ async function validatePythonEnvironment(packageRoot, checkPackages = true) {
4064
4064
  };
4065
4065
  }
4066
4066
  if (checkPackages) {
4067
- const criticalPackages = ["docling", "transformers"];
4067
+ const criticalPackages = ["pypdf", "transformers"];
4068
4068
  const missingPackages = [];
4069
4069
  for (const pkg of criticalPackages) {
4070
4070
  try {
@@ -7151,7 +7151,19 @@ var init_config2 = __esm({
7151
7151
  pinBoost: import_zod11.z.number().min(0).max(1).default(0.15),
7152
7152
  identifierBoost: import_zod11.z.number().min(0).max(1).default(0.15),
7153
7153
  pageWindow: import_zod11.z.number().int().min(0).default(1),
7154
- maxQueriesPerContext: import_zod11.z.number().int().positive().default(5)
7154
+ maxQueriesPerContext: import_zod11.z.number().int().positive().default(5),
7155
+ /**
7156
+ * Orchestration engine. "v1" is the sequential flow every agent ran before 2026-09;
7157
+ * "v2" merges the phase-1 LLM hops and runs identifier pins in parallel. Per agent,
7158
+ * so a candidate agent can run v2 while the production agent stays on v1.
7159
+ */
7160
+ engine: import_zod11.z.enum(["v1", "v2"]).default("v1"),
7161
+ /** v2 sub-features; each can be switched off on its own to bisect a regression. */
7162
+ v2: import_zod11.z.object({
7163
+ mergedMemoryCall: import_zod11.z.boolean().default(true),
7164
+ mergedRoutingCall: import_zod11.z.boolean().default(true),
7165
+ parallelPins: import_zod11.z.boolean().default(true)
7166
+ }).default({ mergedMemoryCall: true, mergedRoutingCall: true, parallelPins: true })
7155
7167
  });
7156
7168
  boolVal = (v) => v === true || v === "true" || v === 1;
7157
7169
  strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
@@ -7246,6 +7258,23 @@ var init_micro_call = __esm({
7246
7258
  }
7247
7259
  });
7248
7260
 
7261
+ // ee/agentic-retrieval/pipeline/timing.ts
7262
+ async function withTiming(sink, key, work, now = Date.now) {
7263
+ if (!sink) return work();
7264
+ const started = now();
7265
+ try {
7266
+ return await work();
7267
+ } finally {
7268
+ sink[key] = Math.max(0, now() - started);
7269
+ }
7270
+ }
7271
+ var init_timing = __esm({
7272
+ "ee/agentic-retrieval/pipeline/timing.ts"() {
7273
+ "use strict";
7274
+ init_cjs_shims();
7275
+ }
7276
+ });
7277
+
7249
7278
  // ee/agentic-retrieval/pipeline/text-utils.ts
7250
7279
  function extractIdentifierTokens(parts) {
7251
7280
  const tokens = /* @__PURE__ */ new Set();
@@ -7553,6 +7582,49 @@ If the question references none, return hasMatches false and an empty array.`;
7553
7582
  });
7554
7583
 
7555
7584
  // ee/agentic-retrieval/pipeline/routing.ts
7585
+ async function runMergedRoutingCall({
7586
+ model,
7587
+ question,
7588
+ knownIdentifiers,
7589
+ enabledContexts,
7590
+ routingRules,
7591
+ extraInstructions
7592
+ }) {
7593
+ const kbListing = enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n");
7594
+ const rulesLines = routingRules.map((r) => `- ${r.id} (${r.label}): ${r.description}`).join("\n");
7595
+ const system = `You analyse the user's request and answer ${routingRules.length ? "three" : "two"} questions in one go.
7596
+
7597
+ A. DOCUMENT / PAGE REFERENCE (docPage):
7598
+ ${buildDocPagePrompt(knownIdentifiers)}
7599
+
7600
+ B. EXPLICIT KNOWLEDGE BASE REQUEST (explicitlyRequestedKnowledgeBases): check if the user has EXPLICITLY asked you to search in one or multiple of the following knowledge bases:
7601
+ ${kbListing}
7602
+ EXPLICIT means the user names a knowledge base or clearly commands searching a specific source (e.g. "search in the tickets", "look this up in the manuals KB"). A question that merely CONCERNS a topic related to a knowledge base's name or contents (e.g. asking about software changes, norms, or a product) is NOT an explicit request. When in doubt, return an empty array. If explicit, return the knowledge base ids.
7603
+ ` + (routingRules.length ? `
7604
+ C. CLASSIFICATION (classification): classify the request into exactly one of these categories:
7605
+ ${rulesLines}` + (extraInstructions ? `
7606
+ <instructions>
7607
+ ${extraInstructions}
7608
+ </instructions>` : "") : "");
7609
+ const ids = enabledContexts.map((c) => c.id);
7610
+ const schema = import_zod13.z.object({
7611
+ docPage: import_zod13.z.object({
7612
+ hasFilenameHint: import_zod13.z.boolean(),
7613
+ filenameHints: import_zod13.z.array(import_zod13.z.string()).optional(),
7614
+ hasPageHint: import_zod13.z.boolean(),
7615
+ pageNumber: import_zod13.z.number().int().nullable().optional()
7616
+ }),
7617
+ explicitlyRequestedKnowledgeBases: import_zod13.z.array(import_zod13.z.enum(ids)),
7618
+ ...routingRules.length ? {
7619
+ classification: import_zod13.z.object({
7620
+ ruleId: import_zod13.z.enum(routingRules.map((r) => r.id)),
7621
+ reason: import_zod13.z.string()
7622
+ })
7623
+ } : {}
7624
+ });
7625
+ const { output } = await microCall({ model, system, messages: [{ role: "user", content: question }], schema });
7626
+ return output;
7627
+ }
7556
7628
  async function runRoutingPhase(opts) {
7557
7629
  const {
7558
7630
  question,
@@ -7562,6 +7634,7 @@ async function runRoutingPhase(opts) {
7562
7634
  preselectedItems,
7563
7635
  extraInstructions,
7564
7636
  knownIdentifiers = [],
7637
+ mergedCall = false,
7565
7638
  model
7566
7639
  } = opts;
7567
7640
  try {
@@ -7581,7 +7654,23 @@ async function runRoutingPhase(opts) {
7581
7654
  ` + enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n") + `
7582
7655
  EXPLICIT means the user names a knowledge base or clearly commands searching a specific source (e.g. "search in the tickets", "look this up in the manuals KB"). A question that merely CONCERNS a topic related to a knowledge base's name or contents (e.g. asking about software changes, norms, or a product) is NOT an explicit request \u2014 classification routing handles those. When in doubt, return an empty array.
7583
7656
  If explicit, return the knowledge base ids. If not, return an empty array.`;
7584
- const [docPageRaw, explicitKBRaw] = await Promise.all([
7657
+ const wantsClassification = routingRules.length > 0 && preselectedItems.size === 0;
7658
+ const merged = mergedCall ? await withTiming(
7659
+ opts.timings,
7660
+ "routing.mergedMs",
7661
+ () => runMergedRoutingCall({
7662
+ model,
7663
+ question,
7664
+ knownIdentifiers,
7665
+ enabledContexts,
7666
+ routingRules: wantsClassification ? routingRules : [],
7667
+ extraInstructions
7668
+ }).catch((err) => {
7669
+ console.warn("[EXULU pipeline] merged routing call failed \u2014 falling back to the v1 hops.", err);
7670
+ return null;
7671
+ })
7672
+ ) : null;
7673
+ const [docPageRaw, explicitKBRaw] = merged ? [{ output: merged.docPage }, { output: { explicitlyRequestedKnowledgeBases: merged.explicitlyRequestedKnowledgeBases } }] : await withTiming(opts.timings, "routing.detectMs", () => Promise.all([
7585
7674
  (async () => {
7586
7675
  try {
7587
7676
  return await microCall({
@@ -7623,7 +7712,7 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
7623
7712
  return { output: { explicitlyRequestedKnowledgeBases: [] } };
7624
7713
  }
7625
7714
  })()
7626
- ]);
7715
+ ]));
7627
7716
  const userPinnedItemIdsByContext = /* @__PURE__ */ new Map();
7628
7717
  let userRequestedPage = null;
7629
7718
  if (docPageRaw.output.hasFilenameHint && docPageRaw.output.filenameHints?.length) {
@@ -7701,7 +7790,7 @@ ${extraInstructions}
7701
7790
  </instructions>`;
7702
7791
  }
7703
7792
  try {
7704
- const { output: classified } = await microCall({
7793
+ const { output: classified } = merged?.classification ? { output: merged.classification } : await withTiming(opts.timings, "routing.classifyMs", () => microCall({
7705
7794
  model,
7706
7795
  system: classifyPrompt,
7707
7796
  messages: [{ role: "user", content: question }],
@@ -7709,7 +7798,7 @@ ${extraInstructions}
7709
7798
  ruleId: import_zod13.z.enum(ruleIds),
7710
7799
  reason: import_zod13.z.string()
7711
7800
  })
7712
- });
7801
+ }));
7713
7802
  const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
7714
7803
  if (matchedRule) {
7715
7804
  const main = matchedRule.main.filter((id) => enabledIds.has(id));
@@ -7769,6 +7858,7 @@ var init_routing = __esm({
7769
7858
  init_cjs_shims();
7770
7859
  import_zod13 = require("zod");
7771
7860
  init_micro_call();
7861
+ init_timing();
7772
7862
  init_prefilter();
7773
7863
  init_text_utils();
7774
7864
  MAX_USER_PIN_MATCHES = 8;
@@ -7910,7 +8000,8 @@ async function recallMemoryByKeywords({
7910
8000
  importantKeyword,
7911
8001
  user,
7912
8002
  role,
7913
- memoryContext
8003
+ memoryContext,
8004
+ timings
7914
8005
  }) {
7915
8006
  const allKeywords = [
7916
8007
  ...new Set(
@@ -7927,7 +8018,7 @@ async function recallMemoryByKeywords({
7927
8018
  ...new Set(allKeywords.flatMap(deriveKeywordVariants).map(stripSeparators))
7928
8019
  ].filter((v) => v.length >= 4);
7929
8020
  if (!allVariants.length) return [];
7930
- const items = await loadMemoryItems(memoryContext);
8021
+ const items = await withTiming(timings, "memory.keywordRecall.itemsMs", () => loadMemoryItems(memoryContext));
7931
8022
  const scored = [];
7932
8023
  for (const item of items) {
7933
8024
  const haystack = stripSeparators(
@@ -7949,14 +8040,14 @@ async function recallMemoryByKeywords({
7949
8040
  "[EXULU pipeline] keyword-triggered memory matches:",
7950
8041
  topMatches.map((s) => `${s.name} (hits=${s.hits}, important=${s.importantHit})`)
7951
8042
  );
7952
- const chunks = await singleSearch({
8043
+ const chunks = await withTiming(timings, "memory.keywordRecall.searchMs", () => singleSearch({
7953
8044
  query: allKeywords.join(", "),
7954
- config: { method: "hybridSearch", cutoffs: void 0, limit: 50 },
8045
+ config: { method: "tsvector", cutoffs: void 0, limit: 50 },
7955
8046
  user,
7956
8047
  role,
7957
8048
  pinnedItemIds: topMatches.map((s) => s.id),
7958
8049
  context: memoryContext
7959
- });
8050
+ }));
7960
8051
  return chunks;
7961
8052
  }
7962
8053
  function neutralResult(question, keywords, importantKeyword, steps = []) {
@@ -7970,7 +8061,103 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
7970
8061
  steps
7971
8062
  };
7972
8063
  }
8064
+ async function runMergedMemoryCall({
8065
+ model,
8066
+ retrievedMemory,
8067
+ question,
8068
+ keywords,
8069
+ importantKeyword,
8070
+ memoryConfig,
8071
+ glossary
8072
+ }) {
8073
+ const glossaryText = glossary.length > 0 ? `
8074
+ The organization's documents use the following abbreviations/terms:
8075
+ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
8076
+ const system = `
8077
+ You review the shared company memory for the user's question and answer FOUR questions in one go.
8078
+
8079
+ 1. RELEVANCE (relevantChunkIds): return the chunk_ids of chunks containing information relevant to the
8080
+ question, or an empty array. Be generous: include chunks that are topically related, share key
8081
+ terminology, describe the same symptom from a different angle, or could plausibly help diagnose the
8082
+ issue \u2014 even if they don't answer the question directly. Memory entries are deliberately broad,
8083
+ hand-curated hints written by domain experts; the user's wording will rarely match the memory verbatim.
8084
+ When in doubt, include the chunk.
8085
+
8086
+ 2. OVERRIDE (override): decide whether ONE of the relevant chunks should become the AUTHORITATIVE basis
8087
+ of the answer, taking precedence over the official documentation even if the documents state something
8088
+ different. This is a deliberately STRICT check. Set overrides=true ONLY if a single chunk, on its own,
8089
+ contains a DIRECT and SUFFICIENT answer to exactly what the user asked. Being topically related, sharing
8090
+ terminology, describing the same component, or only partially addressing the question is NOT sufficient:
8091
+ then set overrides=false. When in doubt, set overrides=false. Memory entries may capture field experience
8092
+ that the manuals get wrong, so a confident, direct match is meant to win over the documents.
8093
+ ${memoryConfig.override ? "" : "(Override is disabled for this agent: return overrides=false.)"}
8094
+
8095
+ 3. FILE PRIORITIZATION (filePrioritization): set shouldPrioritizeFiles=true only if a relevant memory
8096
+ entry explicitly says to look in, prioritize, prefer, or always search a particular document, file, or
8097
+ file family (for example "When asked about X, always search in Y-Dateien first"). General background
8098
+ facts, glossaries, or synonyms are NOT a file prioritization instruction. When true, return
8099
+ fileNameHints exactly as referenced in the memory, bare names without folder paths.
8100
+ ${memoryConfig.filePrioritization ? "" : "(File prioritization is disabled for this agent: return false.)"}
8101
+
8102
+ 4. QUERY AUGMENTATION (augmentation): if, and only if, the relevant memory (or the glossary below)
8103
+ contains synonyms or similar terms for what the user asked, return the user question and keywords
8104
+ updated to include those synonyms \u2014 always keeping the original wording as well. Otherwise return the
8105
+ original question, keywords and important keyword unchanged.
8106
+ ${memoryConfig.queryAugmentation ? "" : "(Query augmentation is disabled for this agent: return the originals.)"}
8107
+
8108
+ <memory_chunks>
8109
+ ${retrievedMemory.map((chunk) => `- ${chunk.chunk_id}: ${chunk.item_name} - ${chunk.chunk_content}`).join("\n")}
8110
+ </memory_chunks>
8111
+ ${glossaryText}
8112
+ `;
8113
+ const { output } = await microCall({
8114
+ model,
8115
+ system,
8116
+ messages: [
8117
+ {
8118
+ role: "user",
8119
+ content: `
8120
+ <user_question>${question}</user_question>
8121
+ <relevant_keywords>${keywords.join(", ")}</relevant_keywords>
8122
+ <important_keyword>${importantKeyword}</important_keyword>
8123
+ `
8124
+ }
8125
+ ],
8126
+ schema: import_zod14.z.object({
8127
+ relevantChunkIds: import_zod14.z.array(import_zod14.z.string()).describe("chunk_ids (UUIDs at the start of each bullet) of relevant chunks; empty array if none."),
8128
+ override: import_zod14.z.object({
8129
+ overrides: import_zod14.z.boolean().describe("True ONLY if a chunk directly and sufficiently answers the question."),
8130
+ confidence: import_zod14.z.enum(["high", "medium", "low"]),
8131
+ authoritativeChunkIds: import_zod14.z.array(import_zod14.z.string()).describe("chunk_ids that directly answer the question; empty if overrides is false."),
8132
+ reason: import_zod14.z.string().describe("One short sentence.")
8133
+ }),
8134
+ filePrioritization: import_zod14.z.object({
8135
+ shouldPrioritizeFiles: import_zod14.z.boolean(),
8136
+ fileNameHints: import_zod14.z.array(import_zod14.z.string()).optional()
8137
+ }),
8138
+ augmentation: import_zod14.z.object({
8139
+ updatedUserQuestion: import_zod14.z.string(),
8140
+ updatedRelevantKeywords: import_zod14.z.array(import_zod14.z.string()),
8141
+ updatedImportantKeyword: import_zod14.z.string()
8142
+ })
8143
+ })
8144
+ });
8145
+ return output;
8146
+ }
8147
+ function mergedFollowups(merged, memoryConfig, hasAugmentationContent, question, importantKeyword) {
8148
+ const overrideResult = {
8149
+ output: memoryConfig.override ? merged.override : { overrides: false, confidence: "low", authoritativeChunkIds: [], reason: "" }
8150
+ };
8151
+ const fileResult = {
8152
+ output: memoryConfig.filePrioritization ? merged.filePrioritization : { shouldPrioritizeFiles: false, fileNameHints: [] }
8153
+ };
8154
+ const queryResult = {
8155
+ output: memoryConfig.queryAugmentation && hasAugmentationContent ? merged.augmentation : { updatedUserQuestion: question, updatedRelevantKeywords: [], updatedImportantKeyword: importantKeyword }
8156
+ };
8157
+ return [overrideResult, fileResult, queryResult];
8158
+ }
7973
8159
  async function runMemoryPhase({
8160
+ timings,
7974
8161
  memoryChunks,
7975
8162
  memoryContext,
7976
8163
  question,
@@ -7981,7 +8168,8 @@ async function runMemoryPhase({
7981
8168
  model,
7982
8169
  memoryConfig,
7983
8170
  glossary,
7984
- documentContexts
8171
+ documentContexts,
8172
+ mergedCall = false
7985
8173
  }) {
7986
8174
  try {
7987
8175
  if (!memoryConfig.enabled || memoryChunks.length === 0 && !memoryContext) {
@@ -7991,13 +8179,14 @@ async function runMemoryPhase({
7991
8179
  let retrieved_memory = [...memoryChunks];
7992
8180
  if (memoryContext) {
7993
8181
  try {
7994
- const keywordMatched = await recallMemoryByKeywords({
8182
+ const keywordMatched = await withTiming(timings, "memory.keywordRecallMs", () => recallMemoryByKeywords({
7995
8183
  keywords,
7996
8184
  importantKeyword,
7997
8185
  user,
7998
8186
  role,
7999
- memoryContext
8000
- });
8187
+ memoryContext,
8188
+ timings
8189
+ }));
8001
8190
  if (keywordMatched.length > 0) {
8002
8191
  const seen = new Set(retrieved_memory.map((c) => c.chunk_id));
8003
8192
  const additions = keywordMatched.filter((c) => !seen.has(c.chunk_id));
@@ -8022,8 +8211,24 @@ async function runMemoryPhase({
8022
8211
  </memory_chunks>
8023
8212
  `;
8024
8213
  let relevantMemoryChunks = [];
8214
+ let mergedOutput;
8025
8215
  try {
8026
- const { output: output_relevant_memory } = await microCall({
8216
+ if (mergedCall) {
8217
+ mergedOutput = await withTiming(
8218
+ timings,
8219
+ "memory.mergedMs",
8220
+ () => runMergedMemoryCall({
8221
+ model,
8222
+ retrievedMemory: retrieved_memory,
8223
+ question,
8224
+ keywords,
8225
+ importantKeyword,
8226
+ memoryConfig,
8227
+ glossary
8228
+ })
8229
+ );
8230
+ }
8231
+ const { output: output_relevant_memory } = mergedOutput ? { output: { relevantChunkIds: mergedOutput.relevantChunkIds } } : await withTiming(timings, "memory.relevanceMs", () => microCall({
8027
8232
  model,
8028
8233
  system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
8029
8234
  messages: [
@@ -8041,7 +8246,7 @@ async function runMemoryPhase({
8041
8246
  "The chunk_ids (UUIDs at the start of each bullet) of chunks containing information relevant to the user's question. Empty array if none are relevant."
8042
8247
  )
8043
8248
  })
8044
- });
8249
+ }));
8045
8250
  const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
8046
8251
  relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
8047
8252
  } catch (e) {
@@ -8131,7 +8336,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
8131
8336
 
8132
8337
  Otherwise, return the original user question, relevant keywords and important keyword.
8133
8338
  `;
8134
- const [overrideResult, fileResult, queryResult] = await Promise.all([
8339
+ const [overrideResult, fileResult, queryResult] = mergedOutput ? mergedFollowups(mergedOutput, memoryConfig, hasAugmentationContent, question, importantKeyword) : await withTiming(timings, "memory.followupsMs", () => Promise.all([
8135
8340
  // Override check: strict gate to decide if memory should be authoritative
8136
8341
  memoryConfig.override ? microCall({
8137
8342
  model,
@@ -8212,7 +8417,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
8212
8417
  updatedImportantKeyword: importantKeyword
8213
8418
  }
8214
8419
  })
8215
- ]);
8420
+ ]));
8216
8421
  const overrideIds = new Set(overrideResult.output?.authoritativeChunkIds ?? []);
8217
8422
  const authoritativeChunks = memoryChunksForAnswer.filter(
8218
8423
  (c) => c.chunk_id && overrideIds.has(c.chunk_id)
@@ -8291,6 +8496,7 @@ var init_memory = __esm({
8291
8496
  init_cjs_shims();
8292
8497
  import_zod14 = require("zod");
8293
8498
  init_micro_call();
8499
+ init_timing();
8294
8500
  init_multi_query();
8295
8501
  init_prefilter();
8296
8502
  init_text_utils();
@@ -8412,7 +8618,7 @@ async function searchContexts(opts) {
8412
8618
  skipPrefilter
8413
8619
  } = opts;
8414
8620
  const chunkArrays = await Promise.all(
8415
- contextIds.map(async (ctxId) => {
8621
+ contextIds.map((ctxId) => withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}Ms`, async () => {
8416
8622
  try {
8417
8623
  const ctx = contextsById.get(ctxId);
8418
8624
  if (!ctx) return [];
@@ -8450,13 +8656,13 @@ async function searchContexts(opts) {
8450
8656
  if (multiQuery) {
8451
8657
  let hydePassage = null;
8452
8658
  if (hyde) {
8453
- hydePassage = await generateHydePassage({
8659
+ hydePassage = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.hydeMs`, () => generateHydePassage({
8454
8660
  originalQuestion: question,
8455
8661
  relevantKeywords: keywords,
8456
8662
  importantKeyword,
8457
8663
  styleHint,
8458
8664
  model
8459
- });
8665
+ }));
8460
8666
  }
8461
8667
  const candidates = [
8462
8668
  question,
@@ -8471,14 +8677,14 @@ async function searchContexts(opts) {
8471
8677
  }
8472
8678
  if (kind === "conversations") {
8473
8679
  if (keywordPrefilter && pinnedItemIds.length === 0) {
8474
- const prefiltered = await fuzzyPrefilter({
8680
+ const prefiltered = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.prefilterMs`, () => fuzzyPrefilter({
8475
8681
  cacheKey: `conversations:${ctxId}`,
8476
8682
  relevantKeywords: keywords,
8477
8683
  importantKeyword,
8478
8684
  context: ctx,
8479
8685
  fields: ["name", "id", "external_id", "description"],
8480
8686
  normalize: (i) => [i.name, i.description].filter(Boolean).join(": ")
8481
- });
8687
+ }));
8482
8688
  pinnedItemIds = prefiltered.map((r) => r.id);
8483
8689
  }
8484
8690
  const keywordQuery = keywords.length ? keywords.join(" ") + " " + importantKeyword : question;
@@ -8493,7 +8699,7 @@ async function searchContexts(opts) {
8493
8699
  console.warn(`[EXULU pipeline] searchContexts failed for context "${ctxId}":`, err);
8494
8700
  return [];
8495
8701
  }
8496
- })
8702
+ }))
8497
8703
  );
8498
8704
  return { chunks: chunkArrays.flat() };
8499
8705
  }
@@ -8506,6 +8712,7 @@ var init_search = __esm({
8506
8712
  init_multi_query();
8507
8713
  init_hyde();
8508
8714
  init_prefilter();
8715
+ init_timing();
8509
8716
  init_text_utils();
8510
8717
  tagContext = (chunks, ctx) => chunks.map((c) => ({ ...c, context: { id: ctx.id, name: ctx.name ?? ctx.id } }));
8511
8718
  }
@@ -8659,6 +8866,33 @@ var init_rerank = __esm({
8659
8866
  }
8660
8867
  });
8661
8868
 
8869
+ // ee/agentic-retrieval/pipeline/pin-rerun.ts
8870
+ function needsPinRerun(originalQuestion, updatedQuestion) {
8871
+ if (originalQuestion === updatedQuestion) return false;
8872
+ const before = designations(originalQuestion);
8873
+ for (const token of designations(updatedQuestion)) {
8874
+ if (!before.has(token)) return true;
8875
+ }
8876
+ return false;
8877
+ }
8878
+ function designations(text) {
8879
+ const out = /* @__PURE__ */ new Set();
8880
+ for (const raw of text.split(/[\s,;:()\[\]"'?!]+/)) {
8881
+ const token = raw.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
8882
+ if (!token) continue;
8883
+ const hasDigit = /\p{N}/u.test(token);
8884
+ const capitals = (token.match(/\p{Lu}/gu) ?? []).length;
8885
+ if (hasDigit || capitals >= 2) out.add(token.toLowerCase());
8886
+ }
8887
+ return out;
8888
+ }
8889
+ var init_pin_rerun = __esm({
8890
+ "ee/agentic-retrieval/pipeline/pin-rerun.ts"() {
8891
+ "use strict";
8892
+ init_cjs_shims();
8893
+ }
8894
+ });
8895
+
8662
8896
  // ee/agentic-retrieval/pipeline/index.ts
8663
8897
  function addChunks(result, chunks) {
8664
8898
  const seen = new Set(result.chunks.map((c) => c.chunk_id));
@@ -8824,6 +9058,14 @@ function createAgenticRetrievalTool(opts) {
8824
9058
  usage: [],
8825
9059
  totalTokens: 0
8826
9060
  };
9061
+ const t0 = Date.now();
9062
+ const timings = {};
9063
+ let tPhase = t0;
9064
+ const lap = (name) => {
9065
+ const now = Date.now();
9066
+ timings[name] = (timings[name] ?? 0) + (now - tPhase);
9067
+ tPhase = now;
9068
+ };
8827
9069
  try {
8828
9070
  let enabledContexts = contexts.filter(
8829
9071
  (ctx) => cfg.knowledgeBases[ctx.id]?.enabled !== false
@@ -8896,8 +9138,19 @@ function createAgenticRetrievalTool(opts) {
8896
9138
  resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
8897
9139
  ${projectScope.customInstructions}` : ""
8898
9140
  ].filter(Boolean).join("\n");
8899
- const [memResult, routResult] = await Promise.all([
8900
- runMemoryPhase({
9141
+ const engineV2 = cfg.tuning.engine === "v2";
9142
+ const v2 = cfg.tuning.v2;
9143
+ const pinsFor = (question) => resolveIdentifierPins({
9144
+ question,
9145
+ identifierSets: cfg.vocabulary.identifiers,
9146
+ contextsById,
9147
+ kbKindById,
9148
+ model: utilityModel
9149
+ });
9150
+ const [memResult, routResult, parallelPins] = await Promise.all([
9151
+ withTiming(timings, "memoryMs", () => runMemoryPhase({
9152
+ timings,
9153
+ mergedCall: engineV2 && v2.mergedMemoryCall,
8901
9154
  memoryChunks: memoryItems ?? [],
8902
9155
  memoryContext,
8903
9156
  question: userQuery,
@@ -8909,8 +9162,10 @@ ${projectScope.customInstructions}` : ""
8909
9162
  memoryConfig: cfg.memory,
8910
9163
  glossary: cfg.vocabulary.glossary,
8911
9164
  documentContexts
8912
- }),
8913
- runRoutingPhase({
9165
+ })),
9166
+ withTiming(timings, "routingMs", () => runRoutingPhase({
9167
+ timings,
9168
+ mergedCall: engineV2 && v2.mergedRoutingCall,
8914
9169
  question: userQuery,
8915
9170
  enabledContexts,
8916
9171
  documentContexts,
@@ -8921,8 +9176,12 @@ ${projectScope.customInstructions}` : ""
8921
9176
  // detector must never treat these as filename hints.
8922
9177
  knownIdentifiers: cfg.vocabulary.identifiers.flatMap((i) => i.examples),
8923
9178
  model: utilityModel
8924
- })
9179
+ })),
9180
+ // engine v2: identifier pins depend only on the question, so they run alongside
9181
+ // memory and routing instead of after them (re-run below if memory rewrote the question).
9182
+ engineV2 && v2.parallelPins ? withTiming(timings, "pinsParallelMs", () => pinsFor(userQuery)) : Promise.resolve(null)
8925
9183
  ]);
9184
+ lap("memoryRoutingMs");
8926
9185
  for (const step of [...memResult.steps, ...routResult.steps]) {
8927
9186
  result.steps.push({
8928
9187
  stepNumber: 1,
@@ -8972,13 +9231,8 @@ ${projectScope.customInstructions}` : ""
8972
9231
  memoryPinnedItemIdsByContext,
8973
9232
  memoryOverride
8974
9233
  } = memResult;
8975
- const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await resolveIdentifierPins({
8976
- question: updatedQuestion,
8977
- identifierSets: cfg.vocabulary.identifiers,
8978
- contextsById,
8979
- kbKindById,
8980
- model: utilityModel
8981
- });
9234
+ const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = parallelPins && !needsPinRerun(userQuery, updatedQuestion) ? parallelPins : await pinsFor(updatedQuestion);
9235
+ lap("pinsMs");
8982
9236
  for (const step of pinSteps) {
8983
9237
  result.steps.push({
8984
9238
  stepNumber: 1,
@@ -9008,6 +9262,8 @@ ${projectScope.customInstructions}` : ""
9008
9262
  rewrites: cfg.vocabulary.rewrites,
9009
9263
  styleHint: cfg.vocabulary.styleHint,
9010
9264
  maxQueries: cfg.tuning.maxQueriesPerContext,
9265
+ timings,
9266
+ timingPrefix: "search.main",
9011
9267
  skipPrefilter: false
9012
9268
  }),
9013
9269
  fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
@@ -9028,9 +9284,12 @@ ${projectScope.customInstructions}` : ""
9028
9284
  rewrites: cfg.vocabulary.rewrites,
9029
9285
  styleHint: cfg.vocabulary.styleHint,
9030
9286
  maxQueries: cfg.tuning.maxQueriesPerContext,
9287
+ timings,
9288
+ timingPrefix: "search.fallback",
9031
9289
  skipPrefilter: true
9032
9290
  }) : Promise.resolve({ chunks: [] })
9033
9291
  ]);
9292
+ lap("searchMs");
9034
9293
  const pinnedItemIds = /* @__PURE__ */ new Set([
9035
9294
  ...(function* () {
9036
9295
  for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
@@ -9093,6 +9352,7 @@ ${projectScope.customInstructions}` : ""
9093
9352
  chunks: [],
9094
9353
  tokens: 0
9095
9354
  });
9355
+ lap("rerankMs");
9096
9356
  addChunks(result, mainRerank.limited_results);
9097
9357
  yield { result: serializeOutput(result) };
9098
9358
  const literalLookupSatisfied = hasExplicitDocAndPage && mainRerank.limited_results.length > 0 && mainRerank.limited_results.some((r) => {
@@ -9141,6 +9401,7 @@ ${projectScope.customInstructions}` : ""
9141
9401
  });
9142
9402
  result.reasoning.push({ text: "Fallback results reranked", tools: [] });
9143
9403
  addChunks(result, fallbackRerank.limited_results);
9404
+ lap("fallbackRerankMs");
9144
9405
  yield { result: serializeOutput(result) };
9145
9406
  }
9146
9407
  if (memoryOverride.active) {
@@ -9163,9 +9424,19 @@ Verified answer:
9163
9424
  addChunks(result, memoryOverride.chunks);
9164
9425
  yield { result: serializeOutput(result) };
9165
9426
  }
9427
+ timings.totalMs = Date.now() - t0;
9428
+ result.timings = timings;
9429
+ result.steps.push({
9430
+ stepNumber: 1,
9431
+ text: `Timing: memory+routing ${timings.memoryRoutingMs ?? 0}ms (memory ${timings.memoryMs ?? 0}ms, routing ${timings.routingMs ?? 0}ms), pins ${timings.pinsMs ?? 0}ms, search ${timings.searchMs ?? 0}ms, rerank ${timings.rerankMs ?? 0}ms` + (timings.fallbackRerankMs !== void 0 ? `, fallback rerank ${timings.fallbackRerankMs}ms` : "") + `, total ${timings.totalMs}ms`,
9432
+ toolCalls: [],
9433
+ chunks: [],
9434
+ tokens: 0
9435
+ });
9166
9436
  if (cfg.logging) {
9167
- console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length }));
9437
+ console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length, timings }));
9168
9438
  }
9439
+ yield { result: serializeOutput(result) };
9169
9440
  return { result: serializeOutput(result) };
9170
9441
  } catch (err) {
9171
9442
  console.warn("[EXULU pipeline] retrieval pipeline failed:", err);
@@ -9200,6 +9471,8 @@ var init_pipeline = __esm({
9200
9471
  init_prefilter();
9201
9472
  init_search();
9202
9473
  init_rerank();
9474
+ init_timing();
9475
+ init_pin_rerun();
9203
9476
  init_global_ids();
9204
9477
  init_global_ids();
9205
9478
  }
@@ -10608,6 +10881,68 @@ function resolveSearchQueryTexts(query) {
10608
10881
  hybridOrQuery: buildFullTextOrQuery(query, ftsText)
10609
10882
  };
10610
10883
  }
10884
+ function chooseFullTextQuery(opts) {
10885
+ if (opts.strictMatches || countOrTerms(opts.orText) > MAX_OR_TERMS) {
10886
+ return { fn: "plainto_tsquery", text: opts.strictText };
10887
+ }
10888
+ return { fn: "websearch_to_tsquery", text: opts.orText };
10889
+ }
10890
+ var MAX_OR_TERMS = 12;
10891
+ function countOrTerms(orText) {
10892
+ return orText.trim() ? orText.split(/\s+or\s+/i).length : 0;
10893
+ }
10894
+
10895
+ // src/graphql/resolvers/expand-neighbours.ts
10896
+ init_cjs_shims();
10897
+ function planNeighbourFetch(results, expand) {
10898
+ const before = Math.max(0, expand.before ?? 0);
10899
+ const after = Math.max(0, expand.after ?? 0);
10900
+ const plan = /* @__PURE__ */ new Map();
10901
+ if (before === 0 && after === 0) return plan;
10902
+ const present = new Set(results.map((r) => `${r.item_id}-${r.chunk_index}`));
10903
+ for (const r of results) {
10904
+ for (let i = r.chunk_index - before; i <= r.chunk_index + after; i++) {
10905
+ if (i < 0 || i === r.chunk_index || present.has(`${r.item_id}-${i}`)) continue;
10906
+ if (!plan.has(r.item_id)) plan.set(r.item_id, /* @__PURE__ */ new Set());
10907
+ plan.get(r.item_id).add(i);
10908
+ }
10909
+ }
10910
+ return plan;
10911
+ }
10912
+ function mergeNeighbours(results, rows, plan, context) {
10913
+ const byItem = /* @__PURE__ */ new Map();
10914
+ for (const r of results) if (!byItem.has(r.item_id)) byItem.set(r.item_id, r);
10915
+ const merged = /* @__PURE__ */ new Map();
10916
+ for (const r of results) merged.set(`${r.item_id}-${r.chunk_index}`, r);
10917
+ for (const row of rows) {
10918
+ if (!plan.get(row.source)?.has(row.chunk_index)) continue;
10919
+ const key = `${row.source}-${row.chunk_index}`;
10920
+ if (merged.has(key)) continue;
10921
+ const parent = byItem.get(row.source);
10922
+ if (!parent) continue;
10923
+ merged.set(key, {
10924
+ chunk_content: row.content,
10925
+ chunk_index: row.chunk_index,
10926
+ chunk_id: row.id,
10927
+ chunk_source: row.source,
10928
+ chunk_metadata: row.metadata,
10929
+ chunk_created_at: row.createdAt,
10930
+ chunk_updated_at: row.updatedAt,
10931
+ item_updated_at: parent.item_updated_at,
10932
+ item_created_at: parent.item_created_at,
10933
+ item_id: parent.item_id,
10934
+ item_external_id: parent.item_external_id,
10935
+ item_name: parent.item_name,
10936
+ chunk_cosine_distance: 0,
10937
+ chunk_fts_rank: 0,
10938
+ chunk_hybrid_score: 0,
10939
+ context
10940
+ });
10941
+ }
10942
+ return Array.from(merged.values()).sort(
10943
+ (a, b) => a.item_id === b.item_id ? a.chunk_index - b.chunk_index : 0
10944
+ );
10945
+ }
10611
10946
 
10612
10947
  // src/graphql/resolvers/apply-sorting.ts
10613
10948
  init_cjs_shims();
@@ -11598,6 +11933,14 @@ var agentsSchema = {
11598
11933
  name: "max_tool_steps",
11599
11934
  type: "number"
11600
11935
  },
11936
+ {
11937
+ // Thinking budget of the answer model, forwarded as LiteLLM's
11938
+ // reasoning_effort ("none" | "disable" | "minimal" | "low" | "medium" |
11939
+ // "high"). null = provider default. See resolve-reasoning-effort.ts.
11940
+ // Auto-ALTERed on boot.
11941
+ name: "reasoning_effort",
11942
+ type: "text"
11943
+ },
11601
11944
  {
11602
11945
  name: "guest_access",
11603
11946
  type: "boolean",
@@ -12315,6 +12658,15 @@ var convertContextToTableDefinition = (context) => {
12315
12658
  // src/graphql/resolvers/vector-search.ts
12316
12659
  init_statistics2();
12317
12660
 
12661
+ // src/graphql/resolvers/query-embedding-policy.ts
12662
+ init_cjs_shims();
12663
+ function needsQueryEmbedding(method) {
12664
+ return method !== "tsvector";
12665
+ }
12666
+ function boostsWithQueryEntities(method) {
12667
+ return method !== "tsvector";
12668
+ }
12669
+
12318
12670
  // src/exulu/entities/index.ts
12319
12671
  init_cjs_shims();
12320
12672
  init_client();
@@ -13095,7 +13447,9 @@ var vectorSearch = async ({
13095
13447
  const embedText = texts.embedText;
13096
13448
  hybridOrQuery = texts.hybridOrQuery;
13097
13449
  query = texts.ftsText;
13098
- if (queryEmbedding && queryEmbedding.length) {
13450
+ if (!needsQueryEmbedding(method)) {
13451
+ _embedSource = "none";
13452
+ } else if (queryEmbedding && queryEmbedding.length) {
13099
13453
  vector = queryEmbedding;
13100
13454
  _embedSource = "reused";
13101
13455
  } else {
@@ -13128,8 +13482,10 @@ var vectorSearch = async ({
13128
13482
  vector = queryVector;
13129
13483
  _embedSource = "computed";
13130
13484
  }
13131
- vectorStr = `ARRAY[${vector.join(",")}]`;
13132
- vectorExpr = `${vectorStr}::vector`;
13485
+ if (vector.length) {
13486
+ vectorStr = `ARRAY[${vector.join(",")}]`;
13487
+ vectorExpr = `${vectorStr}::vector`;
13488
+ }
13133
13489
  }
13134
13490
  let keywordsQuery = [];
13135
13491
  if (keywords) {
@@ -13187,14 +13543,22 @@ var vectorSearch = async ({
13187
13543
  ]);
13188
13544
  resultChunks = await chunksQuery;
13189
13545
  break;
13190
- case "hybridSearch":
13546
+ case "hybridSearch": {
13547
+ let strictMatches = false;
13548
+ if (query && hybridOrQuery) {
13549
+ const probe = await db2(chunksTable + " as chunks").select(db2.raw("1")).whereRaw(`(${languages.map((lang) => `chunks.fts @@ plainto_tsquery('${lang}', ?)`).join(" OR ")})`, languages.map(() => query)).first();
13550
+ strictMatches = Boolean(probe);
13551
+ }
13552
+ const fullText = chooseFullTextQuery({ strictMatches, strictText: query ?? "", orText: hybridOrQuery });
13553
+ const ftsFn = fullText.fn;
13554
+ hybridOrQuery = fullText.text;
13191
13555
  const matchCount = Math.min(limit * 2);
13192
13556
  const fullTextWeight = 2;
13193
13557
  const semanticWeight = 1;
13194
13558
  const rrfK = 50;
13195
- const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ");
13559
+ const ftRankExpression = languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ");
13196
13560
  const ftRankParams = languages.map(() => hybridOrQuery);
13197
- const ftMatchExpression = languages.map((lang) => `chunks.fts @@ websearch_to_tsquery('${lang}', ?)`).join(" OR ");
13561
+ const ftMatchExpression = languages.map((lang) => `chunks.fts @@ ${ftsFn}('${lang}', ?)`).join(" OR ");
13198
13562
  const ftMatchParams = languages.map(() => hybridOrQuery);
13199
13563
  let fullTextQuery = db2(chunksTable + " as chunks").select([
13200
13564
  "chunks.id",
@@ -13235,7 +13599,7 @@ var vectorSearch = async ({
13235
13599
  db2.raw('items."updatedAt" as item_updated_at'),
13236
13600
  db2.raw('items."createdAt" as item_created_at'),
13237
13601
  db2.raw(
13238
- `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) AS fts_rank`,
13602
+ `GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) AS fts_rank`,
13239
13603
  languages.map(() => hybridOrQuery)
13240
13604
  ),
13241
13605
  db2.raw(`(1 - (chunks.embedding <=> ${vectorExpr})) AS cosine_distance`),
@@ -13261,12 +13625,14 @@ var vectorSearch = async ({
13261
13625
  `,
13262
13626
  [rrfK, fullTextWeight, rrfK, semanticWeight, cutoffs?.hybrid || 0]
13263
13627
  ).whereRaw(
13264
- `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, websearch_to_tsquery('${lang}', ?))`).join(", ")}) > ?)`,
13628
+ `(chunks.fts IS NULL OR GREATEST(${languages.map((lang) => `ts_rank(chunks.fts, ${ftsFn}('${lang}', ?))`).join(", ")}) > ?)`,
13265
13629
  [...languages.map(() => hybridOrQuery), cutoffs?.tsvector || 0]
13266
13630
  ).whereRaw(`(chunks.embedding IS NULL OR (1 - (chunks.embedding <=> ${vectorExpr})) >= ?)`, [
13267
13631
  cutoffs?.cosineDistance || 0
13268
13632
  ]).orderByRaw("hybrid_score DESC").limit(Math.min(matchCount, 250));
13269
13633
  resultChunks = await hybridQuery;
13634
+ break;
13635
+ }
13270
13636
  }
13271
13637
  if (process.env.EXULU_VS_TIMING) {
13272
13638
  console.log(
@@ -13318,7 +13684,7 @@ var vectorSearch = async ({
13318
13684
  }
13319
13685
  let queryEntities = [];
13320
13686
  let entityInsights;
13321
- if (entitiesOn && rawQuery) {
13687
+ if (entitiesOn && rawQuery && boostsWithQueryEntities(method)) {
13322
13688
  try {
13323
13689
  const types = await hydrateEntityTypes(context);
13324
13690
  const { mentions: queryMentions } = await extractEntitiesForItem({
@@ -13350,104 +13716,18 @@ var vectorSearch = async ({
13350
13716
  }
13351
13717
  results = results.slice(0, limit);
13352
13718
  if (expand?.before || expand?.after) {
13353
- const expandedMap = /* @__PURE__ */ new Map();
13354
- for (const chunk of results) {
13355
- expandedMap.set(`${chunk.item_id}-${chunk.chunk_index}`, chunk);
13356
- }
13357
- if (expand?.before) {
13358
- for (const chunk of results) {
13359
- const indicesToFetch = Array.from(
13360
- { length: expand.before },
13361
- (_, i) => chunk.chunk_index - expand.before + i
13362
- ).filter((index) => index >= 0);
13363
- await Promise.all(
13364
- indicesToFetch.map(async (index) => {
13365
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
13366
- return;
13367
- }
13368
- const expandedChunk = await db2(chunksTable).where({
13369
- source: chunk.item_id,
13370
- chunk_index: index
13371
- }).first();
13372
- if (expandedChunk) {
13373
- if (expandedChunk) {
13374
- expandedMap.set(`${chunk.item_id}-${index}`, {
13375
- chunk_content: expandedChunk.content,
13376
- chunk_index: expandedChunk.chunk_index,
13377
- chunk_id: expandedChunk.id,
13378
- chunk_source: expandedChunk.source,
13379
- chunk_metadata: expandedChunk.metadata,
13380
- chunk_created_at: expandedChunk.createdAt,
13381
- chunk_updated_at: expandedChunk.updatedAt,
13382
- item_updated_at: chunk.item_updated_at,
13383
- item_created_at: chunk.item_created_at,
13384
- item_id: chunk.item_id,
13385
- item_external_id: chunk.item_external_id,
13386
- item_name: chunk.item_name,
13387
- chunk_cosine_distance: 0,
13388
- chunk_fts_rank: 0,
13389
- chunk_hybrid_score: 0,
13390
- context: {
13391
- name: table.name.singular,
13392
- id: table.id || ""
13393
- }
13394
- });
13395
- }
13396
- }
13397
- })
13398
- );
13399
- }
13400
- }
13401
- if (expand?.after) {
13402
- for (const chunk of results) {
13403
- const indicesToFetch = Array.from(
13404
- { length: expand.after },
13405
- (_, i) => chunk.chunk_index + i + 1
13406
- );
13407
- await Promise.all(
13408
- indicesToFetch.map(async (index) => {
13409
- if (expandedMap.has(`${chunk.item_id}-${index}`)) {
13410
- return;
13411
- }
13412
- const expandedChunk = await db2(chunksTable).where({
13413
- source: chunk.item_id,
13414
- chunk_index: index
13415
- }).first();
13416
- if (expandedChunk) {
13417
- expandedMap.set(`${chunk.item_id}-${index}`, {
13418
- chunk_content: expandedChunk.content,
13419
- chunk_index: expandedChunk.chunk_index,
13420
- chunk_id: expandedChunk.id,
13421
- chunk_source: expandedChunk.source,
13422
- chunk_metadata: expandedChunk.metadata,
13423
- chunk_created_at: expandedChunk.createdAt,
13424
- chunk_updated_at: expandedChunk.updatedAt,
13425
- item_updated_at: chunk.item_updated_at,
13426
- item_created_at: chunk.item_created_at,
13427
- item_id: chunk.item_id,
13428
- item_external_id: chunk.item_external_id,
13429
- item_name: chunk.item_name,
13430
- chunk_cosine_distance: 0,
13431
- chunk_fts_rank: 0,
13432
- chunk_hybrid_score: 0,
13433
- context: {
13434
- name: table.name.singular,
13435
- id: table.id || ""
13436
- }
13437
- });
13438
- }
13439
- })
13440
- );
13441
- }
13719
+ const plan = planNeighbourFetch(results, expand);
13720
+ if (plan.size > 0) {
13721
+ const itemIds = Array.from(plan.keys());
13722
+ const indices = Array.from(new Set(Array.from(plan.values()).flatMap((s) => Array.from(s))));
13723
+ const rows = await db2(chunksTable).select(["id", "source", "chunk_index", "content", "metadata", "createdAt", "updatedAt"]).whereIn("source", itemIds).whereIn("chunk_index", indices);
13724
+ results = mergeNeighbours(results, rows, plan, { name: table.name.singular, id: table.id || "" });
13442
13725
  }
13443
- results = Array.from(expandedMap.values());
13444
13726
  results = results.sort((a, b) => {
13445
13727
  if (a.item_id !== b.item_id) {
13446
13728
  return a.item_id.localeCompare(b.item_id);
13447
13729
  }
13448
- const aIndex = Number(a.chunk_index);
13449
- const bIndex = Number(b.chunk_index);
13450
- return aIndex - bIndex;
13730
+ return Number(a.chunk_index) - Number(b.chunk_index);
13451
13731
  });
13452
13732
  }
13453
13733
  if (entitiesOn) {
@@ -17038,6 +17318,20 @@ function serializeError(err, depth = 0) {
17038
17318
  return { message: String(err) };
17039
17319
  }
17040
17320
 
17321
+ // src/exulu/turn-metadata.ts
17322
+ init_cjs_shims();
17323
+ function finishTurnMetadata(opts) {
17324
+ const now = opts.now ?? Date.now();
17325
+ return {
17326
+ totalTokens: opts.totalUsage.totalTokens,
17327
+ reasoningTokens: opts.totalUsage.reasoningTokens,
17328
+ inputTokens: opts.totalUsage.inputTokens,
17329
+ outputTokens: opts.totalUsage.outputTokens,
17330
+ cachedInputTokens: opts.totalUsage.cachedInputTokens,
17331
+ durationMs: Math.max(0, now - opts.startedAt)
17332
+ };
17333
+ }
17334
+
17041
17335
  // src/utils/enabled-tools.ts
17042
17336
  init_cjs_shims();
17043
17337
  init_pipeline();
@@ -17162,6 +17456,58 @@ async function resolveFreshFileUrl(url, opts) {
17162
17456
  }
17163
17457
  }
17164
17458
 
17459
+ // src/exulu/session-file-listing.ts
17460
+ init_cjs_shims();
17461
+ init_uppy();
17462
+ init_artifact_filter();
17463
+ init_session_files();
17464
+ var DEFAULT_MAX = 25;
17465
+ function formatSize(bytes) {
17466
+ if (bytes < 1024) return `${bytes} B`;
17467
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
17468
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
17469
+ }
17470
+ function formatAge(then, now) {
17471
+ const minutes = Math.max(0, Math.round((now.getTime() - then.getTime()) / 6e4));
17472
+ if (minutes < 1) return "just now";
17473
+ if (minutes < 60) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
17474
+ const hours = Math.round(minutes / 60);
17475
+ if (hours < 48) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
17476
+ const days = Math.round(hours / 24);
17477
+ return `${days} day${days === 1 ? "" : "s"} ago`;
17478
+ }
17479
+ function describeSessionFiles(files, opts = {}) {
17480
+ const now = opts.now ?? /* @__PURE__ */ new Date();
17481
+ const max = opts.max ?? DEFAULT_MAX;
17482
+ const usable = files.filter((f) => f.name && !f.name.endsWith("/") && !isIgnoredArtifactPath(f.name)).sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
17483
+ if (usable.length === 0) return "";
17484
+ const shown = usable.slice(0, max);
17485
+ const lines = shown.map((f) => {
17486
+ const isNew = opts.lastTurnAt ? f.lastModified.getTime() > opts.lastTurnAt.getTime() : false;
17487
+ return `- ${f.name} (${formatSize(f.size)}, ${formatAge(f.lastModified, now)})${isNew ? " [NEW since your last answer]" : ""}`;
17488
+ });
17489
+ const omitted = usable.length - shown.length;
17490
+ if (omitted > 0) lines.push(`\u2026 and ${omitted} more file${omitted === 1 ? "" : "s"} (list them with \`ls\`).`);
17491
+ 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.';
17492
+ }
17493
+ async function loadSessionFileListing(opts) {
17494
+ const uploads = opts.exuluConfig?.fileUploads;
17495
+ if (!uploads?.s3Bucket) return "";
17496
+ const prefix = sessionFilePrefix(opts.ownerId, opts.sessionID, uploads.s3prefix);
17497
+ try {
17498
+ const objects = await listS3ObjectsByPrefix(prefix, opts.exuluConfig);
17499
+ const files = objects.map((o) => ({
17500
+ name: o.key.slice(o.key.indexOf(prefix) + prefix.length),
17501
+ size: o.size,
17502
+ lastModified: new Date(o.lastModified)
17503
+ }));
17504
+ return describeSessionFiles(files, { lastTurnAt: opts.lastTurnAt });
17505
+ } catch (err) {
17506
+ console.warn(`[EXULU] could not list session files for prompt (session ${opts.sessionID}):`, err);
17507
+ return "";
17508
+ }
17509
+ }
17510
+
17165
17511
  // src/exulu/generate-stream.ts
17166
17512
  init_uppy();
17167
17513
  var import_ai6 = require("ai");
@@ -17413,11 +17759,12 @@ function resolveTurnStepBudget(maxStepCount, agent) {
17413
17759
  }
17414
17760
  return DEFAULT_MAX_STEPS;
17415
17761
  }
17762
+ var TOOL_INPUT_FLATTEN_CHARS = 6e3;
17416
17763
  function flattenPart(part) {
17417
17764
  const p = part;
17418
17765
  if (p?.type === "text") return p.text ?? "";
17419
17766
  if (p?.type === "tool-call") {
17420
- return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, 300)}`;
17767
+ return `Earlier, the assistant ran the "${p.toolName}" tool with input: ${JSON.stringify(p.input ?? {}).slice(0, TOOL_INPUT_FLATTEN_CHARS)}`;
17421
17768
  }
17422
17769
  if (p?.type === "tool-result") {
17423
17770
  const out = p.output?.value ?? p.output;
@@ -17439,7 +17786,7 @@ function flattenToolHistory(messages) {
17439
17786
  return m;
17440
17787
  });
17441
17788
  }
17442
- 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.`;
17789
+ 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.`;
17443
17790
  function finalAnswerGuard(maxSteps) {
17444
17791
  return ({ stepNumber, messages }) => stepNumber >= maxSteps - 1 ? {
17445
17792
  toolChoice: "none",
@@ -17464,6 +17811,37 @@ function retrievalBudgetGuard(limit, agenticToolKey, allToolKeys) {
17464
17811
  };
17465
17812
  }
17466
17813
 
17814
+ // src/exulu/resolve-reasoning-effort.ts
17815
+ init_cjs_shims();
17816
+ var REASONING_EFFORTS = ["none", "disable", "minimal", "low", "medium", "high"];
17817
+ function resolveReasoningEffort(agent) {
17818
+ const raw = agent?.reasoning_effort;
17819
+ if (typeof raw !== "string") return void 0;
17820
+ const normalized = raw.trim().toLowerCase();
17821
+ return REASONING_EFFORTS.includes(normalized) ? normalized : void 0;
17822
+ }
17823
+ function resolveProviderOptions(agent) {
17824
+ const effort = resolveReasoningEffort(agent);
17825
+ return {
17826
+ openai: { reasoningSummary: "auto" },
17827
+ ...effort ? { litellm: { reasoningEffort: effort } } : {}
17828
+ };
17829
+ }
17830
+
17831
+ // src/exulu/stream-error.ts
17832
+ init_cjs_shims();
17833
+ function onChatStreamError({ error }) {
17834
+ const detail = error instanceof Error ? error.message : error === void 0 ? "unknown error" : safeStringify(error);
17835
+ console.error("[EXULU] chat stream error.", detail);
17836
+ }
17837
+ function safeStringify(value) {
17838
+ try {
17839
+ return JSON.stringify(value) ?? String(value);
17840
+ } catch {
17841
+ return String(value);
17842
+ }
17843
+ }
17844
+
17467
17845
  // src/exulu/generate-stream.ts
17468
17846
  init_sanitize_tool_name();
17469
17847
  init_tool_image_attachments();
@@ -17561,6 +17939,15 @@ var saveChat = async ({
17561
17939
  await mutation;
17562
17940
  }
17563
17941
  };
17942
+ var lastMessageTime = (rows) => {
17943
+ let latest;
17944
+ for (const row of rows) {
17945
+ if (!row.createdAt) continue;
17946
+ const d = new Date(row.createdAt);
17947
+ if (!latest || d > latest) latest = d;
17948
+ }
17949
+ return latest;
17950
+ };
17564
17951
  var getAgentMessages = async ({
17565
17952
  session,
17566
17953
  user,
@@ -17618,6 +18005,7 @@ var generateSync = async ({
17618
18005
  let project;
17619
18006
  let sessionItems;
17620
18007
  let sessionOwnerId;
18008
+ let lastTurnAt;
17621
18009
  if (session) {
17622
18010
  const sessionData = await getSession({ sessionID: session });
17623
18011
  sessionItems = sessionData.session_items;
@@ -17632,6 +18020,7 @@ var generateSync = async ({
17632
18020
  session,
17633
18021
  user: user.id
17634
18022
  });
18023
+ lastTurnAt = lastMessageTime(previousMessages);
17635
18024
  const previousMessagesContent = previousMessages.map(
17636
18025
  (message) => JSON.parse(message.content)
17637
18026
  );
@@ -17794,6 +18183,15 @@ var generateSync = async ({
17794
18183
  commands like \`node create_doc.js\`) live in the same place. These files are scoped to
17795
18184
  this single session; they are NOT visible in other sessions, projects, or knowledge bases.
17796
18185
  `;
18186
+ if (session) {
18187
+ const listing = await loadSessionFileListing({
18188
+ sessionID: session,
18189
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
18190
+ exuluConfig,
18191
+ lastTurnAt
18192
+ });
18193
+ if (listing) system += "\n\n" + listing;
18194
+ }
17797
18195
  system += `
17798
18196
 
17799
18197
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -17963,6 +18361,7 @@ var generateStream = async ({
17963
18361
  let project;
17964
18362
  let sessionItems;
17965
18363
  let sessionOwnerId;
18364
+ let lastTurnAt;
17966
18365
  if (session) {
17967
18366
  const sessionData = await getSession({ sessionID: session });
17968
18367
  project = sessionData.project;
@@ -17978,6 +18377,7 @@ var generateStream = async ({
17978
18377
  includeAllUsers: isRunSessionMetadata(sessionData.metadata)
17979
18378
  });
17980
18379
  previousMessagesContent = previousMessages2.map((message2) => JSON.parse(message2.content));
18380
+ lastTurnAt = lastMessageTime(previousMessages2);
17981
18381
  }
17982
18382
  const model = languageModel;
17983
18383
  messages = await (0, import_ai6.validateUIMessages)({
@@ -18120,6 +18520,15 @@ ${skillsList}
18120
18520
  truncation notice, e.g. tool-output-*.txt). Use the read_session_file tool with offset/limit
18121
18521
  to page through it \u2014 do not ask the user to re-upload.
18122
18522
  `;
18523
+ if (session) {
18524
+ const listing = await loadSessionFileListing({
18525
+ sessionID: session,
18526
+ ownerId: sessionOwnerId ?? user?.id ?? "api",
18527
+ exuluConfig,
18528
+ lastTurnAt
18529
+ });
18530
+ if (listing) system += "\n\n" + listing;
18531
+ }
18123
18532
  system += `
18124
18533
 
18125
18534
  When a tool execution is not approved by the user, do not retry it unless explicitly asked by the user. ' +
@@ -18222,18 +18631,12 @@ When a tool execution is not approved by the user, do not retry it unless explic
18222
18631
  // for the first step or change other parameters.
18223
18632
  system,
18224
18633
  maxRetries: 2,
18225
- providerOptions: {
18226
- openai: {
18227
- reasoningSummary: "auto"
18228
- }
18229
- },
18634
+ // OpenAI reasoning summaries + the agent's optional thinking budget
18635
+ // (agents.reasoning_effort → LiteLLM reasoning_effort).
18636
+ providerOptions: resolveProviderOptions(agent),
18230
18637
  tools,
18231
- onError: (error) => {
18232
- console.error("[EXULU] chat stream error.", error);
18233
- throw new Error(
18234
- `Chat stream error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
18235
- );
18236
- },
18638
+ // Log only — throwing here crashed the process (see stream-error.ts).
18639
+ onError: onChatStreamError,
18237
18640
  // todo allow configuring the step budget per skill
18238
18641
  prepareStep: composePrepareSteps(contextGuard(contextWindow), retrievalGuard, finalAnswerGuard(turnBudget), imageAttachmentGuard()),
18239
18642
  stopWhen: [(0, import_ai6.stepCountIs)(turnBudget), (0, import_ai6.hasToolCall)("image_generation")]
@@ -18245,6 +18648,45 @@ When a tool execution is not approved by the user, do not retry it unless explic
18245
18648
  };
18246
18649
  };
18247
18650
 
18651
+ // src/utils/agent-glossary.ts
18652
+ init_cjs_shims();
18653
+ function extractGlossaryFromAgentTools(tools) {
18654
+ if (!Array.isArray(tools)) return [];
18655
+ const searchTool = tools.find((t) => t?.id === "agentic_context_search");
18656
+ const entry = searchTool?.config?.find((c) => c?.name === "vocabulary");
18657
+ if (entry === void 0 || entry.variable === void 0 || entry.variable === null || entry.variable === "") {
18658
+ return [];
18659
+ }
18660
+ try {
18661
+ const parsed = typeof entry.variable === "string" ? JSON.parse(entry.variable) : entry.variable;
18662
+ const glossary = parsed?.glossary;
18663
+ if (!Array.isArray(glossary)) return [];
18664
+ return glossary.filter(
18665
+ (g) => !!g && typeof g.term === "string" && typeof g.meaning === "string" && g.term.trim().length > 0
18666
+ );
18667
+ } catch (err) {
18668
+ console.warn(
18669
+ "[EXULU] Failed to parse the agentic_context_search vocabulary config while building agent instructions.",
18670
+ err
18671
+ );
18672
+ return [];
18673
+ }
18674
+ }
18675
+ function formatGlossaryBlock(glossary) {
18676
+ if (!glossary.length) return "";
18677
+ const lines = glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n");
18678
+ return `The organization's documents and internal terminology use the following abbreviations/terms:
18679
+ ${lines}`;
18680
+ }
18681
+ function withGlossary(baseInstructions, tools) {
18682
+ const instructions = baseInstructions ?? "";
18683
+ const block = formatGlossaryBlock(extractGlossaryFromAgentTools(tools));
18684
+ if (!block) return instructions;
18685
+ return instructions ? `${block}
18686
+
18687
+ ${instructions}` : block;
18688
+ }
18689
+
18248
18690
  // src/exulu/agent-as-tool.ts
18249
18691
  var createAgentTool = async (instance2, contexts) => {
18250
18692
  const agent = await exuluApp.get().agent(instance2);
@@ -18303,7 +18745,7 @@ var createAgentTool = async (instance2, contexts) => {
18303
18745
  const response = await generateSync({
18304
18746
  agent,
18305
18747
  contexts,
18306
- instructions: agent.instructions,
18748
+ instructions: withGlossary(agent.instructions, agent.tools),
18307
18749
  prompt: "The user has asked the following question: " + prompt + " and the following information is available: " + information,
18308
18750
  languageModel: resolved.languageModel,
18309
18751
  user,
@@ -20097,13 +20539,7 @@ var processUiMessagesFlow = async ({
20097
20539
  messageMetadata: ({ part }) => {
20098
20540
  console.log("[EXULU] part", part.type);
20099
20541
  if (part.type === "finish") {
20100
- return {
20101
- totalTokens: part.totalUsage.totalTokens,
20102
- reasoningTokens: part.totalUsage.reasoningTokens,
20103
- inputTokens: part.totalUsage.inputTokens,
20104
- outputTokens: part.totalUsage.outputTokens,
20105
- cachedInputTokens: part.totalUsage.cachedInputTokens
20106
- };
20542
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
20107
20543
  }
20108
20544
  return void 0;
20109
20545
  },
@@ -21337,7 +21773,7 @@ var recallService = {
21337
21773
  );
21338
21774
  const { text } = await (0, import_ai8.generateText)({
21339
21775
  model: resolved.languageModel,
21340
- system: agent.instructions || void 0,
21776
+ system: withGlossary(agent.instructions, agent.tools) || void 0,
21341
21777
  prompt: `${prompt.content}
21342
21778
 
21343
21779
  ---
@@ -26130,10 +26566,14 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
26130
26566
  }
26131
26567
  const approvedTools = req.body.approvedTools ? typeof req.body.approvedTools === "string" ? JSON.parse(req.body.approvedTools) : req.body.approvedTools : [];
26132
26568
  const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
26133
- const instructions = customInstructions ? `${agent.instructions}
26569
+ const instructions = withGlossary(
26570
+ customInstructions ? `${agent.instructions}
26134
26571
 
26135
- ${customInstructions}` : agent.instructions;
26572
+ ${customInstructions}` : agent.instructions,
26573
+ agent.tools
26574
+ );
26136
26575
  if (headers.session) markStreamActive(headers.session);
26576
+ const turnStartedAt = Date.now();
26137
26577
  let result;
26138
26578
  try {
26139
26579
  result = await generateStream({
@@ -26175,13 +26615,7 @@ ${customInstructions}` : agent.instructions;
26175
26615
  };
26176
26616
  }
26177
26617
  if (part.type === "finish") {
26178
- return {
26179
- totalTokens: part.totalUsage.totalTokens,
26180
- reasoningTokens: part.totalUsage.reasoningTokens,
26181
- inputTokens: part.totalUsage.inputTokens,
26182
- outputTokens: part.totalUsage.outputTokens,
26183
- cachedInputTokens: part.totalUsage.cachedInputTokens
26184
- };
26618
+ return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
26185
26619
  }
26186
26620
  return void 0;
26187
26621
  },
@@ -26273,9 +26707,12 @@ ${customInstructions}` : agent.instructions;
26273
26707
  return;
26274
26708
  } else {
26275
26709
  const customInstructions = req.body.customInstructions ? typeof req.body.customInstructions === "string" ? req.body.customInstructions : JSON.stringify(req.body.customInstructions) : "";
26276
- const instructions = customInstructions ? `${agent.instructions}
26710
+ const instructions = withGlossary(
26711
+ customInstructions ? `${agent.instructions}
26277
26712
 
26278
- ${customInstructions}` : agent.instructions;
26713
+ ${customInstructions}` : agent.instructions,
26714
+ agent.tools
26715
+ );
26279
26716
  let response;
26280
26717
  try {
26281
26718
  response = await generateSync({
@@ -33317,7 +33754,6 @@ ${command}`;
33317
33754
  }
33318
33755
 
33319
33756
  // ee/python/documents/processing/doc_processor.ts
33320
- init_python_setup();
33321
33757
  var import_liteparse = require("@llamaindex/liteparse");
33322
33758
 
33323
33759
  // src/exulu/resolve-ocr.ts
@@ -33704,7 +34140,7 @@ function reconstructTableHeaders(document2, validationResults, verbose = false)
33704
34140
  }
33705
34141
  }
33706
34142
  async function validateWithVLM(document2, model, verbose = false, concurrency = 10) {
33707
- console.log(`[EXULU] Starting VLM validation for docling output, ${document2.length} pages...`);
34143
+ console.log(`[EXULU] Starting VLM validation for processor output, ${document2.length} pages...`);
33708
34144
  console.log(`[EXULU] Concurrency limit: ${concurrency}`);
33709
34145
  const limit = (0, import_p_limit.default)(concurrency);
33710
34146
  const validationResults = /* @__PURE__ */ new Map();
@@ -33839,48 +34275,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
33839
34275
  async function processPdf(buffer, paths, config, verbose = false) {
33840
34276
  try {
33841
34277
  let json = [];
33842
- if (config?.processor.name === "docling") {
33843
- console.log(`[EXULU] Validating Python environment...`);
33844
- const validation = await validatePythonEnvironment(void 0, true);
33845
- if (!validation.valid) {
33846
- console.log(`[EXULU] Python environment not ready, setting up automatically...`);
33847
- console.log(`[EXULU] Reason: ${validation.message}`);
33848
- const setupResult = await setupPythonEnvironment({
33849
- verbose: true,
33850
- force: false
33851
- // Only setup if not already done
33852
- });
33853
- if (!setupResult.success) {
33854
- throw new Error(`Failed to setup Python environment: ${setupResult.message}
33855
-
33856
- ${setupResult.output || ""}`);
33857
- }
33858
- console.log(`[EXULU] Python environment setup completed successfully`);
33859
- } else {
33860
- console.log(`[EXULU] Python environment is valid`);
33861
- }
33862
- console.log(`[EXULU] Processing document with document_to_markdown.py`);
33863
- const result = await executePythonScript({
33864
- scriptPath: "ee/python/documents/processing/document_to_markdown.py",
33865
- args: [
33866
- paths.source,
33867
- "-o",
33868
- paths.json,
33869
- "--images-dir",
33870
- paths.images
33871
- ],
33872
- timeout: 30 * 60 * 1e3
33873
- // 30 minutes for large documents
33874
- });
33875
- if (result.stderr) {
33876
- console.log("Processing info:", result.stderr.trim());
33877
- }
33878
- if (!result.success) {
33879
- throw new Error(`Document processing failed: ${result.stderr}`);
33880
- }
33881
- const jsonContent = await fs4.promises.readFile(paths.json, "utf-8");
33882
- json = JSON.parse(jsonContent);
33883
- } else if (config?.processor.name === "officeparser") {
34278
+ if (config?.processor.name === "officeparser") {
33884
34279
  const text = await (0, import_officeparser3.parseOfficeAsync)(buffer, {
33885
34280
  outputErrorToConsole: false,
33886
34281
  newlineDelimiter: "\n"
@@ -33971,14 +34366,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
33971
34366
  image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
33972
34367
  }));
33973
34368
  fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
34369
+ } else {
34370
+ const configured = String(config?.processor?.name ?? "");
34371
+ throw new Error(
34372
+ 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.' : "")
34373
+ );
33974
34374
  }
33975
34375
  console.log(`[EXULU]
33976
34376
  \u2713 Document processing completed successfully`);
33977
34377
  console.log(`[EXULU] Total pages: ${json.length}`);
33978
34378
  console.log(`[EXULU] Output file: ${paths.json}`);
33979
- if (config?.vlm?.model) {
33980
- console.error("[EXULU] VLM validation is only supported when docling is enabled, skipping validation.");
33981
- }
33982
34379
  const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
33983
34380
  if (vlmModel && json.length > 0) {
33984
34381
  json = await validateWithVLM(
@@ -34096,9 +34493,6 @@ async function documentProcessor({
34096
34493
  } = await loadFile(file, name, tempDir);
34097
34494
  let supportedTypes = [];
34098
34495
  switch (config?.processor.name) {
34099
- case "docling":
34100
- supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
34101
- break;
34102
34496
  case "officeparser":
34103
34497
  supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
34104
34498
  break;