@exulu/backend 1.54.0 → 1.55.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/index.cjs +1970 -1176
- package/dist/index.d.cts +6 -29
- package/dist/index.d.ts +6 -29
- package/dist/index.js +1963 -1164
- package/ee/agentic-retrieval/v3/agent-loop.ts +49 -3
- package/ee/agentic-retrieval/v3/classifier.ts +42 -37
- package/ee/agentic-retrieval/v3/index.ts +112 -18
- package/ee/agentic-retrieval/v3/session-tools-registry.ts +20 -0
- package/ee/agentic-retrieval/v3/strategies.ts +28 -24
- package/ee/agentic-retrieval/v3/tools.ts +226 -111
- package/ee/agentic-retrieval/v3/trajectory.ts +227 -14
- package/ee/invoke-skills/create-sandbox.ts +119 -0
- package/ee/python/documents/processing/doc_processor.ts +106 -14
- package/package.json +4 -2
- package/ee/agentic-retrieval/ANALYSIS.md +0 -658
- package/ee/agentic-retrieval/index.ts +0 -1109
- package/ee/agentic-retrieval/logs/README.md +0 -198
- package/ee/agentic-retrieval/v2.ts +0 -1628
- package/ee/agentic-retrieval/v4/agent-loop.ts +0 -121
- package/ee/agentic-retrieval/v4/embed-preprocessor.ts +0 -76
- package/ee/agentic-retrieval/v4/index.ts +0 -181
- package/ee/agentic-retrieval/v4/system-prompt.ts +0 -248
- package/ee/agentic-retrieval/v4/tools.ts +0 -241
- package/ee/agentic-retrieval/v4/types.ts +0 -29
|
@@ -6,6 +6,8 @@ import type { ExuluReranker } from "@SRC/exulu/reranker";
|
|
|
6
6
|
import type { AgenticRetrievalOutput, ChunkResult, ClassificationResult } from "./types";
|
|
7
7
|
import type { StrategyConfig } from "./strategies";
|
|
8
8
|
import { createDynamicTools } from "./dynamic-tools";
|
|
9
|
+
import { registerSessionTools } from "./session-tools-registry";
|
|
10
|
+
import type { TrajectoryStepData } from "./trajectory";
|
|
9
11
|
|
|
10
12
|
const FINISH_TOOL_NAME = "finish_retrieval";
|
|
11
13
|
|
|
@@ -71,9 +73,11 @@ export async function* runAgentLoop(params: {
|
|
|
71
73
|
contextGuidance?: string;
|
|
72
74
|
customInstructions?: string;
|
|
73
75
|
classification: ClassificationResult;
|
|
76
|
+
sessionId?: string;
|
|
74
77
|
onStepComplete?: (step: AgenticRetrievalOutput["steps"][0]) => void;
|
|
78
|
+
onTrajectoryStep?: (data: TrajectoryStepData) => void;
|
|
75
79
|
}): AsyncGenerator<AgenticRetrievalOutput> {
|
|
76
|
-
const { query, strategy, tools, model, reranker, contextGuidance, customInstructions, onStepComplete } = params;
|
|
80
|
+
const { query, strategy, tools, model, reranker, contextGuidance, customInstructions, sessionId, onStepComplete, onTrajectoryStep } = params;
|
|
77
81
|
|
|
78
82
|
const output: AgenticRetrievalOutput = {
|
|
79
83
|
steps: [],
|
|
@@ -147,6 +151,16 @@ export async function* runAgentLoop(params: {
|
|
|
147
151
|
// Extract chunks from tool results
|
|
148
152
|
let stepChunks: any[] = extractChunksFromToolResults(result.toolResults as any[]);
|
|
149
153
|
|
|
154
|
+
// Deduplicate by chunk_id within this step (parallel tool calls can return the same chunk
|
|
155
|
+
// if the agent searches the same context twice, or the same chunk is indexed in two contexts).
|
|
156
|
+
const seenChunkIds = new Set<string>();
|
|
157
|
+
stepChunks = stepChunks.filter((c) => {
|
|
158
|
+
if (!c.chunk_id) return true;
|
|
159
|
+
if (seenChunkIds.has(c.chunk_id)) return false;
|
|
160
|
+
seenChunkIds.add(c.chunk_id);
|
|
161
|
+
return true;
|
|
162
|
+
});
|
|
163
|
+
|
|
150
164
|
// Check if any search_content call excluded content (triggers page-load dynamic tools)
|
|
151
165
|
// AI SDK v6 uses `input` (not `args`) for tool call arguments
|
|
152
166
|
const hadExcludedContent = (result.toolCalls as any[])?.some(
|
|
@@ -164,6 +178,9 @@ export async function* runAgentLoop(params: {
|
|
|
164
178
|
// Create dynamic tools (browse adjacent pages, load specific pages)
|
|
165
179
|
const newDynamic = await createDynamicTools(stepChunks as ChunkResult[], hadExcludedContent);
|
|
166
180
|
Object.assign(dynamicTools, newDynamic);
|
|
181
|
+
if (sessionId && Object.keys(newDynamic).length > 0) {
|
|
182
|
+
registerSessionTools(sessionId, newDynamic);
|
|
183
|
+
}
|
|
167
184
|
|
|
168
185
|
// If relevant content was found but fewer than 5 chunks, withhold finish_retrieval
|
|
169
186
|
// on the next step to force depth exploration via dynamic tools.
|
|
@@ -175,9 +192,14 @@ export async function* runAgentLoop(params: {
|
|
|
175
192
|
Object.keys(newDynamic).length > 0 &&
|
|
176
193
|
step < strategy.stepBudget - 2;
|
|
177
194
|
|
|
178
|
-
// Track which suggested contexts have been searched this step
|
|
195
|
+
// Track which suggested contexts have been searched this step.
|
|
196
|
+
// search_content and save_search_results now use knowledge_base_id (singular);
|
|
197
|
+
// count_items_or_chunks and search_items_by_name still use knowledge_base_ids (plural array).
|
|
179
198
|
for (const tc of (result.toolCalls as any[]) ?? []) {
|
|
180
199
|
if (SEARCH_TOOL_NAMES.has(tc.toolName)) {
|
|
200
|
+
if (tc.input?.knowledge_base_id) {
|
|
201
|
+
searchedContextIds.add(tc.input.knowledge_base_id);
|
|
202
|
+
}
|
|
181
203
|
for (const id of (tc.input?.knowledge_base_ids ?? [])) {
|
|
182
204
|
searchedContextIds.add(id);
|
|
183
205
|
}
|
|
@@ -217,11 +239,35 @@ export async function* runAgentLoop(params: {
|
|
|
217
239
|
output: stepChunks,
|
|
218
240
|
})) ?? [],
|
|
219
241
|
});
|
|
220
|
-
|
|
242
|
+
// Deduplicate against chunks already accumulated from prior steps
|
|
243
|
+
const existingChunkIds = new Set(output.chunks.map((c) => c.chunk_id).filter(Boolean));
|
|
244
|
+
output.chunks.push(...stepChunks.filter((c) => !c.chunk_id || !existingChunkIds.has(c.chunk_id)));
|
|
221
245
|
output.usage.push(result.usage);
|
|
222
246
|
|
|
223
247
|
onStepComplete?.(stepRecord);
|
|
224
248
|
|
|
249
|
+
if (onTrajectoryStep) {
|
|
250
|
+
const toolResultMap = new Map<string, any>();
|
|
251
|
+
for (const tr of (result.toolResults as any[]) ?? []) {
|
|
252
|
+
toolResultMap.set(tr.toolCallId, tr.output ?? tr.result);
|
|
253
|
+
}
|
|
254
|
+
onTrajectoryStep({
|
|
255
|
+
stepNumber: step + 1,
|
|
256
|
+
systemPrompt: stepSystemPrompt,
|
|
257
|
+
text: result.text ?? "",
|
|
258
|
+
toolCalls:
|
|
259
|
+
(result.toolCalls as any[])?.map((tc) => ({
|
|
260
|
+
name: tc.toolName,
|
|
261
|
+
id: tc.toolCallId,
|
|
262
|
+
input: tc.input,
|
|
263
|
+
output: toolResultMap.get(tc.toolCallId),
|
|
264
|
+
})) ?? [],
|
|
265
|
+
chunks: stepChunks,
|
|
266
|
+
dynamicToolsCreated: Object.keys(newDynamic),
|
|
267
|
+
tokens: result.usage?.totalTokens ?? 0,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
225
271
|
yield { ...output };
|
|
226
272
|
|
|
227
273
|
// Stop if the model called finish_retrieval AND no forced continuation is needed
|
|
@@ -3,6 +3,7 @@ import type { LanguageModel } from "ai";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import type { ExuluContext } from "@SRC/exulu/context";
|
|
5
5
|
import type { ClassificationResult, ContextSample } from "./types";
|
|
6
|
+
import { withRetry } from "@SRC/utils/with-retry";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Classifies a query into one of four types and identifies which contexts are
|
|
@@ -27,47 +28,51 @@ export async function classifyQuery(
|
|
|
27
28
|
})
|
|
28
29
|
.join("\n\n");
|
|
29
30
|
|
|
30
|
-
const result = await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const result: ClassificationResult = await withRetry(async () => {
|
|
32
|
+
const result = await generateText({
|
|
33
|
+
model,
|
|
34
|
+
temperature: 0,
|
|
35
|
+
output: Output.object({
|
|
36
|
+
schema: z.object({
|
|
37
|
+
queryType: z
|
|
38
|
+
.enum(["aggregate", "list", "targeted", "exploratory"])
|
|
39
|
+
.describe(
|
|
40
|
+
"aggregate: ONLY use when the user explicitly asks to COUNT how many documents/items/tickets exist in the knowledge base (e.g. 'how many documents about X?', 'total number of tickets'). NEVER use for: real-world statistics stored in a document, intent statements, how-to questions, error/fault descriptions, configuration questions, or any query that does not explicitly ask for a count of knowledge base entries. When in doubt, choose targeted. " +
|
|
39
41
|
"list: user wants to enumerate matching items/documents (show me all, list documents about). " +
|
|
40
42
|
"targeted: use for almost everything — specific fact, answer, configuration, how-to, error/fault, feature/behavior question. Also use for intent statements and short commands describing a desired state (phrases that state what the user wants to do or achieve, even without an explicit question word). Real-world statistics stored in documents also go here. When in doubt, choose targeted over aggregate or exploratory. " +
|
|
41
43
|
"exploratory: only for broad conceptual questions needing multi-source synthesis (what is the process for Z, explain how X works, general overview of topic Y).",
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
),
|
|
45
|
+
language: z
|
|
46
|
+
.string()
|
|
47
|
+
.describe("ISO 639-3 language code of the query (e.g. eng, deu, fra)"),
|
|
48
|
+
suggestedContextIds: z
|
|
49
|
+
.array(z.enum(contexts.map((c) => c.id)))
|
|
50
|
+
.describe(
|
|
51
|
+
"IDs of knowledge bases most likely to contain the answer. Return empty array to search all contexts.",
|
|
52
|
+
),
|
|
53
|
+
}),
|
|
51
54
|
}),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
toolChoice: "none",
|
|
56
|
+
system: `You are a query classifier for a multi-knowledge-base retrieval system.
|
|
57
|
+
Classify the query and identify which knowledge bases are most relevant.
|
|
58
|
+
|
|
59
|
+
Available knowledge bases:
|
|
60
|
+
${contextDescriptions}
|
|
61
|
+
|
|
62
|
+
Guidelines for queryType:
|
|
63
|
+
- Use "aggregate" ONLY when the query contains explicit counting language (e.g., "how many", "count", "total number", "wie viele"). Short statements, commands, or phrases without a question word are NEVER aggregate — classify them as targeted.
|
|
64
|
+
- When in doubt between aggregate and targeted: always choose targeted.
|
|
65
|
+
|
|
66
|
+
Guidelines for suggestedContextIds:
|
|
67
|
+
- Be conservative: only suggest contexts that are genuinely likely to contain the answer.
|
|
68
|
+
Aim for 2–3 focused suggestions rather than listing everything.
|
|
69
|
+
- Use each knowledge base's name and description (shown above) to judge relevance.
|
|
70
|
+
- Return an empty array only if you truly cannot determine which contexts are relevant.`,
|
|
71
|
+
prompt: `Query: ${query}`,
|
|
72
|
+
});
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
return result.output as ClassificationResult;
|
|
75
|
+
}, 3)
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
- Use "aggregate" ONLY when the query contains explicit counting language (e.g., "how many", "count", "total number", "wie viele"). Short statements, commands, or phrases without a question word are NEVER aggregate — classify them as targeted.
|
|
62
|
-
- When in doubt between aggregate and targeted: always choose targeted.
|
|
63
|
-
|
|
64
|
-
Guidelines for suggestedContextIds:
|
|
65
|
-
- Be conservative: only suggest contexts that are genuinely likely to contain the answer.
|
|
66
|
-
Aim for 2–3 focused suggestions rather than listing everything.
|
|
67
|
-
- Use each knowledge base's name and description (shown above) to judge relevance.
|
|
68
|
-
- Return an empty array only if you truly cannot determine which contexts are relevant.`,
|
|
69
|
-
prompt: `Query: ${query}`,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return result.output as ClassificationResult;
|
|
77
|
+
return result;
|
|
73
78
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { createBashTool } from "bash-tool";
|
|
3
|
-
import type { LanguageModel } from "ai";
|
|
3
|
+
import type { LanguageModel, Tool } from "ai";
|
|
4
4
|
import type { ExuluContext } from "@SRC/exulu/context";
|
|
5
5
|
import type { ExuluReranker } from "@SRC/exulu/reranker";
|
|
6
6
|
import { ExuluTool } from "@SRC/exulu/tool";
|
|
@@ -8,7 +8,7 @@ import type { User } from "@EXULU_TYPES/models/user";
|
|
|
8
8
|
import { checkLicense } from "@EE/entitlements";
|
|
9
9
|
import { ContextSampler } from "./context-sampler";
|
|
10
10
|
import { classifyQuery } from "./classifier";
|
|
11
|
-
import { createRetrievalTools } from "./tools";
|
|
11
|
+
import { createRetrievalTools, parseGlobalItemIds } from "./tools";
|
|
12
12
|
import { STRATEGIES } from "./strategies";
|
|
13
13
|
import { runAgentLoop } from "./agent-loop";
|
|
14
14
|
import { TrajectoryLogger } from "./trajectory";
|
|
@@ -26,6 +26,9 @@ async function* executeV3({
|
|
|
26
26
|
user,
|
|
27
27
|
role,
|
|
28
28
|
customInstructions,
|
|
29
|
+
logTrajectory,
|
|
30
|
+
sessionId,
|
|
31
|
+
preselectedItemIds,
|
|
29
32
|
}: {
|
|
30
33
|
query: string;
|
|
31
34
|
contexts: ExuluContext[];
|
|
@@ -34,16 +37,29 @@ async function* executeV3({
|
|
|
34
37
|
user?: User;
|
|
35
38
|
role?: string;
|
|
36
39
|
customInstructions?: string;
|
|
40
|
+
logTrajectory?: boolean;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
preselectedItemIds?: string[];
|
|
37
43
|
}): AsyncGenerator<AgenticRetrievalOutput> {
|
|
38
|
-
// ── 1.
|
|
44
|
+
// ── 1. Parse preselected item IDs (global format: "<context_id>/<item_id>") ─
|
|
45
|
+
const preselectedByContext = preselectedItemIds?.length
|
|
46
|
+
? parseGlobalItemIds(preselectedItemIds)
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
// When preselection is active, restrict to only contexts that have selected items
|
|
50
|
+
const activeContexts = preselectedByContext?.size
|
|
51
|
+
? contexts.filter((c) => preselectedByContext.has(c.id))
|
|
52
|
+
: contexts;
|
|
53
|
+
|
|
54
|
+
// ── 2. Sample example records from each context (cached) ──────────────────
|
|
39
55
|
console.log("[EXULU] v3 — sampling contexts");
|
|
40
|
-
const samples = await sampler.getSamples(
|
|
56
|
+
const samples = await sampler.getSamples(activeContexts, user, role);
|
|
41
57
|
|
|
42
|
-
// ──
|
|
58
|
+
// ── 3. Classify query (single fast LLM call) ──────────────────────────────
|
|
43
59
|
console.log("[EXULU] v3 — classifying query");
|
|
44
60
|
let classification;
|
|
45
61
|
try {
|
|
46
|
-
classification = await classifyQuery(query,
|
|
62
|
+
classification = await classifyQuery(query, activeContexts, samples, model);
|
|
47
63
|
} catch (err) {
|
|
48
64
|
console.warn("[EXULU] v3 — classification failed, falling back to exploratory:", err);
|
|
49
65
|
classification = {
|
|
@@ -54,32 +70,39 @@ async function* executeV3({
|
|
|
54
70
|
}
|
|
55
71
|
console.log("[EXULU] v3 — classified as:", classification);
|
|
56
72
|
|
|
57
|
-
// ──
|
|
73
|
+
// ── 4. Select strategy ────────────────────────────────────────────────────
|
|
58
74
|
const strategy = STRATEGIES[classification.queryType];
|
|
59
75
|
|
|
60
76
|
// Build context guidance: the classifier is a priority hint, not a hard filter.
|
|
61
77
|
// All contexts remain available so the agent can fall back if suggested ones miss.
|
|
62
78
|
const suggestedIds = classification.suggestedContextIds;
|
|
63
|
-
const fallbackIds =
|
|
79
|
+
const fallbackIds = activeContexts
|
|
64
80
|
.filter((c) => !suggestedIds.includes(c.id))
|
|
65
81
|
.map((c) => c.id);
|
|
66
|
-
const
|
|
82
|
+
const contextBase =
|
|
67
83
|
suggestedIds.length > 0
|
|
68
84
|
? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts — follow them.`
|
|
69
|
-
: `All contexts available: [${
|
|
85
|
+
: `All contexts available: [${activeContexts.map((c) => c.id).join(", ")}].`;
|
|
86
|
+
|
|
87
|
+
const preselectedNote = preselectedByContext?.size
|
|
88
|
+
? `\nSCOPE CONSTRAINT: Retrieval is scoped to preselected items/contexts. Per context: ${[...preselectedByContext.entries()].map(([ctx, ids]) => ids === null ? `${ctx} (full context)` : `${ctx} (${ids.length} item${ids.length === 1 ? "" : "s"})`).join(", ")}. All tools enforce this scope automatically. For full-context entries you may search freely; for item-restricted entries do NOT use search_items_by_name for discovery — go directly to search_content or save_search_results.`
|
|
89
|
+
: "";
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
const contextGuidance = contextBase + preselectedNote;
|
|
92
|
+
|
|
93
|
+
// ── 5. Initialize tools ───────────────────────────────────────────────────
|
|
72
94
|
const bashToolkit = await createBashTool({ files: {} });
|
|
73
95
|
|
|
74
96
|
const retrievalTools = createRetrievalTools({
|
|
75
|
-
contexts,
|
|
97
|
+
contexts: activeContexts,
|
|
76
98
|
user,
|
|
77
99
|
role,
|
|
78
100
|
updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files),
|
|
101
|
+
preselectedItemsByContext: preselectedByContext,
|
|
79
102
|
});
|
|
80
103
|
|
|
81
104
|
// Build the tool set for this strategy
|
|
82
|
-
const activeTools: Record<string,
|
|
105
|
+
const activeTools: Record<string, Tool> = {};
|
|
83
106
|
for (const name of strategy.retrieval_tools) {
|
|
84
107
|
if (name in retrievalTools) {
|
|
85
108
|
activeTools[name] = retrievalTools[name as keyof typeof retrievalTools];
|
|
@@ -89,10 +112,10 @@ async function* executeV3({
|
|
|
89
112
|
Object.assign(activeTools, bashToolkit.tools);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
// ──
|
|
93
|
-
const trajectory = new TrajectoryLogger(query, classification);
|
|
115
|
+
// ── 6. Set up trajectory logging ──────────────────────────────────────────
|
|
116
|
+
const trajectory = new TrajectoryLogger(query, classification, undefined, preselectedItemIds);
|
|
94
117
|
|
|
95
|
-
// ──
|
|
118
|
+
// ── 7. Run agent loop ─────────────────────────────────────────────────────
|
|
96
119
|
let finalOutput: AgenticRetrievalOutput | undefined;
|
|
97
120
|
let executionError: Error | undefined;
|
|
98
121
|
|
|
@@ -106,7 +129,9 @@ async function* executeV3({
|
|
|
106
129
|
contextGuidance,
|
|
107
130
|
customInstructions,
|
|
108
131
|
classification,
|
|
132
|
+
sessionId,
|
|
109
133
|
onStepComplete: (step) => trajectory.recordStep(step),
|
|
134
|
+
onTrajectoryStep: (data) => trajectory.recordRichStep(data),
|
|
110
135
|
})) {
|
|
111
136
|
finalOutput = output;
|
|
112
137
|
yield output;
|
|
@@ -117,7 +142,7 @@ async function* executeV3({
|
|
|
117
142
|
throw err;
|
|
118
143
|
} finally {
|
|
119
144
|
if (finalOutput) {
|
|
120
|
-
const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError);
|
|
145
|
+
const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError, logTrajectory);
|
|
121
146
|
if (trajectoryFile) {
|
|
122
147
|
finalOutput.trajectoryFile = trajectoryFile;
|
|
123
148
|
}
|
|
@@ -141,6 +166,7 @@ export function createAgenticRetrievalToolV3({
|
|
|
141
166
|
user,
|
|
142
167
|
role,
|
|
143
168
|
model,
|
|
169
|
+
preselectedItemIds,
|
|
144
170
|
}: {
|
|
145
171
|
contexts: ExuluContext[];
|
|
146
172
|
rerankers: ExuluReranker[];
|
|
@@ -148,6 +174,7 @@ export function createAgenticRetrievalToolV3({
|
|
|
148
174
|
role?: string;
|
|
149
175
|
model?: LanguageModel;
|
|
150
176
|
instructions?: string;
|
|
177
|
+
preselectedItemIds?: string[];
|
|
151
178
|
}): ExuluTool | undefined {
|
|
152
179
|
const license = checkLicense();
|
|
153
180
|
if (!license["agentic-retrieval"]) {
|
|
@@ -177,6 +204,12 @@ export function createAgenticRetrievalToolV3({
|
|
|
177
204
|
type: "string",
|
|
178
205
|
default: "none",
|
|
179
206
|
},
|
|
207
|
+
{
|
|
208
|
+
name: "managed_context",
|
|
209
|
+
description: "Makes sure the user defines which items from which contexts the agentic retrieval tool will search in",
|
|
210
|
+
type: "boolean",
|
|
211
|
+
default: false,
|
|
212
|
+
},
|
|
180
213
|
{
|
|
181
214
|
name: "reasoning_model",
|
|
182
215
|
description: "By default the agentic retrieval tool uses the model from the agent calling the tool, but you can overwrite this here for the reasoning phase",
|
|
@@ -189,6 +222,18 @@ export function createAgenticRetrievalToolV3({
|
|
|
189
222
|
type: "string",
|
|
190
223
|
default: "",
|
|
191
224
|
},
|
|
225
|
+
{
|
|
226
|
+
name: "require_preselected_contexts",
|
|
227
|
+
description: "Require the user to preselect contexts before executing the tool, meaning the user will be asked to select the contexts they want to search in",
|
|
228
|
+
type: "boolean",
|
|
229
|
+
default: false,
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "log_trajectories",
|
|
233
|
+
description: "Save a detailed markdown + JSON log of every retrieval execution to disk. Useful for debugging and evaluation.",
|
|
234
|
+
type: "boolean",
|
|
235
|
+
default: false,
|
|
236
|
+
},
|
|
192
237
|
...contexts.map((ctx) => ({
|
|
193
238
|
name: ctx.id,
|
|
194
239
|
description: `Enable search in "${ctx.name}". ${ctx.description}`,
|
|
@@ -202,15 +247,26 @@ export function createAgenticRetrievalToolV3({
|
|
|
202
247
|
.string()
|
|
203
248
|
.optional()
|
|
204
249
|
.describe("Additional instructions from the user to guide retrieval"),
|
|
250
|
+
confirmedContextIds: z
|
|
251
|
+
.array(z.string())
|
|
252
|
+
.optional()
|
|
253
|
+
.describe(
|
|
254
|
+
"Knowledge base IDs explicitly confirmed by the user to be used in the retrieval. " +
|
|
255
|
+
"When presen only searches these contexts. "
|
|
256
|
+
)
|
|
205
257
|
}),
|
|
206
258
|
execute: async function* ({
|
|
207
259
|
query,
|
|
208
260
|
userInstructions,
|
|
261
|
+
confirmedContextIds,
|
|
209
262
|
toolVariablesConfig,
|
|
263
|
+
sessionID,
|
|
210
264
|
}: {
|
|
211
265
|
query: string;
|
|
212
266
|
userInstructions?: string;
|
|
267
|
+
confirmedContextIds?: string[];
|
|
213
268
|
toolVariablesConfig?: Record<string, any>;
|
|
269
|
+
sessionID?: string;
|
|
214
270
|
}) {
|
|
215
271
|
|
|
216
272
|
/* ROADMAP:
|
|
@@ -234,14 +290,24 @@ export function createAgenticRetrievalToolV3({
|
|
|
234
290
|
} */
|
|
235
291
|
|
|
236
292
|
if (!model) {
|
|
237
|
-
|
|
293
|
+
yield { result: "Model is required for executing the agentic retrieval tool" };
|
|
294
|
+
return;
|
|
238
295
|
}
|
|
296
|
+
|
|
239
297
|
let activeContexts = contexts;
|
|
240
298
|
let configuredReranker: ExuluReranker | undefined;
|
|
241
299
|
let configInstructions = "";
|
|
300
|
+
let logTrajectory = false;
|
|
301
|
+
let requiresPreselectedContexts = false;
|
|
302
|
+
let managedContextEnabled = false;
|
|
242
303
|
|
|
243
304
|
if (toolVariablesConfig) {
|
|
244
305
|
configInstructions = toolVariablesConfig["instructions"] ?? "";
|
|
306
|
+
logTrajectory =
|
|
307
|
+
toolVariablesConfig["log_trajectories"] === true ||
|
|
308
|
+
toolVariablesConfig["log_trajectories"] === "true";
|
|
309
|
+
|
|
310
|
+
managedContextEnabled = toolVariablesConfig["managed_context"] === true || toolVariablesConfig["managed_context"] === "true";
|
|
245
311
|
|
|
246
312
|
activeContexts = contexts.filter(
|
|
247
313
|
(ctx) =>
|
|
@@ -251,12 +317,36 @@ export function createAgenticRetrievalToolV3({
|
|
|
251
317
|
);
|
|
252
318
|
if (activeContexts.length === 0) activeContexts = contexts;
|
|
253
319
|
|
|
320
|
+
requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
|
|
321
|
+
|
|
254
322
|
const rerankerId = toolVariablesConfig["reranker"];
|
|
323
|
+
|
|
255
324
|
if (rerankerId && rerankerId !== "none") {
|
|
256
325
|
configuredReranker = rerankers.find((r) => r.id === rerankerId);
|
|
257
326
|
}
|
|
258
327
|
}
|
|
259
328
|
|
|
329
|
+
console.log("[EXULU] Managed context enabled:", managedContextEnabled);
|
|
330
|
+
console.log("[EXULU] Preselected item IDs:", preselectedItemIds);
|
|
331
|
+
|
|
332
|
+
if (managedContextEnabled && !preselectedItemIds?.length) {
|
|
333
|
+
console.log("[EXULU] Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool.");
|
|
334
|
+
yield { result: "Managed context was enabled for the agentic retrieval tool. This means that the user must preselect items that the agentic retrieval tool will search in, please notify the user to preselect items before executing the tool." };
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (requiresPreselectedContexts && !confirmedContextIds?.length && !preselectedItemIds?.length) {
|
|
339
|
+
console.log("[EXULU] The user must choose between the available contexts before executing the tool. The available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text.");
|
|
340
|
+
yield { result: "The user must choose between the available contexts before executing the tool, the available contexts are: " + activeContexts.map((c) => c.id).join(", ") + ". If the question_ask tool is available use that to ask the user which contexts they want to search in, otherwise just ask them in plain text." };
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (confirmedContextIds?.length) {
|
|
345
|
+
const confirmed = new Set(confirmedContextIds);
|
|
346
|
+
const filtered = activeContexts.filter((c) => confirmed.has(c.id));
|
|
347
|
+
if (filtered.length > 0) activeContexts = filtered;
|
|
348
|
+
}
|
|
349
|
+
|
|
260
350
|
const combinedInstructions = [
|
|
261
351
|
configInstructions ? `Configuration instructions: ${configInstructions}` : "",
|
|
262
352
|
adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
|
|
@@ -273,9 +363,13 @@ export function createAgenticRetrievalToolV3({
|
|
|
273
363
|
user,
|
|
274
364
|
role,
|
|
275
365
|
customInstructions: combinedInstructions || undefined,
|
|
366
|
+
logTrajectory,
|
|
367
|
+
sessionId: sessionID,
|
|
368
|
+
preselectedItemIds,
|
|
276
369
|
})) {
|
|
277
370
|
yield { result: JSON.stringify(output) };
|
|
278
371
|
}
|
|
372
|
+
return;
|
|
279
373
|
},
|
|
280
374
|
});
|
|
281
375
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Tool as AITool } from "ai";
|
|
2
|
+
|
|
3
|
+
// Persists dynamic tools (get_more_content_from_X, get_X_page_N_content) created
|
|
4
|
+
// during an agentic retrieval run, keyed by session ID. This lets the outer chat
|
|
5
|
+
// agent call them directly on follow-up questions without re-running retrieval.
|
|
6
|
+
const registry = new Map<string, Map<string, AITool>>();
|
|
7
|
+
|
|
8
|
+
export function registerSessionTools(sessionId: string, tools: Record<string, AITool>): void {
|
|
9
|
+
const existing = registry.get(sessionId) ?? new Map();
|
|
10
|
+
for (const [name, toolDef] of Object.entries(tools)) {
|
|
11
|
+
existing.set(name, toolDef);
|
|
12
|
+
}
|
|
13
|
+
registry.set(sessionId, existing);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getSessionTools(sessionId: string): Record<string, AITool> {
|
|
17
|
+
const toolMap = registry.get(sessionId);
|
|
18
|
+
if (!toolMap || toolMap.size === 0) return {};
|
|
19
|
+
return Object.fromEntries(toolMap.entries());
|
|
20
|
+
}
|
|
@@ -25,9 +25,10 @@ another component will do that based on what you retrieve.
|
|
|
25
25
|
Always respond in the SAME LANGUAGE as the user's query.
|
|
26
26
|
Always write search queries in the SAME LANGUAGE as the user's query — do NOT translate to English.
|
|
27
27
|
|
|
28
|
-
SEARCH APPROACH —
|
|
29
|
-
1.
|
|
30
|
-
|
|
28
|
+
SEARCH APPROACH — one knowledge base at a time, then go deep:
|
|
29
|
+
1. search_content and save_search_results accept ONE knowledge base per call. Make a separate
|
|
30
|
+
call for each knowledge base you need to cover — never skip one. Search all relevant
|
|
31
|
+
knowledge bases before concluding, even if the first one already returned good results.
|
|
31
32
|
2. After finding a relevant document, use get_more_content_from_{item} dynamic tools to load
|
|
32
33
|
additional pages/sections. The specific answer is often NOT in the first retrieved chunk —
|
|
33
34
|
always explore adjacent content before concluding.
|
|
@@ -44,9 +45,8 @@ export const AGGREGATE_INSTRUCTIONS = `
|
|
|
44
45
|
${BASE_INSTRUCTIONS}
|
|
45
46
|
|
|
46
47
|
STRATEGY: This is a COUNTING or AGGREGATION query.
|
|
47
|
-
- Use count_items_or_chunks exclusively
|
|
48
|
+
- Use count_items_or_chunks exclusively — it accepts multiple knowledge bases in one call for efficiency
|
|
48
49
|
- Do NOT use search_content — it loads unnecessary data
|
|
49
|
-
- Search ALL contexts in parallel in a single tool call
|
|
50
50
|
- Return immediately after counting — one step is sufficient
|
|
51
51
|
- If the count needs a content filter, use content_query parameter
|
|
52
52
|
`.trim();
|
|
@@ -81,9 +81,23 @@ Search language:
|
|
|
81
81
|
- Always write search queries in the SAME LANGUAGE as the user's query.
|
|
82
82
|
- Do NOT translate the query to English — the documents are indexed in their original language.
|
|
83
83
|
|
|
84
|
-
Step 1 —
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
Step 1 — match the opening move to what the query actually needs:
|
|
85
|
+
|
|
86
|
+
Query references a SPECIFIC NAMED DOCUMENT (product manual, titled report, named file):
|
|
87
|
+
→ ALWAYS start with search_items_by_name — searches document name/title directly
|
|
88
|
+
→ Only proceed to load content if the document is found
|
|
89
|
+
|
|
90
|
+
Query asks WHETHER a topic EXISTS or WHAT documents cover a topic (no specific title given):
|
|
91
|
+
→ search_content with includeContent: false
|
|
92
|
+
→ Returns matching document names without loading chunk text — efficient and precise
|
|
93
|
+
→ Load content with dynamic get_{item}_page_{n}_content tools only if needed in step 2
|
|
94
|
+
|
|
95
|
+
Query asks for CONTENT itself (procedures, parameters, explanations, how-to):
|
|
96
|
+
→ search_content with includeContent: true, limit 20, searchMethod: "hybrid"
|
|
97
|
+
→ Make one call per knowledge base — search each separately before concluding
|
|
98
|
+
|
|
99
|
+
Query provides an EXACT TERM (error code, product code, ID, parameter name):
|
|
100
|
+
→ search_content with searchMethod: "keyword"
|
|
87
101
|
|
|
88
102
|
Step 2+ — depth and follow-up:
|
|
89
103
|
- For any relevant document found with fewer than 5 chunks, use get_more_content_from_{item}
|
|
@@ -93,19 +107,9 @@ Step 2+ — depth and follow-up:
|
|
|
93
107
|
- Try alternative phrasings if the first query doesn't surface the right answer.
|
|
94
108
|
|
|
95
109
|
Product-specific filtering:
|
|
96
|
-
- When the query mentions a specific
|
|
97
|
-
item_names: ["<
|
|
110
|
+
- When the query mentions a specific named entity (product, model, version), you MAY use
|
|
111
|
+
item_names: ["<entity>"] on a follow-up search to narrow results — but only after an initial
|
|
98
112
|
wide search. Never start with item_names filtering alone.
|
|
99
|
-
|
|
100
|
-
Two-step approach — use includeContent: false first:
|
|
101
|
-
- Only when you expect many results (>20) and need to identify the right document first.
|
|
102
|
-
- Step 1: search_content with includeContent: false → see which documents/chunks match.
|
|
103
|
-
- Step 2: use dynamic get_{item}_page_{n}_content tools to load specific pages.
|
|
104
|
-
|
|
105
|
-
Search method selection:
|
|
106
|
-
- hybrid (default): best for most queries
|
|
107
|
-
- keyword: exact product codes, document IDs, error codes
|
|
108
|
-
- semantic: conceptual questions, synonyms, paraphrasing
|
|
109
113
|
`.trim();
|
|
110
114
|
|
|
111
115
|
export const EXPLORATORY_INSTRUCTIONS = `
|
|
@@ -114,13 +118,13 @@ ${BASE_INSTRUCTIONS}
|
|
|
114
118
|
STRATEGY: This is an EXPLORATORY query — general question requiring broad search.
|
|
115
119
|
|
|
116
120
|
Recommended approach:
|
|
117
|
-
1.
|
|
121
|
+
1. Search each relevant knowledge base separately with hybrid search (includeContent: true, limit: 20) — one call per knowledge base
|
|
118
122
|
2. If results are insufficient: try alternative search terms or different search method
|
|
119
123
|
3. Use save_search_results + bash grep when you need to scan many results without context bloat
|
|
120
124
|
4. Use dynamic get_more_content_from_{item} tools to read adjacent pages when a relevant item is found
|
|
121
125
|
|
|
122
126
|
When to declare done:
|
|
123
|
-
- You have retrieved chunks that cover the key aspects of the query
|
|
127
|
+
- You have retrieved chunks that cover the key aspects of the query from all relevant knowledge bases
|
|
124
128
|
- OR you have tried 3+ different search strategies and found nothing relevant
|
|
125
129
|
|
|
126
130
|
Do NOT use count_items_or_chunks for exploratory queries — the user wants content, not statistics.
|
|
@@ -140,7 +144,7 @@ export const STRATEGIES: Record<QueryType, StrategyConfig> = {
|
|
|
140
144
|
},
|
|
141
145
|
list: {
|
|
142
146
|
queryType: "list",
|
|
143
|
-
stepBudget:
|
|
147
|
+
stepBudget: 3,
|
|
144
148
|
retrieval_tools: ["count_items_or_chunks", "search_items_by_name", "search_content"],
|
|
145
149
|
include_bash: false,
|
|
146
150
|
instructions: LIST_INSTRUCTIONS,
|
|
@@ -154,7 +158,7 @@ export const STRATEGIES: Record<QueryType, StrategyConfig> = {
|
|
|
154
158
|
},
|
|
155
159
|
exploratory: {
|
|
156
160
|
queryType: "exploratory",
|
|
157
|
-
stepBudget:
|
|
161
|
+
stepBudget: 5,
|
|
158
162
|
retrieval_tools: [
|
|
159
163
|
"count_items_or_chunks",
|
|
160
164
|
"search_items_by_name",
|