@exulu/backend 3.7.3 → 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-T6JVFT7L.js → chunk-QMN6MVHQ.js} +6 -1
- package/dist/{chunk-BNTL6LYY.js → chunk-RBEWHG7I.js} +404 -59
- package/dist/cli/start-whisper.js +1 -1
- package/dist/{convert-exulu-tools-to-ai-sdk-tools-UQSLJDXE.js → convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js} +1 -1
- package/dist/index.cjs +957 -474
- package/dist/index.d.cts +3 -6
- package/dist/index.d.ts +3 -6
- package/dist/index.js +258 -182
- package/dist/python-setup-DRJ3QX5F.js +17 -0
- 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/invoke-skills/artifact-filter.test.ts +49 -0
- package/ee/invoke-skills/artifact-filter.ts +38 -0
- package/ee/invoke-skills/create-sandbox.ts +56 -4
- 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 +17 -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
|
@@ -1716,7 +1716,7 @@ var ExuluTool = class _ExuluTool {
|
|
|
1716
1716
|
if (!agent) {
|
|
1717
1717
|
throw new Error("Agent not found.");
|
|
1718
1718
|
}
|
|
1719
|
-
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-
|
|
1719
|
+
const { convertExuluToolsToAiSdkTools: convertExuluToolsToAiSdkTools2 } = await import("./convert-exulu-tools-to-ai-sdk-tools-6RU4IZMI.js");
|
|
1720
1720
|
const tools = await convertExuluToolsToAiSdkTools2(
|
|
1721
1721
|
[this],
|
|
1722
1722
|
[],
|
|
@@ -2068,7 +2068,19 @@ var tuningSchema = z3.object({
|
|
|
2068
2068
|
pinBoost: z3.number().min(0).max(1).default(0.15),
|
|
2069
2069
|
identifierBoost: z3.number().min(0).max(1).default(0.15),
|
|
2070
2070
|
pageWindow: z3.number().int().min(0).default(1),
|
|
2071
|
-
maxQueriesPerContext: z3.number().int().positive().default(5)
|
|
2071
|
+
maxQueriesPerContext: z3.number().int().positive().default(5),
|
|
2072
|
+
/**
|
|
2073
|
+
* Orchestration engine. "v1" is the sequential flow every agent ran before 2026-09;
|
|
2074
|
+
* "v2" merges the phase-1 LLM hops and runs identifier pins in parallel. Per agent,
|
|
2075
|
+
* so a candidate agent can run v2 while the production agent stays on v1.
|
|
2076
|
+
*/
|
|
2077
|
+
engine: z3.enum(["v1", "v2"]).default("v1"),
|
|
2078
|
+
/** v2 sub-features; each can be switched off on its own to bisect a regression. */
|
|
2079
|
+
v2: z3.object({
|
|
2080
|
+
mergedMemoryCall: z3.boolean().default(true),
|
|
2081
|
+
mergedRoutingCall: z3.boolean().default(true),
|
|
2082
|
+
parallelPins: z3.boolean().default(true)
|
|
2083
|
+
}).default({ mergedMemoryCall: true, mergedRoutingCall: true, parallelPins: true })
|
|
2072
2084
|
});
|
|
2073
2085
|
var boolVal = (v) => v === true || v === "true" || v === 1;
|
|
2074
2086
|
var strVal = (v, fallback) => typeof v === "string" && v.length > 0 ? v : fallback;
|
|
@@ -2291,6 +2303,17 @@ async function microCall(args) {
|
|
|
2291
2303
|
);
|
|
2292
2304
|
}
|
|
2293
2305
|
|
|
2306
|
+
// ee/agentic-retrieval/pipeline/timing.ts
|
|
2307
|
+
async function withTiming(sink, key, work, now = Date.now) {
|
|
2308
|
+
if (!sink) return work();
|
|
2309
|
+
const started = now();
|
|
2310
|
+
try {
|
|
2311
|
+
return await work();
|
|
2312
|
+
} finally {
|
|
2313
|
+
sink[key] = Math.max(0, now() - started);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2294
2317
|
// ee/agentic-retrieval/pipeline/prefilter.ts
|
|
2295
2318
|
import Fuse from "fuse.js";
|
|
2296
2319
|
import { z as z4 } from "zod";
|
|
@@ -2606,6 +2629,49 @@ Only set pageNumber when the user names a specific page.
|
|
|
2606
2629
|
Return hasFilenameHint/hasPageHint false (and omit the hint fields) when neither
|
|
2607
2630
|
is present. When in doubt, return false.
|
|
2608
2631
|
`;
|
|
2632
|
+
async function runMergedRoutingCall({
|
|
2633
|
+
model,
|
|
2634
|
+
question,
|
|
2635
|
+
knownIdentifiers,
|
|
2636
|
+
enabledContexts,
|
|
2637
|
+
routingRules,
|
|
2638
|
+
extraInstructions
|
|
2639
|
+
}) {
|
|
2640
|
+
const kbListing = enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n");
|
|
2641
|
+
const rulesLines = routingRules.map((r) => `- ${r.id} (${r.label}): ${r.description}`).join("\n");
|
|
2642
|
+
const system = `You analyse the user's request and answer ${routingRules.length ? "three" : "two"} questions in one go.
|
|
2643
|
+
|
|
2644
|
+
A. DOCUMENT / PAGE REFERENCE (docPage):
|
|
2645
|
+
${buildDocPagePrompt(knownIdentifiers)}
|
|
2646
|
+
|
|
2647
|
+
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:
|
|
2648
|
+
${kbListing}
|
|
2649
|
+
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.
|
|
2650
|
+
` + (routingRules.length ? `
|
|
2651
|
+
C. CLASSIFICATION (classification): classify the request into exactly one of these categories:
|
|
2652
|
+
${rulesLines}` + (extraInstructions ? `
|
|
2653
|
+
<instructions>
|
|
2654
|
+
${extraInstructions}
|
|
2655
|
+
</instructions>` : "") : "");
|
|
2656
|
+
const ids = enabledContexts.map((c) => c.id);
|
|
2657
|
+
const schema = z5.object({
|
|
2658
|
+
docPage: z5.object({
|
|
2659
|
+
hasFilenameHint: z5.boolean(),
|
|
2660
|
+
filenameHints: z5.array(z5.string()).optional(),
|
|
2661
|
+
hasPageHint: z5.boolean(),
|
|
2662
|
+
pageNumber: z5.number().int().nullable().optional()
|
|
2663
|
+
}),
|
|
2664
|
+
explicitlyRequestedKnowledgeBases: z5.array(z5.enum(ids)),
|
|
2665
|
+
...routingRules.length ? {
|
|
2666
|
+
classification: z5.object({
|
|
2667
|
+
ruleId: z5.enum(routingRules.map((r) => r.id)),
|
|
2668
|
+
reason: z5.string()
|
|
2669
|
+
})
|
|
2670
|
+
} : {}
|
|
2671
|
+
});
|
|
2672
|
+
const { output } = await microCall({ model, system, messages: [{ role: "user", content: question }], schema });
|
|
2673
|
+
return output;
|
|
2674
|
+
}
|
|
2609
2675
|
async function runRoutingPhase(opts) {
|
|
2610
2676
|
const {
|
|
2611
2677
|
question,
|
|
@@ -2615,6 +2681,7 @@ async function runRoutingPhase(opts) {
|
|
|
2615
2681
|
preselectedItems,
|
|
2616
2682
|
extraInstructions,
|
|
2617
2683
|
knownIdentifiers = [],
|
|
2684
|
+
mergedCall = false,
|
|
2618
2685
|
model
|
|
2619
2686
|
} = opts;
|
|
2620
2687
|
try {
|
|
@@ -2634,7 +2701,23 @@ async function runRoutingPhase(opts) {
|
|
|
2634
2701
|
` + enabledContexts.map((c) => `- ${c.id}: ${c.name}${c.description ? " \u2014 " + c.description : ""}`).join("\n") + `
|
|
2635
2702
|
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.
|
|
2636
2703
|
If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
2637
|
-
const
|
|
2704
|
+
const wantsClassification = routingRules.length > 0 && preselectedItems.size === 0;
|
|
2705
|
+
const merged = mergedCall ? await withTiming(
|
|
2706
|
+
opts.timings,
|
|
2707
|
+
"routing.mergedMs",
|
|
2708
|
+
() => runMergedRoutingCall({
|
|
2709
|
+
model,
|
|
2710
|
+
question,
|
|
2711
|
+
knownIdentifiers,
|
|
2712
|
+
enabledContexts,
|
|
2713
|
+
routingRules: wantsClassification ? routingRules : [],
|
|
2714
|
+
extraInstructions
|
|
2715
|
+
}).catch((err) => {
|
|
2716
|
+
console.warn("[EXULU pipeline] merged routing call failed \u2014 falling back to the v1 hops.", err);
|
|
2717
|
+
return null;
|
|
2718
|
+
})
|
|
2719
|
+
) : null;
|
|
2720
|
+
const [docPageRaw, explicitKBRaw] = merged ? [{ output: merged.docPage }, { output: { explicitlyRequestedKnowledgeBases: merged.explicitlyRequestedKnowledgeBases } }] : await withTiming(opts.timings, "routing.detectMs", () => Promise.all([
|
|
2638
2721
|
(async () => {
|
|
2639
2722
|
try {
|
|
2640
2723
|
return await microCall({
|
|
@@ -2676,7 +2759,7 @@ If explicit, return the knowledge base ids. If not, return an empty array.`;
|
|
|
2676
2759
|
return { output: { explicitlyRequestedKnowledgeBases: [] } };
|
|
2677
2760
|
}
|
|
2678
2761
|
})()
|
|
2679
|
-
]);
|
|
2762
|
+
]));
|
|
2680
2763
|
const userPinnedItemIdsByContext = /* @__PURE__ */ new Map();
|
|
2681
2764
|
let userRequestedPage = null;
|
|
2682
2765
|
if (docPageRaw.output.hasFilenameHint && docPageRaw.output.filenameHints?.length) {
|
|
@@ -2754,7 +2837,7 @@ ${extraInstructions}
|
|
|
2754
2837
|
</instructions>`;
|
|
2755
2838
|
}
|
|
2756
2839
|
try {
|
|
2757
|
-
const { output: classified } = await microCall({
|
|
2840
|
+
const { output: classified } = merged?.classification ? { output: merged.classification } : await withTiming(opts.timings, "routing.classifyMs", () => microCall({
|
|
2758
2841
|
model,
|
|
2759
2842
|
system: classifyPrompt,
|
|
2760
2843
|
messages: [{ role: "user", content: question }],
|
|
@@ -2762,7 +2845,7 @@ ${extraInstructions}
|
|
|
2762
2845
|
ruleId: z5.enum(ruleIds),
|
|
2763
2846
|
reason: z5.string()
|
|
2764
2847
|
})
|
|
2765
|
-
});
|
|
2848
|
+
}));
|
|
2766
2849
|
const matchedRule = routingRules.find((r) => r.id === classified.ruleId);
|
|
2767
2850
|
if (matchedRule) {
|
|
2768
2851
|
const main = matchedRule.main.filter((id) => enabledIds.has(id));
|
|
@@ -2929,7 +3012,8 @@ async function recallMemoryByKeywords({
|
|
|
2929
3012
|
importantKeyword,
|
|
2930
3013
|
user,
|
|
2931
3014
|
role,
|
|
2932
|
-
memoryContext
|
|
3015
|
+
memoryContext,
|
|
3016
|
+
timings
|
|
2933
3017
|
}) {
|
|
2934
3018
|
const allKeywords = [
|
|
2935
3019
|
...new Set(
|
|
@@ -2946,7 +3030,7 @@ async function recallMemoryByKeywords({
|
|
|
2946
3030
|
...new Set(allKeywords.flatMap(deriveKeywordVariants).map(stripSeparators))
|
|
2947
3031
|
].filter((v) => v.length >= 4);
|
|
2948
3032
|
if (!allVariants.length) return [];
|
|
2949
|
-
const items = await loadMemoryItems(memoryContext);
|
|
3033
|
+
const items = await withTiming(timings, "memory.keywordRecall.itemsMs", () => loadMemoryItems(memoryContext));
|
|
2950
3034
|
const scored = [];
|
|
2951
3035
|
for (const item of items) {
|
|
2952
3036
|
const haystack = stripSeparators(
|
|
@@ -2968,14 +3052,14 @@ async function recallMemoryByKeywords({
|
|
|
2968
3052
|
"[EXULU pipeline] keyword-triggered memory matches:",
|
|
2969
3053
|
topMatches.map((s) => `${s.name} (hits=${s.hits}, important=${s.importantHit})`)
|
|
2970
3054
|
);
|
|
2971
|
-
const chunks = await singleSearch({
|
|
3055
|
+
const chunks = await withTiming(timings, "memory.keywordRecall.searchMs", () => singleSearch({
|
|
2972
3056
|
query: allKeywords.join(", "),
|
|
2973
|
-
config: { method: "
|
|
3057
|
+
config: { method: "tsvector", cutoffs: void 0, limit: 50 },
|
|
2974
3058
|
user,
|
|
2975
3059
|
role,
|
|
2976
3060
|
pinnedItemIds: topMatches.map((s) => s.id),
|
|
2977
3061
|
context: memoryContext
|
|
2978
|
-
});
|
|
3062
|
+
}));
|
|
2979
3063
|
return chunks;
|
|
2980
3064
|
}
|
|
2981
3065
|
function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
@@ -2989,7 +3073,103 @@ function neutralResult(question, keywords, importantKeyword, steps = []) {
|
|
|
2989
3073
|
steps
|
|
2990
3074
|
};
|
|
2991
3075
|
}
|
|
3076
|
+
async function runMergedMemoryCall({
|
|
3077
|
+
model,
|
|
3078
|
+
retrievedMemory,
|
|
3079
|
+
question,
|
|
3080
|
+
keywords,
|
|
3081
|
+
importantKeyword,
|
|
3082
|
+
memoryConfig,
|
|
3083
|
+
glossary
|
|
3084
|
+
}) {
|
|
3085
|
+
const glossaryText = glossary.length > 0 ? `
|
|
3086
|
+
The organization's documents use the following abbreviations/terms:
|
|
3087
|
+
${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
3088
|
+
const system = `
|
|
3089
|
+
You review the shared company memory for the user's question and answer FOUR questions in one go.
|
|
3090
|
+
|
|
3091
|
+
1. RELEVANCE (relevantChunkIds): return the chunk_ids of chunks containing information relevant to the
|
|
3092
|
+
question, or an empty array. Be generous: include chunks that are topically related, share key
|
|
3093
|
+
terminology, describe the same symptom from a different angle, or could plausibly help diagnose the
|
|
3094
|
+
issue \u2014 even if they don't answer the question directly. Memory entries are deliberately broad,
|
|
3095
|
+
hand-curated hints written by domain experts; the user's wording will rarely match the memory verbatim.
|
|
3096
|
+
When in doubt, include the chunk.
|
|
3097
|
+
|
|
3098
|
+
2. OVERRIDE (override): decide whether ONE of the relevant chunks should become the AUTHORITATIVE basis
|
|
3099
|
+
of the answer, taking precedence over the official documentation even if the documents state something
|
|
3100
|
+
different. This is a deliberately STRICT check. Set overrides=true ONLY if a single chunk, on its own,
|
|
3101
|
+
contains a DIRECT and SUFFICIENT answer to exactly what the user asked. Being topically related, sharing
|
|
3102
|
+
terminology, describing the same component, or only partially addressing the question is NOT sufficient:
|
|
3103
|
+
then set overrides=false. When in doubt, set overrides=false. Memory entries may capture field experience
|
|
3104
|
+
that the manuals get wrong, so a confident, direct match is meant to win over the documents.
|
|
3105
|
+
${memoryConfig.override ? "" : "(Override is disabled for this agent: return overrides=false.)"}
|
|
3106
|
+
|
|
3107
|
+
3. FILE PRIORITIZATION (filePrioritization): set shouldPrioritizeFiles=true only if a relevant memory
|
|
3108
|
+
entry explicitly says to look in, prioritize, prefer, or always search a particular document, file, or
|
|
3109
|
+
file family (for example "When asked about X, always search in Y-Dateien first"). General background
|
|
3110
|
+
facts, glossaries, or synonyms are NOT a file prioritization instruction. When true, return
|
|
3111
|
+
fileNameHints exactly as referenced in the memory, bare names without folder paths.
|
|
3112
|
+
${memoryConfig.filePrioritization ? "" : "(File prioritization is disabled for this agent: return false.)"}
|
|
3113
|
+
|
|
3114
|
+
4. QUERY AUGMENTATION (augmentation): if, and only if, the relevant memory (or the glossary below)
|
|
3115
|
+
contains synonyms or similar terms for what the user asked, return the user question and keywords
|
|
3116
|
+
updated to include those synonyms \u2014 always keeping the original wording as well. Otherwise return the
|
|
3117
|
+
original question, keywords and important keyword unchanged.
|
|
3118
|
+
${memoryConfig.queryAugmentation ? "" : "(Query augmentation is disabled for this agent: return the originals.)"}
|
|
3119
|
+
|
|
3120
|
+
<memory_chunks>
|
|
3121
|
+
${retrievedMemory.map((chunk) => `- ${chunk.chunk_id}: ${chunk.item_name} - ${chunk.chunk_content}`).join("\n")}
|
|
3122
|
+
</memory_chunks>
|
|
3123
|
+
${glossaryText}
|
|
3124
|
+
`;
|
|
3125
|
+
const { output } = await microCall({
|
|
3126
|
+
model,
|
|
3127
|
+
system,
|
|
3128
|
+
messages: [
|
|
3129
|
+
{
|
|
3130
|
+
role: "user",
|
|
3131
|
+
content: `
|
|
3132
|
+
<user_question>${question}</user_question>
|
|
3133
|
+
<relevant_keywords>${keywords.join(", ")}</relevant_keywords>
|
|
3134
|
+
<important_keyword>${importantKeyword}</important_keyword>
|
|
3135
|
+
`
|
|
3136
|
+
}
|
|
3137
|
+
],
|
|
3138
|
+
schema: z6.object({
|
|
3139
|
+
relevantChunkIds: z6.array(z6.string()).describe("chunk_ids (UUIDs at the start of each bullet) of relevant chunks; empty array if none."),
|
|
3140
|
+
override: z6.object({
|
|
3141
|
+
overrides: z6.boolean().describe("True ONLY if a chunk directly and sufficiently answers the question."),
|
|
3142
|
+
confidence: z6.enum(["high", "medium", "low"]),
|
|
3143
|
+
authoritativeChunkIds: z6.array(z6.string()).describe("chunk_ids that directly answer the question; empty if overrides is false."),
|
|
3144
|
+
reason: z6.string().describe("One short sentence.")
|
|
3145
|
+
}),
|
|
3146
|
+
filePrioritization: z6.object({
|
|
3147
|
+
shouldPrioritizeFiles: z6.boolean(),
|
|
3148
|
+
fileNameHints: z6.array(z6.string()).optional()
|
|
3149
|
+
}),
|
|
3150
|
+
augmentation: z6.object({
|
|
3151
|
+
updatedUserQuestion: z6.string(),
|
|
3152
|
+
updatedRelevantKeywords: z6.array(z6.string()),
|
|
3153
|
+
updatedImportantKeyword: z6.string()
|
|
3154
|
+
})
|
|
3155
|
+
})
|
|
3156
|
+
});
|
|
3157
|
+
return output;
|
|
3158
|
+
}
|
|
3159
|
+
function mergedFollowups(merged, memoryConfig, hasAugmentationContent, question, importantKeyword) {
|
|
3160
|
+
const overrideResult = {
|
|
3161
|
+
output: memoryConfig.override ? merged.override : { overrides: false, confidence: "low", authoritativeChunkIds: [], reason: "" }
|
|
3162
|
+
};
|
|
3163
|
+
const fileResult = {
|
|
3164
|
+
output: memoryConfig.filePrioritization ? merged.filePrioritization : { shouldPrioritizeFiles: false, fileNameHints: [] }
|
|
3165
|
+
};
|
|
3166
|
+
const queryResult = {
|
|
3167
|
+
output: memoryConfig.queryAugmentation && hasAugmentationContent ? merged.augmentation : { updatedUserQuestion: question, updatedRelevantKeywords: [], updatedImportantKeyword: importantKeyword }
|
|
3168
|
+
};
|
|
3169
|
+
return [overrideResult, fileResult, queryResult];
|
|
3170
|
+
}
|
|
2992
3171
|
async function runMemoryPhase({
|
|
3172
|
+
timings,
|
|
2993
3173
|
memoryChunks,
|
|
2994
3174
|
memoryContext,
|
|
2995
3175
|
question,
|
|
@@ -3000,7 +3180,8 @@ async function runMemoryPhase({
|
|
|
3000
3180
|
model,
|
|
3001
3181
|
memoryConfig,
|
|
3002
3182
|
glossary,
|
|
3003
|
-
documentContexts
|
|
3183
|
+
documentContexts,
|
|
3184
|
+
mergedCall = false
|
|
3004
3185
|
}) {
|
|
3005
3186
|
try {
|
|
3006
3187
|
if (!memoryConfig.enabled || memoryChunks.length === 0 && !memoryContext) {
|
|
@@ -3010,13 +3191,14 @@ async function runMemoryPhase({
|
|
|
3010
3191
|
let retrieved_memory = [...memoryChunks];
|
|
3011
3192
|
if (memoryContext) {
|
|
3012
3193
|
try {
|
|
3013
|
-
const keywordMatched = await recallMemoryByKeywords({
|
|
3194
|
+
const keywordMatched = await withTiming(timings, "memory.keywordRecallMs", () => recallMemoryByKeywords({
|
|
3014
3195
|
keywords,
|
|
3015
3196
|
importantKeyword,
|
|
3016
3197
|
user,
|
|
3017
3198
|
role,
|
|
3018
|
-
memoryContext
|
|
3019
|
-
|
|
3199
|
+
memoryContext,
|
|
3200
|
+
timings
|
|
3201
|
+
}));
|
|
3020
3202
|
if (keywordMatched.length > 0) {
|
|
3021
3203
|
const seen = new Set(retrieved_memory.map((c) => c.chunk_id));
|
|
3022
3204
|
const additions = keywordMatched.filter((c) => !seen.has(c.chunk_id));
|
|
@@ -3041,8 +3223,24 @@ async function runMemoryPhase({
|
|
|
3041
3223
|
</memory_chunks>
|
|
3042
3224
|
`;
|
|
3043
3225
|
let relevantMemoryChunks = [];
|
|
3226
|
+
let mergedOutput;
|
|
3044
3227
|
try {
|
|
3045
|
-
|
|
3228
|
+
if (mergedCall) {
|
|
3229
|
+
mergedOutput = await withTiming(
|
|
3230
|
+
timings,
|
|
3231
|
+
"memory.mergedMs",
|
|
3232
|
+
() => runMergedMemoryCall({
|
|
3233
|
+
model,
|
|
3234
|
+
retrievedMemory: retrieved_memory,
|
|
3235
|
+
question,
|
|
3236
|
+
keywords,
|
|
3237
|
+
importantKeyword,
|
|
3238
|
+
memoryConfig,
|
|
3239
|
+
glossary
|
|
3240
|
+
})
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
const { output: output_relevant_memory } = mergedOutput ? { output: { relevantChunkIds: mergedOutput.relevantChunkIds } } : await withTiming(timings, "memory.relevanceMs", () => microCall({
|
|
3046
3244
|
model,
|
|
3047
3245
|
system: CHECK_MEMORIES_FOR_RELEVANT_INFORMATION,
|
|
3048
3246
|
messages: [
|
|
@@ -3060,7 +3258,7 @@ async function runMemoryPhase({
|
|
|
3060
3258
|
"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."
|
|
3061
3259
|
)
|
|
3062
3260
|
})
|
|
3063
|
-
});
|
|
3261
|
+
}));
|
|
3064
3262
|
const ids = new Set(output_relevant_memory?.relevantChunkIds ?? []);
|
|
3065
3263
|
relevantMemoryChunks = ids.size === 0 ? [] : retrieved_memory.filter((c) => ids.has(c.chunk_id));
|
|
3066
3264
|
} catch (e) {
|
|
@@ -3150,7 +3348,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3150
3348
|
|
|
3151
3349
|
Otherwise, return the original user question, relevant keywords and important keyword.
|
|
3152
3350
|
`;
|
|
3153
|
-
const [overrideResult, fileResult, queryResult] = await Promise.all([
|
|
3351
|
+
const [overrideResult, fileResult, queryResult] = mergedOutput ? mergedFollowups(mergedOutput, memoryConfig, hasAugmentationContent, question, importantKeyword) : await withTiming(timings, "memory.followupsMs", () => Promise.all([
|
|
3154
3352
|
// Override check: strict gate to decide if memory should be authoritative
|
|
3155
3353
|
memoryConfig.override ? microCall({
|
|
3156
3354
|
model,
|
|
@@ -3231,7 +3429,7 @@ ${glossary.map((g) => `${g.term} : ${g.meaning}`).join("\n")}` : "";
|
|
|
3231
3429
|
updatedImportantKeyword: importantKeyword
|
|
3232
3430
|
}
|
|
3233
3431
|
})
|
|
3234
|
-
]);
|
|
3432
|
+
]));
|
|
3235
3433
|
const overrideIds = new Set(overrideResult.output?.authoritativeChunkIds ?? []);
|
|
3236
3434
|
const authoritativeChunks = memoryChunksForAnswer.filter(
|
|
3237
3435
|
(c) => c.chunk_id && overrideIds.has(c.chunk_id)
|
|
@@ -3407,7 +3605,7 @@ async function searchContexts(opts) {
|
|
|
3407
3605
|
skipPrefilter
|
|
3408
3606
|
} = opts;
|
|
3409
3607
|
const chunkArrays = await Promise.all(
|
|
3410
|
-
contextIds.map(async (
|
|
3608
|
+
contextIds.map((ctxId) => withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}Ms`, async () => {
|
|
3411
3609
|
try {
|
|
3412
3610
|
const ctx = contextsById.get(ctxId);
|
|
3413
3611
|
if (!ctx) return [];
|
|
@@ -3445,13 +3643,13 @@ async function searchContexts(opts) {
|
|
|
3445
3643
|
if (multiQuery) {
|
|
3446
3644
|
let hydePassage = null;
|
|
3447
3645
|
if (hyde) {
|
|
3448
|
-
hydePassage = await generateHydePassage({
|
|
3646
|
+
hydePassage = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.hydeMs`, () => generateHydePassage({
|
|
3449
3647
|
originalQuestion: question,
|
|
3450
3648
|
relevantKeywords: keywords,
|
|
3451
3649
|
importantKeyword,
|
|
3452
3650
|
styleHint,
|
|
3453
3651
|
model
|
|
3454
|
-
});
|
|
3652
|
+
}));
|
|
3455
3653
|
}
|
|
3456
3654
|
const candidates = [
|
|
3457
3655
|
question,
|
|
@@ -3466,14 +3664,14 @@ async function searchContexts(opts) {
|
|
|
3466
3664
|
}
|
|
3467
3665
|
if (kind === "conversations") {
|
|
3468
3666
|
if (keywordPrefilter && pinnedItemIds.length === 0) {
|
|
3469
|
-
const prefiltered = await fuzzyPrefilter({
|
|
3667
|
+
const prefiltered = await withTiming(opts.timings, `${opts.timingPrefix ?? "search"}.${ctxId}.prefilterMs`, () => fuzzyPrefilter({
|
|
3470
3668
|
cacheKey: `conversations:${ctxId}`,
|
|
3471
3669
|
relevantKeywords: keywords,
|
|
3472
3670
|
importantKeyword,
|
|
3473
3671
|
context: ctx,
|
|
3474
3672
|
fields: ["name", "id", "external_id", "description"],
|
|
3475
3673
|
normalize: (i) => [i.name, i.description].filter(Boolean).join(": ")
|
|
3476
|
-
});
|
|
3674
|
+
}));
|
|
3477
3675
|
pinnedItemIds = prefiltered.map((r) => r.id);
|
|
3478
3676
|
}
|
|
3479
3677
|
const keywordQuery = keywords.length ? keywords.join(" ") + " " + importantKeyword : question;
|
|
@@ -3488,7 +3686,7 @@ async function searchContexts(opts) {
|
|
|
3488
3686
|
console.warn(`[EXULU pipeline] searchContexts failed for context "${ctxId}":`, err);
|
|
3489
3687
|
return [];
|
|
3490
3688
|
}
|
|
3491
|
-
})
|
|
3689
|
+
}))
|
|
3492
3690
|
);
|
|
3493
3691
|
return { chunks: chunkArrays.flat() };
|
|
3494
3692
|
}
|
|
@@ -3633,6 +3831,27 @@ async function rerankResults(opts) {
|
|
|
3633
3831
|
return { limited_results, sorted_reranked_results: sorted, rerank_score_max_genuine };
|
|
3634
3832
|
}
|
|
3635
3833
|
|
|
3834
|
+
// ee/agentic-retrieval/pipeline/pin-rerun.ts
|
|
3835
|
+
function needsPinRerun(originalQuestion, updatedQuestion) {
|
|
3836
|
+
if (originalQuestion === updatedQuestion) return false;
|
|
3837
|
+
const before = designations(originalQuestion);
|
|
3838
|
+
for (const token of designations(updatedQuestion)) {
|
|
3839
|
+
if (!before.has(token)) return true;
|
|
3840
|
+
}
|
|
3841
|
+
return false;
|
|
3842
|
+
}
|
|
3843
|
+
function designations(text) {
|
|
3844
|
+
const out = /* @__PURE__ */ new Set();
|
|
3845
|
+
for (const raw of text.split(/[\s,;:()\[\]"'?!]+/)) {
|
|
3846
|
+
const token = raw.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
|
|
3847
|
+
if (!token) continue;
|
|
3848
|
+
const hasDigit = /\p{N}/u.test(token);
|
|
3849
|
+
const capitals = (token.match(/\p{Lu}/gu) ?? []).length;
|
|
3850
|
+
if (hasDigit || capitals >= 2) out.add(token.toLowerCase());
|
|
3851
|
+
}
|
|
3852
|
+
return out;
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3636
3855
|
// ee/agentic-retrieval/pipeline/index.ts
|
|
3637
3856
|
function addChunks(result, chunks) {
|
|
3638
3857
|
const seen = new Set(result.chunks.map((c) => c.chunk_id));
|
|
@@ -3798,6 +4017,14 @@ function createAgenticRetrievalTool(opts) {
|
|
|
3798
4017
|
usage: [],
|
|
3799
4018
|
totalTokens: 0
|
|
3800
4019
|
};
|
|
4020
|
+
const t0 = Date.now();
|
|
4021
|
+
const timings = {};
|
|
4022
|
+
let tPhase = t0;
|
|
4023
|
+
const lap = (name) => {
|
|
4024
|
+
const now = Date.now();
|
|
4025
|
+
timings[name] = (timings[name] ?? 0) + (now - tPhase);
|
|
4026
|
+
tPhase = now;
|
|
4027
|
+
};
|
|
3801
4028
|
try {
|
|
3802
4029
|
let enabledContexts = contexts.filter(
|
|
3803
4030
|
(ctx) => cfg.knowledgeBases[ctx.id]?.enabled !== false
|
|
@@ -3870,8 +4097,19 @@ function createAgenticRetrievalTool(opts) {
|
|
|
3870
4097
|
resolvedProject && projectScope?.customInstructions ? `Instructions for the attached project "${projectScope.name}":
|
|
3871
4098
|
${projectScope.customInstructions}` : ""
|
|
3872
4099
|
].filter(Boolean).join("\n");
|
|
3873
|
-
const
|
|
3874
|
-
|
|
4100
|
+
const engineV2 = cfg.tuning.engine === "v2";
|
|
4101
|
+
const v2 = cfg.tuning.v2;
|
|
4102
|
+
const pinsFor = (question) => resolveIdentifierPins({
|
|
4103
|
+
question,
|
|
4104
|
+
identifierSets: cfg.vocabulary.identifiers,
|
|
4105
|
+
contextsById,
|
|
4106
|
+
kbKindById,
|
|
4107
|
+
model: utilityModel
|
|
4108
|
+
});
|
|
4109
|
+
const [memResult, routResult, parallelPins] = await Promise.all([
|
|
4110
|
+
withTiming(timings, "memoryMs", () => runMemoryPhase({
|
|
4111
|
+
timings,
|
|
4112
|
+
mergedCall: engineV2 && v2.mergedMemoryCall,
|
|
3875
4113
|
memoryChunks: memoryItems ?? [],
|
|
3876
4114
|
memoryContext,
|
|
3877
4115
|
question: userQuery,
|
|
@@ -3883,8 +4121,10 @@ ${projectScope.customInstructions}` : ""
|
|
|
3883
4121
|
memoryConfig: cfg.memory,
|
|
3884
4122
|
glossary: cfg.vocabulary.glossary,
|
|
3885
4123
|
documentContexts
|
|
3886
|
-
}),
|
|
3887
|
-
runRoutingPhase({
|
|
4124
|
+
})),
|
|
4125
|
+
withTiming(timings, "routingMs", () => runRoutingPhase({
|
|
4126
|
+
timings,
|
|
4127
|
+
mergedCall: engineV2 && v2.mergedRoutingCall,
|
|
3888
4128
|
question: userQuery,
|
|
3889
4129
|
enabledContexts,
|
|
3890
4130
|
documentContexts,
|
|
@@ -3895,8 +4135,12 @@ ${projectScope.customInstructions}` : ""
|
|
|
3895
4135
|
// detector must never treat these as filename hints.
|
|
3896
4136
|
knownIdentifiers: cfg.vocabulary.identifiers.flatMap((i) => i.examples),
|
|
3897
4137
|
model: utilityModel
|
|
3898
|
-
})
|
|
4138
|
+
})),
|
|
4139
|
+
// engine v2: identifier pins depend only on the question, so they run alongside
|
|
4140
|
+
// memory and routing instead of after them (re-run below if memory rewrote the question).
|
|
4141
|
+
engineV2 && v2.parallelPins ? withTiming(timings, "pinsParallelMs", () => pinsFor(userQuery)) : Promise.resolve(null)
|
|
3899
4142
|
]);
|
|
4143
|
+
lap("memoryRoutingMs");
|
|
3900
4144
|
for (const step of [...memResult.steps, ...routResult.steps]) {
|
|
3901
4145
|
result.steps.push({
|
|
3902
4146
|
stepNumber: 1,
|
|
@@ -3946,13 +4190,8 @@ ${projectScope.customInstructions}` : ""
|
|
|
3946
4190
|
memoryPinnedItemIdsByContext,
|
|
3947
4191
|
memoryOverride
|
|
3948
4192
|
} = memResult;
|
|
3949
|
-
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = await
|
|
3950
|
-
|
|
3951
|
-
identifierSets: cfg.vocabulary.identifiers,
|
|
3952
|
-
contextsById,
|
|
3953
|
-
kbKindById,
|
|
3954
|
-
model: utilityModel
|
|
3955
|
-
});
|
|
4193
|
+
const { pinsByContext: identifierPinsByContext, exactPinsByContext, steps: pinSteps } = parallelPins && !needsPinRerun(userQuery, updatedQuestion) ? parallelPins : await pinsFor(updatedQuestion);
|
|
4194
|
+
lap("pinsMs");
|
|
3956
4195
|
for (const step of pinSteps) {
|
|
3957
4196
|
result.steps.push({
|
|
3958
4197
|
stepNumber: 1,
|
|
@@ -3982,6 +4221,8 @@ ${projectScope.customInstructions}` : ""
|
|
|
3982
4221
|
rewrites: cfg.vocabulary.rewrites,
|
|
3983
4222
|
styleHint: cfg.vocabulary.styleHint,
|
|
3984
4223
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
4224
|
+
timings,
|
|
4225
|
+
timingPrefix: "search.main",
|
|
3985
4226
|
skipPrefilter: false
|
|
3986
4227
|
}),
|
|
3987
4228
|
fallbackContextsToSearch.length > 0 && !hasExplicitDocAndPage ? searchContexts({
|
|
@@ -4002,9 +4243,12 @@ ${projectScope.customInstructions}` : ""
|
|
|
4002
4243
|
rewrites: cfg.vocabulary.rewrites,
|
|
4003
4244
|
styleHint: cfg.vocabulary.styleHint,
|
|
4004
4245
|
maxQueries: cfg.tuning.maxQueriesPerContext,
|
|
4246
|
+
timings,
|
|
4247
|
+
timingPrefix: "search.fallback",
|
|
4005
4248
|
skipPrefilter: true
|
|
4006
4249
|
}) : Promise.resolve({ chunks: [] })
|
|
4007
4250
|
]);
|
|
4251
|
+
lap("searchMs");
|
|
4008
4252
|
const pinnedItemIds = /* @__PURE__ */ new Set([
|
|
4009
4253
|
...(function* () {
|
|
4010
4254
|
for (const s of memoryPinnedItemIdsByContext.values()) yield* s;
|
|
@@ -4067,6 +4311,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
4067
4311
|
chunks: [],
|
|
4068
4312
|
tokens: 0
|
|
4069
4313
|
});
|
|
4314
|
+
lap("rerankMs");
|
|
4070
4315
|
addChunks(result, mainRerank.limited_results);
|
|
4071
4316
|
yield { result: serializeOutput(result) };
|
|
4072
4317
|
const literalLookupSatisfied = hasExplicitDocAndPage && mainRerank.limited_results.length > 0 && mainRerank.limited_results.some((r) => {
|
|
@@ -4115,6 +4360,7 @@ ${projectScope.customInstructions}` : ""
|
|
|
4115
4360
|
});
|
|
4116
4361
|
result.reasoning.push({ text: "Fallback results reranked", tools: [] });
|
|
4117
4362
|
addChunks(result, fallbackRerank.limited_results);
|
|
4363
|
+
lap("fallbackRerankMs");
|
|
4118
4364
|
yield { result: serializeOutput(result) };
|
|
4119
4365
|
}
|
|
4120
4366
|
if (memoryOverride.active) {
|
|
@@ -4137,9 +4383,19 @@ Verified answer:
|
|
|
4137
4383
|
addChunks(result, memoryOverride.chunks);
|
|
4138
4384
|
yield { result: serializeOutput(result) };
|
|
4139
4385
|
}
|
|
4386
|
+
timings.totalMs = Date.now() - t0;
|
|
4387
|
+
result.timings = timings;
|
|
4388
|
+
result.steps.push({
|
|
4389
|
+
stepNumber: 1,
|
|
4390
|
+
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`,
|
|
4391
|
+
toolCalls: [],
|
|
4392
|
+
chunks: [],
|
|
4393
|
+
tokens: 0
|
|
4394
|
+
});
|
|
4140
4395
|
if (cfg.logging) {
|
|
4141
|
-
console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length }));
|
|
4396
|
+
console.log("[EXULU pipeline] final result:", JSON.stringify({ steps: result.steps.length, chunks: result.chunks.length, timings }));
|
|
4142
4397
|
}
|
|
4398
|
+
yield { result: serializeOutput(result) };
|
|
4143
4399
|
return { result: serializeOutput(result) };
|
|
4144
4400
|
} catch (err) {
|
|
4145
4401
|
console.warn("[EXULU pipeline] retrieval pipeline failed:", err);
|
|
@@ -5723,6 +5979,33 @@ var createUppyRoutes = async (app, config) => {
|
|
|
5723
5979
|
return app;
|
|
5724
5980
|
};
|
|
5725
5981
|
|
|
5982
|
+
// ee/invoke-skills/artifact-filter.ts
|
|
5983
|
+
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
5984
|
+
"venv",
|
|
5985
|
+
".venv",
|
|
5986
|
+
"env",
|
|
5987
|
+
"node_modules",
|
|
5988
|
+
"__pycache__",
|
|
5989
|
+
"site-packages",
|
|
5990
|
+
"dist-packages",
|
|
5991
|
+
".cache",
|
|
5992
|
+
".git",
|
|
5993
|
+
".pytest_cache",
|
|
5994
|
+
".mypy_cache",
|
|
5995
|
+
".ipynb_checkpoints"
|
|
5996
|
+
]);
|
|
5997
|
+
function isIgnoredArtifactPath(relativePath) {
|
|
5998
|
+
return relativePath.split(/[\\/]+/).some((segment) => IGNORED_SEGMENTS.has(segment));
|
|
5999
|
+
}
|
|
6000
|
+
var DEFAULT_ARTIFACT_CAP = 50;
|
|
6001
|
+
function capArtifacts(artifacts, max = DEFAULT_ARTIFACT_CAP) {
|
|
6002
|
+
if (artifacts.length <= max) return { kept: artifacts, omitted: 0 };
|
|
6003
|
+
return { kept: artifacts.slice(0, max), omitted: artifacts.length - max };
|
|
6004
|
+
}
|
|
6005
|
+
function needsDownload(localSize, remoteSize) {
|
|
6006
|
+
return localSize === void 0 || localSize !== remoteSize;
|
|
6007
|
+
}
|
|
6008
|
+
|
|
5726
6009
|
// src/exulu/system-dependencies.ts
|
|
5727
6010
|
import { exec } from "child_process";
|
|
5728
6011
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -5959,7 +6242,7 @@ function resolveSessionPath(inputPath, sessionDir) {
|
|
|
5959
6242
|
}
|
|
5960
6243
|
return resolved;
|
|
5961
6244
|
}
|
|
5962
|
-
async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
6245
|
+
async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config, opts = {}) {
|
|
5963
6246
|
const userPrefix = `user_${userId}/sessions/${sessionId}/`;
|
|
5964
6247
|
let objects;
|
|
5965
6248
|
try {
|
|
@@ -5979,7 +6262,17 @@ async function restoreArtifactsFromS3(sessionDir, sessionId, userId, config) {
|
|
|
5979
6262
|
const idx = obj.key.indexOf(userPrefix);
|
|
5980
6263
|
const relativePath = idx >= 0 ? obj.key.slice(idx + userPrefix.length) : "";
|
|
5981
6264
|
if (!relativePath) continue;
|
|
6265
|
+
if (isIgnoredArtifactPath(relativePath)) continue;
|
|
5982
6266
|
const localPath = join2(sessionDir, relativePath);
|
|
6267
|
+
if (opts.onlyMissing) {
|
|
6268
|
+
let localSize;
|
|
6269
|
+
try {
|
|
6270
|
+
localSize = (await stat(localPath)).size;
|
|
6271
|
+
} catch {
|
|
6272
|
+
localSize = void 0;
|
|
6273
|
+
}
|
|
6274
|
+
if (!needsDownload(localSize, obj.size)) continue;
|
|
6275
|
+
}
|
|
5983
6276
|
try {
|
|
5984
6277
|
const bytes = await getS3ObjectBytes(obj.key, config);
|
|
5985
6278
|
await mkdir(dirname(localPath), { recursive: true });
|
|
@@ -6013,6 +6306,15 @@ async function downloadKeyIntoSandbox(opts) {
|
|
|
6013
6306
|
await writeFile(localPath, bytes);
|
|
6014
6307
|
return { written: true, localPath };
|
|
6015
6308
|
}
|
|
6309
|
+
async function resolvePythonVenvPath() {
|
|
6310
|
+
try {
|
|
6311
|
+
const { getPythonVenvPath } = await import("./python-setup-DRJ3QX5F.js");
|
|
6312
|
+
return getPythonVenvPath();
|
|
6313
|
+
} catch (err) {
|
|
6314
|
+
console.warn("[SKILLS] Could not resolve the Python venv for the session sandbox; skill scripts fall back to the system python.", err);
|
|
6315
|
+
return void 0;
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
6016
6318
|
async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
6017
6319
|
const cached = sandboxCache.get(sessionId);
|
|
6018
6320
|
if (cached) {
|
|
@@ -6026,6 +6328,13 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6026
6328
|
await downloadSkill(skill, skillsDirectory2, config);
|
|
6027
6329
|
cached.installedSkills.set(skill.id, skill.current_version);
|
|
6028
6330
|
}
|
|
6331
|
+
if (userId && config.fileUploads) {
|
|
6332
|
+
try {
|
|
6333
|
+
await restoreArtifactsFromS3(cached.handle.sessionDir, sessionId, userId, config, { onlyMissing: true });
|
|
6334
|
+
} catch (err) {
|
|
6335
|
+
console.error(`[SKILLS] Failed to re-sync S3 session files for session ${sessionId}; continuing.`, err);
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6029
6338
|
return cached.handle;
|
|
6030
6339
|
}
|
|
6031
6340
|
const sessionDir = join2("/tmp", "exulu-sessions", sessionId);
|
|
@@ -6043,8 +6352,8 @@ async function createSessionSandbox(sessionId, skills, config, userId) {
|
|
|
6043
6352
|
`[SKILLS] S3 artifact persistence disabled for session ${sessionId} (userId=${userId ?? "missing"}, fileUploads=${config.fileUploads ? "configured" : "missing"})`
|
|
6044
6353
|
);
|
|
6045
6354
|
}
|
|
6046
|
-
if (userId && config.fileUploads
|
|
6047
|
-
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config);
|
|
6355
|
+
if (userId && config.fileUploads) {
|
|
6356
|
+
await restoreArtifactsFromS3(sessionDir, sessionId, userId, config, { onlyMissing: dirExisted });
|
|
6048
6357
|
}
|
|
6049
6358
|
const probe = await probeSandboxSupport();
|
|
6050
6359
|
const useDirectExec = !probe.canSandbox;
|
|
@@ -6079,6 +6388,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
6079
6388
|
await SandboxManager.initialize(baselineSandboxConfig);
|
|
6080
6389
|
}
|
|
6081
6390
|
const npmGlobalRoot = await getNpmGlobalRoot();
|
|
6391
|
+
const pythonVenvPath = await resolvePythonVenvPath();
|
|
6082
6392
|
const sessionSandboxConfig = {
|
|
6083
6393
|
network: {
|
|
6084
6394
|
allowedDomains: [],
|
|
@@ -6093,7 +6403,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
6093
6403
|
// Allow Node to read globally-installed packages from inside
|
|
6094
6404
|
// the sandbox. Without this, `require('docx')` fails with
|
|
6095
6405
|
// EPERM even when NODE_PATH points the resolver here.
|
|
6096
|
-
...npmGlobalRoot ? [npmGlobalRoot] : []
|
|
6406
|
+
...npmGlobalRoot ? [npmGlobalRoot] : [],
|
|
6407
|
+
...pythonVenvPath ? [pythonVenvPath] : []
|
|
6097
6408
|
],
|
|
6098
6409
|
allowWrite: [sessionDir],
|
|
6099
6410
|
denyWrite: []
|
|
@@ -6111,7 +6422,8 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
6111
6422
|
const sandboxedExecEnv = {
|
|
6112
6423
|
...configuredVariables,
|
|
6113
6424
|
...process.env,
|
|
6114
|
-
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {}
|
|
6425
|
+
...npmGlobalRoot ? { NODE_PATH: npmGlobalRoot } : {},
|
|
6426
|
+
...pythonVenvPath ? { PATH: `${join2(pythonVenvPath, "bin")}:${process.env.PATH ?? ""}`, VIRTUAL_ENV: pythonVenvPath } : {}
|
|
6115
6427
|
};
|
|
6116
6428
|
const wrapIfNeeded = async (command) => {
|
|
6117
6429
|
if (useDirectExec) return command;
|
|
@@ -6236,6 +6548,7 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
6236
6548
|
for (const entry of entries) {
|
|
6237
6549
|
const full = join2(dir, entry.name);
|
|
6238
6550
|
if (full === skillsDir) continue;
|
|
6551
|
+
if (isIgnoredArtifactPath(relative(sessionDir, full))) continue;
|
|
6239
6552
|
if (entry.isDirectory()) {
|
|
6240
6553
|
await walk(full);
|
|
6241
6554
|
} else if (entry.isFile()) {
|
|
@@ -6331,19 +6644,24 @@ Probe error: ${probe.reason ?? "(no detail)"}`
|
|
|
6331
6644
|
}
|
|
6332
6645
|
}
|
|
6333
6646
|
let stdout = result?.stdout ?? "";
|
|
6334
|
-
const
|
|
6647
|
+
const { kept, omitted } = capArtifacts(artifacts);
|
|
6648
|
+
const withUrls = kept.filter((a) => a.url);
|
|
6335
6649
|
if (withUrls.length > 0) {
|
|
6336
6650
|
const lines = ["", "[exulu-artifacts]"];
|
|
6337
6651
|
for (const a of withUrls) {
|
|
6338
6652
|
lines.push(` ${a.relativePath}: ${a.url}`);
|
|
6339
6653
|
}
|
|
6654
|
+
if (omitted > 0) {
|
|
6655
|
+
lines.push(` \u2026 ${omitted} more file(s) were created and mirrored but are not listed here.`);
|
|
6656
|
+
}
|
|
6340
6657
|
stdout = `${stdout}
|
|
6341
6658
|
${lines.join("\n")}`;
|
|
6342
6659
|
}
|
|
6343
6660
|
return {
|
|
6344
6661
|
...result,
|
|
6345
6662
|
stdout,
|
|
6346
|
-
artifacts
|
|
6663
|
+
artifacts: kept,
|
|
6664
|
+
...omitted > 0 ? { artifactsOmitted: omitted } : {}
|
|
6347
6665
|
};
|
|
6348
6666
|
}
|
|
6349
6667
|
});
|
|
@@ -6561,12 +6879,21 @@ var buildAuthToolModelOutput = (tool3) => ({ output }) => {
|
|
|
6561
6879
|
|
|
6562
6880
|
// src/templates/tools/session-file-read-tool.ts
|
|
6563
6881
|
import { z as z12 } from "zod";
|
|
6882
|
+
|
|
6883
|
+
// src/exulu/session-files.ts
|
|
6884
|
+
function sessionFilePrefix(ownerId, sessionId, s3prefix) {
|
|
6885
|
+
const general = s3prefix ? `${s3prefix.replace(/\/+$/, "")}/` : "";
|
|
6886
|
+
return `${general}user_${ownerId}/sessions/${sessionId}/`;
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6889
|
+
// src/templates/tools/session-file-read-tool.ts
|
|
6564
6890
|
var DEFAULT_LIMIT = 250;
|
|
6565
6891
|
var MAX_CONTENT_CHARS = 16e3;
|
|
6566
6892
|
var createSessionFileReadTool = ({
|
|
6567
6893
|
sessionID,
|
|
6568
6894
|
user,
|
|
6569
|
-
exuluConfig
|
|
6895
|
+
exuluConfig,
|
|
6896
|
+
ownerId
|
|
6570
6897
|
}) => {
|
|
6571
6898
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
6572
6899
|
const readSessionFileExecute = async ({ filename, offset, limit }) => {
|
|
@@ -6577,8 +6904,7 @@ var createSessionFileReadTool = ({
|
|
|
6577
6904
|
};
|
|
6578
6905
|
}
|
|
6579
6906
|
const uploads = exuluConfig.fileUploads;
|
|
6580
|
-
const
|
|
6581
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
6907
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
6582
6908
|
try {
|
|
6583
6909
|
const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
|
|
6584
6910
|
const res = await fetch(url);
|
|
@@ -6681,6 +7007,18 @@ async function renderPdfPageToPng(pdf, page, scaleTo) {
|
|
|
6681
7007
|
var DEFAULT_LIMIT2 = 250;
|
|
6682
7008
|
var MAX_CONTENT_CHARS2 = 16e3;
|
|
6683
7009
|
var MIN_CHARS_PER_PAGE = 20;
|
|
7010
|
+
function looksLikeGarbledTextLayer(text) {
|
|
7011
|
+
let control = 0;
|
|
7012
|
+
let visible = 0;
|
|
7013
|
+
for (const ch of text) {
|
|
7014
|
+
const code = ch.charCodeAt(0);
|
|
7015
|
+
if (code === 9 || code === 10 || code === 12 || code === 13 || code === 32) continue;
|
|
7016
|
+
visible++;
|
|
7017
|
+
if (code < 32 || code === 127) control++;
|
|
7018
|
+
}
|
|
7019
|
+
if (visible < 40) return false;
|
|
7020
|
+
return control / visible > 0.01;
|
|
7021
|
+
}
|
|
6684
7022
|
var OFFICE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6685
7023
|
".docx",
|
|
6686
7024
|
".doc",
|
|
@@ -6697,7 +7035,8 @@ var pagesPattern = /^(\d+)(?:-(\d+))?$/;
|
|
|
6697
7035
|
var createParseDocumentTool = ({
|
|
6698
7036
|
sessionID,
|
|
6699
7037
|
user,
|
|
6700
|
-
exuluConfig
|
|
7038
|
+
exuluConfig,
|
|
7039
|
+
ownerId
|
|
6701
7040
|
}) => {
|
|
6702
7041
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
6703
7042
|
const parseDocumentExecute = async ({
|
|
@@ -6722,8 +7061,7 @@ var createParseDocumentTool = ({
|
|
|
6722
7061
|
return { error: `The pages option is only supported for PDF files \u2014 "${ext}" documents are extracted whole.` };
|
|
6723
7062
|
}
|
|
6724
7063
|
const uploads = exuluConfig.fileUploads;
|
|
6725
|
-
const
|
|
6726
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
7064
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
6727
7065
|
try {
|
|
6728
7066
|
const url = await getPresignedUrl(uploads.s3Bucket, key, exuluConfig);
|
|
6729
7067
|
const res = await fetch(url);
|
|
@@ -6738,6 +7076,11 @@ var createParseDocumentTool = ({
|
|
|
6738
7076
|
const pageTexts = raw.replace(/\f$/, "").split("\f");
|
|
6739
7077
|
totalPages = pageTexts.length;
|
|
6740
7078
|
const nonWhitespace = raw.replace(/\s/g, "").length;
|
|
7079
|
+
if (looksLikeGarbledTextLayer(raw)) {
|
|
7080
|
+
return {
|
|
7081
|
+
error: `"${safeName}" has a text layer that is unreadable (its font encoding maps glyphs to the wrong characters, so words and especially numbers come out wrong or vanish). Do not use extracted text from this file. Use view_document_page to read the pages visually, or suggest the user add the document to a knowledge base with a document processor for full OCR.`
|
|
7082
|
+
};
|
|
7083
|
+
}
|
|
6741
7084
|
if (nonWhitespace < totalPages * MIN_CHARS_PER_PAGE) {
|
|
6742
7085
|
return {
|
|
6743
7086
|
error: `"${safeName}" has no extractable text layer (likely a scan or image-based PDF). Use view_document_page to look at pages visually, or suggest the user add the document to a knowledge base with a document processor for full OCR.`
|
|
@@ -6985,7 +7328,8 @@ var OFFICE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
|
6985
7328
|
var createViewDocumentPageTool = ({
|
|
6986
7329
|
sessionID,
|
|
6987
7330
|
user,
|
|
6988
|
-
exuluConfig
|
|
7331
|
+
exuluConfig,
|
|
7332
|
+
ownerId
|
|
6989
7333
|
}) => {
|
|
6990
7334
|
if (!sessionID || !exuluConfig?.fileUploads?.s3Bucket) return void 0;
|
|
6991
7335
|
const viewDocumentPageExecute = async ({ filename, page, model }, options) => {
|
|
@@ -7018,8 +7362,7 @@ var createViewDocumentPageTool = ({
|
|
|
7018
7362
|
}
|
|
7019
7363
|
}
|
|
7020
7364
|
const uploads = exuluConfig.fileUploads;
|
|
7021
|
-
const
|
|
7022
|
-
const key = `${generalPrefix}user_${user?.id ?? "api"}/sessions/${sessionID}/${safeName}`;
|
|
7365
|
+
const key = `${sessionFilePrefix(ownerId ?? user?.id ?? "api", sessionID, uploads.s3prefix)}${safeName}`;
|
|
7023
7366
|
const pageNumber = page ?? 1;
|
|
7024
7367
|
try {
|
|
7025
7368
|
let imageBytes;
|
|
@@ -7704,7 +8047,7 @@ var hydrateVariables = async (tool3) => {
|
|
|
7704
8047
|
await Promise.all(promises);
|
|
7705
8048
|
return tool3;
|
|
7706
8049
|
};
|
|
7707
|
-
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools) => {
|
|
8050
|
+
var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approvedTools, allExuluTools, configs, contexts, user, exuluConfig, sessionID, req, project, sessionItems, model, agent, memoryItems, contextWindow, disabledTools, sessionOwnerId) => {
|
|
7708
8051
|
if (!currentTools) return {};
|
|
7709
8052
|
if (!allExuluTools) {
|
|
7710
8053
|
allExuluTools = [];
|
|
@@ -7721,7 +8064,7 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
7721
8064
|
sessionID,
|
|
7722
8065
|
currentSkills || [],
|
|
7723
8066
|
exuluConfig,
|
|
7724
|
-
user?.id
|
|
8067
|
+
sessionOwnerId ?? user?.id
|
|
7725
8068
|
);
|
|
7726
8069
|
} catch (err) {
|
|
7727
8070
|
console.error(
|
|
@@ -7786,15 +8129,15 @@ var convertExuluToolsToAiSdkTools = async (currentTools, currentSkills, approved
|
|
|
7786
8129
|
currentTools.push(sessionItemsRetrievalTool);
|
|
7787
8130
|
}
|
|
7788
8131
|
}
|
|
7789
|
-
const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig });
|
|
8132
|
+
const sessionFileReadTool = createSessionFileReadTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
7790
8133
|
if (sessionFileReadTool && !disabled.has(sessionFileReadTool.id)) {
|
|
7791
8134
|
currentTools.push(sessionFileReadTool);
|
|
7792
8135
|
}
|
|
7793
|
-
const parseDocumentTool = createParseDocumentTool({ sessionID, user, exuluConfig });
|
|
8136
|
+
const parseDocumentTool = createParseDocumentTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
7794
8137
|
if (parseDocumentTool && !disabled.has(parseDocumentTool.id)) {
|
|
7795
8138
|
currentTools.push(parseDocumentTool);
|
|
7796
8139
|
}
|
|
7797
|
-
const viewDocumentPageTool = createViewDocumentPageTool({ sessionID, user, exuluConfig });
|
|
8140
|
+
const viewDocumentPageTool = createViewDocumentPageTool({ sessionID, user, exuluConfig, ownerId: sessionOwnerId });
|
|
7798
8141
|
if (viewDocumentPageTool && !disabled.has(viewDocumentPageTool.id)) {
|
|
7799
8142
|
currentTools.push(viewDocumentPageTool);
|
|
7800
8143
|
}
|
|
@@ -8161,6 +8504,7 @@ export {
|
|
|
8161
8504
|
sanitizeToolName,
|
|
8162
8505
|
KB_EDITOR_TOOL_ID,
|
|
8163
8506
|
createKbEditorPickerTool,
|
|
8507
|
+
isIgnoredArtifactPath,
|
|
8164
8508
|
reportSystemDependencies,
|
|
8165
8509
|
downloadKeyIntoSandbox,
|
|
8166
8510
|
truncateToolOutput,
|
|
@@ -8176,6 +8520,7 @@ export {
|
|
|
8176
8520
|
guardExtractedFileText,
|
|
8177
8521
|
SCRUBBED_CREDENTIAL_TEXT,
|
|
8178
8522
|
SCRUBBED_OAUTH_TEXT,
|
|
8523
|
+
sessionFilePrefix,
|
|
8179
8524
|
PreviewRenderError,
|
|
8180
8525
|
getPdfPreviewBytes,
|
|
8181
8526
|
imageAttachmentGuard,
|