@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.
- package/dist/{chunk-27K2CO47.js → chunk-QMN6MVHQ.js} +1 -1
- package/dist/{chunk-AWMU6QXB.js → chunk-RBEWHG7I.js} +297 -39
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-XNQ6Q3X6.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
- package/dist/index.cjs +566 -217
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +250 -180
- package/dist/{python-setup-JZGHWQCG.js → python-setup-DRJ3QX5F.js} +1 -1
- package/ee/LICENSE.md +2 -2
- package/ee/agentic-retrieval/pipeline/config.test.ts +18 -1
- package/ee/agentic-retrieval/pipeline/config.ts +15 -0
- package/ee/agentic-retrieval/pipeline/index.test.ts +73 -0
- package/ee/agentic-retrieval/pipeline/index.ts +67 -13
- package/ee/agentic-retrieval/pipeline/memory.test.ts +59 -0
- package/ee/agentic-retrieval/pipeline/memory.ts +181 -11
- package/ee/agentic-retrieval/pipeline/pin-rerun.test.ts +17 -0
- package/ee/agentic-retrieval/pipeline/pin-rerun.ts +29 -0
- package/ee/agentic-retrieval/pipeline/routing.test.ts +34 -0
- package/ee/agentic-retrieval/pipeline/routing.ts +96 -5
- package/ee/agentic-retrieval/pipeline/search.ts +9 -6
- package/ee/agentic-retrieval/pipeline/timing.test.ts +24 -0
- package/ee/agentic-retrieval/pipeline/timing.ts +26 -0
- package/ee/agentic-retrieval/pipeline/types.ts +2 -0
- package/ee/python/documents/processing/README.md +2 -3
- package/ee/python/documents/processing/doc_processor.ts +21 -61
- package/ee/python/documents/processing/split_pdf.py +25 -30
- package/ee/python/documents/processing/tests/__init__.py +0 -0
- package/ee/python/documents/processing/tests/test_split_pdf.py +230 -0
- package/ee/python/requirements.txt +12 -2
- package/ee/python/setup.sh +40 -1
- package/ee/python/transcription/pipeline.py +109 -15
- package/ee/python/transcription/tests/test_align_model_licensing.py +184 -0
- package/ee/workers.ts +2 -7
- package/license.md +2 -2
- package/package.json +3 -4
- package/scripts/postinstall.cjs +52 -1
- 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 = ["
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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 (
|
|
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
|
|
8900
|
-
|
|
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
|
|
8976
|
-
|
|
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 (
|
|
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
|
-
|
|
13132
|
-
|
|
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,
|
|
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 @@
|
|
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,
|
|
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,
|
|
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
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
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
|
-
}
|
|
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 || "" });
|
|
13400
13725
|
}
|
|
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
|
-
}
|
|
13442
|
-
}
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
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
|
-
|
|
18232
|
-
|
|
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")]
|
|
@@ -20097,13 +20500,7 @@ var processUiMessagesFlow = async ({
|
|
|
20097
20500
|
messageMetadata: ({ part }) => {
|
|
20098
20501
|
console.log("[EXULU] part", part.type);
|
|
20099
20502
|
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
|
-
};
|
|
20503
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: startTime });
|
|
20107
20504
|
}
|
|
20108
20505
|
return void 0;
|
|
20109
20506
|
},
|
|
@@ -26134,6 +26531,7 @@ var createExpressRoutes = async (app, tools, contexts, config, evals, tracer) =>
|
|
|
26134
26531
|
|
|
26135
26532
|
${customInstructions}` : agent.instructions;
|
|
26136
26533
|
if (headers.session) markStreamActive(headers.session);
|
|
26534
|
+
const turnStartedAt = Date.now();
|
|
26137
26535
|
let result;
|
|
26138
26536
|
try {
|
|
26139
26537
|
result = await generateStream({
|
|
@@ -26175,13 +26573,7 @@ ${customInstructions}` : agent.instructions;
|
|
|
26175
26573
|
};
|
|
26176
26574
|
}
|
|
26177
26575
|
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
|
-
};
|
|
26576
|
+
return finishTurnMetadata({ totalUsage: part.totalUsage, startedAt: turnStartedAt });
|
|
26185
26577
|
}
|
|
26186
26578
|
return void 0;
|
|
26187
26579
|
},
|
|
@@ -33317,7 +33709,6 @@ ${command}`;
|
|
|
33317
33709
|
}
|
|
33318
33710
|
|
|
33319
33711
|
// ee/python/documents/processing/doc_processor.ts
|
|
33320
|
-
init_python_setup();
|
|
33321
33712
|
var import_liteparse = require("@llamaindex/liteparse");
|
|
33322
33713
|
|
|
33323
33714
|
// src/exulu/resolve-ocr.ts
|
|
@@ -33704,7 +34095,7 @@ function reconstructTableHeaders(document2, validationResults, verbose = false)
|
|
|
33704
34095
|
}
|
|
33705
34096
|
}
|
|
33706
34097
|
async function validateWithVLM(document2, model, verbose = false, concurrency = 10) {
|
|
33707
|
-
console.log(`[EXULU] Starting VLM validation for
|
|
34098
|
+
console.log(`[EXULU] Starting VLM validation for processor output, ${document2.length} pages...`);
|
|
33708
34099
|
console.log(`[EXULU] Concurrency limit: ${concurrency}`);
|
|
33709
34100
|
const limit = (0, import_p_limit.default)(concurrency);
|
|
33710
34101
|
const validationResults = /* @__PURE__ */ new Map();
|
|
@@ -33839,48 +34230,7 @@ async function processDocument(filePath, fileType, buffer, tempDir, config, verb
|
|
|
33839
34230
|
async function processPdf(buffer, paths, config, verbose = false) {
|
|
33840
34231
|
try {
|
|
33841
34232
|
let json = [];
|
|
33842
|
-
if (config?.processor.name === "
|
|
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") {
|
|
34233
|
+
if (config?.processor.name === "officeparser") {
|
|
33884
34234
|
const text = await (0, import_officeparser3.parseOfficeAsync)(buffer, {
|
|
33885
34235
|
outputErrorToConsole: false,
|
|
33886
34236
|
newlineDelimiter: "\n"
|
|
@@ -33971,14 +34321,16 @@ stderr: ${splitResult.stderr.slice(-1e3)}`
|
|
|
33971
34321
|
image: screenshots.find((s) => s.pageNum === page.pageNum)?.imagePath
|
|
33972
34322
|
}));
|
|
33973
34323
|
fs4.writeFileSync(paths.json, JSON.stringify(json, null, 2));
|
|
34324
|
+
} else {
|
|
34325
|
+
const configured = String(config?.processor?.name ?? "");
|
|
34326
|
+
throw new Error(
|
|
34327
|
+
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.' : "")
|
|
34328
|
+
);
|
|
33974
34329
|
}
|
|
33975
34330
|
console.log(`[EXULU]
|
|
33976
34331
|
\u2713 Document processing completed successfully`);
|
|
33977
34332
|
console.log(`[EXULU] Total pages: ${json.length}`);
|
|
33978
34333
|
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
34334
|
const vlmModel = config?.vlm?.model ? await resolveVlmModel(config) : void 0;
|
|
33983
34335
|
if (vlmModel && json.length > 0) {
|
|
33984
34336
|
json = await validateWithVLM(
|
|
@@ -34096,9 +34448,6 @@ async function documentProcessor({
|
|
|
34096
34448
|
} = await loadFile(file, name, tempDir);
|
|
34097
34449
|
let supportedTypes = [];
|
|
34098
34450
|
switch (config?.processor.name) {
|
|
34099
|
-
case "docling":
|
|
34100
|
-
supportedTypes = ["pdf", "docx", "doc", "txt", "md", "jpg", "jpeg", "png", "gif", "webp"];
|
|
34101
|
-
break;
|
|
34102
34451
|
case "officeparser":
|
|
34103
34452
|
supportedTypes = ["docx", "pptx", "xlsx", "odt", "odp", "ods", "pdf", "rtf", "csv", "md", "html"];
|
|
34104
34453
|
break;
|