@exulu/backend 1.53.1 → 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 +5812 -4003
- package/dist/index.d.cts +46 -7
- package/dist/index.d.ts +46 -7
- package/dist/index.js +5830 -4023
- package/ee/agentic-retrieval/v3/agent-loop.ts +288 -0
- package/ee/agentic-retrieval/v3/classifier.ts +78 -0
- package/ee/agentic-retrieval/v3/context-sampler.ts +70 -0
- package/ee/agentic-retrieval/v3/dynamic-tools.ts +115 -0
- package/ee/agentic-retrieval/v3/index.ts +375 -0
- package/ee/agentic-retrieval/v3/session-tools-registry.ts +20 -0
- package/ee/agentic-retrieval/v3/strategies.ts +171 -0
- package/ee/agentic-retrieval/v3/tools.ts +550 -0
- package/ee/agentic-retrieval/v3/trajectory.ts +309 -0
- package/ee/agentic-retrieval/v3/types.ts +59 -0
- package/ee/chunking/markdown.ts +4 -2
- package/ee/invoke-skills/create-sandbox.ts +119 -0
- package/ee/python/documents/processing/doc_processor.ts +106 -14
- package/ee/workers.ts +1 -1
- package/package.json +9 -4
- package/ee/agentic-retrieval/index.ts +0 -1109
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createBashTool } from "bash-tool";
|
|
3
|
+
import type { LanguageModel, Tool } from "ai";
|
|
4
|
+
import type { ExuluContext } from "@SRC/exulu/context";
|
|
5
|
+
import type { ExuluReranker } from "@SRC/exulu/reranker";
|
|
6
|
+
import { ExuluTool } from "@SRC/exulu/tool";
|
|
7
|
+
import type { User } from "@EXULU_TYPES/models/user";
|
|
8
|
+
import { checkLicense } from "@EE/entitlements";
|
|
9
|
+
import { ContextSampler } from "./context-sampler";
|
|
10
|
+
import { classifyQuery } from "./classifier";
|
|
11
|
+
import { createRetrievalTools, parseGlobalItemIds } from "./tools";
|
|
12
|
+
import { STRATEGIES } from "./strategies";
|
|
13
|
+
import { runAgentLoop } from "./agent-loop";
|
|
14
|
+
import { TrajectoryLogger } from "./trajectory";
|
|
15
|
+
import type { AgenticRetrievalOutput, QueryType } from "./types";
|
|
16
|
+
|
|
17
|
+
// Module-level sampler — shared across all tool instances so the cache is warm
|
|
18
|
+
// across requests within the same process.
|
|
19
|
+
const sampler = new ContextSampler();
|
|
20
|
+
|
|
21
|
+
async function* executeV3({
|
|
22
|
+
query,
|
|
23
|
+
contexts,
|
|
24
|
+
reranker,
|
|
25
|
+
model,
|
|
26
|
+
user,
|
|
27
|
+
role,
|
|
28
|
+
customInstructions,
|
|
29
|
+
logTrajectory,
|
|
30
|
+
sessionId,
|
|
31
|
+
preselectedItemIds,
|
|
32
|
+
}: {
|
|
33
|
+
query: string;
|
|
34
|
+
contexts: ExuluContext[];
|
|
35
|
+
reranker?: ExuluReranker;
|
|
36
|
+
model: LanguageModel;
|
|
37
|
+
user?: User;
|
|
38
|
+
role?: string;
|
|
39
|
+
customInstructions?: string;
|
|
40
|
+
logTrajectory?: boolean;
|
|
41
|
+
sessionId?: string;
|
|
42
|
+
preselectedItemIds?: string[];
|
|
43
|
+
}): AsyncGenerator<AgenticRetrievalOutput> {
|
|
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) ──────────────────
|
|
55
|
+
console.log("[EXULU] v3 — sampling contexts");
|
|
56
|
+
const samples = await sampler.getSamples(activeContexts, user, role);
|
|
57
|
+
|
|
58
|
+
// ── 3. Classify query (single fast LLM call) ──────────────────────────────
|
|
59
|
+
console.log("[EXULU] v3 — classifying query");
|
|
60
|
+
let classification;
|
|
61
|
+
try {
|
|
62
|
+
classification = await classifyQuery(query, activeContexts, samples, model);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.warn("[EXULU] v3 — classification failed, falling back to exploratory:", err);
|
|
65
|
+
classification = {
|
|
66
|
+
queryType: "exploratory" as QueryType,
|
|
67
|
+
language: "eng",
|
|
68
|
+
suggestedContextIds: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
console.log("[EXULU] v3 — classified as:", classification);
|
|
72
|
+
|
|
73
|
+
// ── 4. Select strategy ────────────────────────────────────────────────────
|
|
74
|
+
const strategy = STRATEGIES[classification.queryType];
|
|
75
|
+
|
|
76
|
+
// Build context guidance: the classifier is a priority hint, not a hard filter.
|
|
77
|
+
// All contexts remain available so the agent can fall back if suggested ones miss.
|
|
78
|
+
const suggestedIds = classification.suggestedContextIds;
|
|
79
|
+
const fallbackIds = activeContexts
|
|
80
|
+
.filter((c) => !suggestedIds.includes(c.id))
|
|
81
|
+
.map((c) => c.id);
|
|
82
|
+
const contextBase =
|
|
83
|
+
suggestedIds.length > 0
|
|
84
|
+
? `Suggested priority contexts: [${suggestedIds.join(", ")}]. Also available: [${fallbackIds.join(", ")}]. Custom instructions may require searching additional or all contexts — follow them.`
|
|
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
|
+
: "";
|
|
90
|
+
|
|
91
|
+
const contextGuidance = contextBase + preselectedNote;
|
|
92
|
+
|
|
93
|
+
// ── 5. Initialize tools ───────────────────────────────────────────────────
|
|
94
|
+
const bashToolkit = await createBashTool({ files: {} });
|
|
95
|
+
|
|
96
|
+
const retrievalTools = createRetrievalTools({
|
|
97
|
+
contexts: activeContexts,
|
|
98
|
+
user,
|
|
99
|
+
role,
|
|
100
|
+
updateVirtualFiles: (files) => bashToolkit.sandbox.writeFiles(files),
|
|
101
|
+
preselectedItemsByContext: preselectedByContext,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Build the tool set for this strategy
|
|
105
|
+
const activeTools: Record<string, Tool> = {};
|
|
106
|
+
for (const name of strategy.retrieval_tools) {
|
|
107
|
+
if (name in retrievalTools) {
|
|
108
|
+
activeTools[name] = retrievalTools[name as keyof typeof retrievalTools];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (strategy.include_bash) {
|
|
112
|
+
Object.assign(activeTools, bashToolkit.tools);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 6. Set up trajectory logging ──────────────────────────────────────────
|
|
116
|
+
const trajectory = new TrajectoryLogger(query, classification, undefined, preselectedItemIds);
|
|
117
|
+
|
|
118
|
+
// ── 7. Run agent loop ─────────────────────────────────────────────────────
|
|
119
|
+
let finalOutput: AgenticRetrievalOutput | undefined;
|
|
120
|
+
let executionError: Error | undefined;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
for await (const output of runAgentLoop({
|
|
124
|
+
query,
|
|
125
|
+
strategy,
|
|
126
|
+
tools: activeTools,
|
|
127
|
+
model,
|
|
128
|
+
reranker,
|
|
129
|
+
contextGuidance,
|
|
130
|
+
customInstructions,
|
|
131
|
+
classification,
|
|
132
|
+
sessionId,
|
|
133
|
+
onStepComplete: (step) => trajectory.recordStep(step),
|
|
134
|
+
onTrajectoryStep: (data) => trajectory.recordRichStep(data),
|
|
135
|
+
})) {
|
|
136
|
+
finalOutput = output;
|
|
137
|
+
yield output;
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
executionError = err as Error;
|
|
141
|
+
console.error("[EXULU] v3 — agent loop error:", err);
|
|
142
|
+
throw err;
|
|
143
|
+
} finally {
|
|
144
|
+
if (finalOutput) {
|
|
145
|
+
const trajectoryFile = await trajectory.finalize(finalOutput, !executionError, executionError, logTrajectory);
|
|
146
|
+
if (trajectoryFile) {
|
|
147
|
+
finalOutput.trajectoryFile = trajectoryFile;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates the v3 ExuluTool for agentic context retrieval.
|
|
155
|
+
*
|
|
156
|
+
* Compared to v2:
|
|
157
|
+
* - Single LLM call per step (vs two in v2)
|
|
158
|
+
* - Query classification upfront → strategy-based step budget (1–3 vs hardcoded 2)
|
|
159
|
+
* - Context example records sampled at init and cached
|
|
160
|
+
* - Strategy-specific instructions and tool sets
|
|
161
|
+
*/
|
|
162
|
+
export function createAgenticRetrievalToolV3({
|
|
163
|
+
contexts,
|
|
164
|
+
instructions: adminInstructions,
|
|
165
|
+
rerankers,
|
|
166
|
+
user,
|
|
167
|
+
role,
|
|
168
|
+
model,
|
|
169
|
+
preselectedItemIds,
|
|
170
|
+
}: {
|
|
171
|
+
contexts: ExuluContext[];
|
|
172
|
+
rerankers: ExuluReranker[];
|
|
173
|
+
user?: User;
|
|
174
|
+
role?: string;
|
|
175
|
+
model?: LanguageModel;
|
|
176
|
+
instructions?: string;
|
|
177
|
+
preselectedItemIds?: string[];
|
|
178
|
+
}): ExuluTool | undefined {
|
|
179
|
+
const license = checkLicense();
|
|
180
|
+
if (!license["agentic-retrieval"]) {
|
|
181
|
+
console.warn("[EXULU] Not licensed for agentic retrieval");
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const contextNames = contexts.map((c) => c.id).join(", ");
|
|
186
|
+
|
|
187
|
+
return new ExuluTool({
|
|
188
|
+
id: "agentic_context_search",
|
|
189
|
+
name: "Agentic Context Search",
|
|
190
|
+
description: `Intelligent context search with query classification, strategy-based retrieval, and virtual filesystem filtering. Searches: ${contextNames}`,
|
|
191
|
+
category: "contexts",
|
|
192
|
+
needsApproval: false,
|
|
193
|
+
type: "context",
|
|
194
|
+
config: [
|
|
195
|
+
{
|
|
196
|
+
name: "instructions",
|
|
197
|
+
description: "Custom instructions for the retrieval agent",
|
|
198
|
+
type: "string",
|
|
199
|
+
default: "",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "reranker",
|
|
203
|
+
description: "Reranker to use for result ranking",
|
|
204
|
+
type: "string",
|
|
205
|
+
default: "none",
|
|
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
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "reasoning_model",
|
|
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",
|
|
216
|
+
type: "string",
|
|
217
|
+
default: "",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "search_model",
|
|
221
|
+
description: "By default the agentic retrieval tool uses the model from the agent calling the tool, but you can overwrite this here for the search phase",
|
|
222
|
+
type: "string",
|
|
223
|
+
default: "",
|
|
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
|
+
},
|
|
237
|
+
...contexts.map((ctx) => ({
|
|
238
|
+
name: ctx.id,
|
|
239
|
+
description: `Enable search in "${ctx.name}". ${ctx.description}`,
|
|
240
|
+
type: "boolean" as const,
|
|
241
|
+
default: true,
|
|
242
|
+
})),
|
|
243
|
+
],
|
|
244
|
+
inputSchema: z.object({
|
|
245
|
+
query: z.string().describe("The question or query to answer"),
|
|
246
|
+
userInstructions: z
|
|
247
|
+
.string()
|
|
248
|
+
.optional()
|
|
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
|
+
)
|
|
257
|
+
}),
|
|
258
|
+
execute: async function* ({
|
|
259
|
+
query,
|
|
260
|
+
userInstructions,
|
|
261
|
+
confirmedContextIds,
|
|
262
|
+
toolVariablesConfig,
|
|
263
|
+
sessionID,
|
|
264
|
+
}: {
|
|
265
|
+
query: string;
|
|
266
|
+
userInstructions?: string;
|
|
267
|
+
confirmedContextIds?: string[];
|
|
268
|
+
toolVariablesConfig?: Record<string, any>;
|
|
269
|
+
sessionID?: string;
|
|
270
|
+
}) {
|
|
271
|
+
|
|
272
|
+
/* ROADMAP:
|
|
273
|
+
const app = exuluApp.get();
|
|
274
|
+
let reasoningModel: LanguageModel | undefined = model;
|
|
275
|
+
let searchModel: LanguageModel | undefined = model;
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if (toolVariablesConfig?.reasoning_model) {
|
|
279
|
+
reasoningModel = app.provider(toolVariablesConfig.reasoning_model)?.model?.create({});
|
|
280
|
+
if (!reasoningModel) {
|
|
281
|
+
throw new Error("Reasoning model not found");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (toolVariablesConfig?.search_model) {
|
|
286
|
+
searchModel = app.provider(toolVariablesConfig.search_model);
|
|
287
|
+
if (!searchModel) {
|
|
288
|
+
throw new Error("Search model not found");
|
|
289
|
+
}
|
|
290
|
+
} */
|
|
291
|
+
|
|
292
|
+
if (!model) {
|
|
293
|
+
yield { result: "Model is required for executing the agentic retrieval tool" };
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let activeContexts = contexts;
|
|
298
|
+
let configuredReranker: ExuluReranker | undefined;
|
|
299
|
+
let configInstructions = "";
|
|
300
|
+
let logTrajectory = false;
|
|
301
|
+
let requiresPreselectedContexts = false;
|
|
302
|
+
let managedContextEnabled = false;
|
|
303
|
+
|
|
304
|
+
if (toolVariablesConfig) {
|
|
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";
|
|
311
|
+
|
|
312
|
+
activeContexts = contexts.filter(
|
|
313
|
+
(ctx) =>
|
|
314
|
+
toolVariablesConfig[ctx.id] === true ||
|
|
315
|
+
toolVariablesConfig[ctx.id] === "true" ||
|
|
316
|
+
toolVariablesConfig[ctx.id] === 1,
|
|
317
|
+
);
|
|
318
|
+
if (activeContexts.length === 0) activeContexts = contexts;
|
|
319
|
+
|
|
320
|
+
requiresPreselectedContexts = toolVariablesConfig["require_preselected_contexts"] === true || toolVariablesConfig["require_preselected_contexts"] === "true";
|
|
321
|
+
|
|
322
|
+
const rerankerId = toolVariablesConfig["reranker"];
|
|
323
|
+
|
|
324
|
+
if (rerankerId && rerankerId !== "none") {
|
|
325
|
+
configuredReranker = rerankers.find((r) => r.id === rerankerId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
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
|
+
|
|
350
|
+
const combinedInstructions = [
|
|
351
|
+
configInstructions ? `Configuration instructions: ${configInstructions}` : "",
|
|
352
|
+
adminInstructions ? `Admin instructions: ${adminInstructions}` : "",
|
|
353
|
+
userInstructions ? `User instructions: ${userInstructions}` : "",
|
|
354
|
+
]
|
|
355
|
+
.filter(Boolean)
|
|
356
|
+
.join("\n");
|
|
357
|
+
|
|
358
|
+
for await (const output of executeV3({
|
|
359
|
+
query,
|
|
360
|
+
contexts: activeContexts,
|
|
361
|
+
reranker: configuredReranker,
|
|
362
|
+
model,
|
|
363
|
+
user,
|
|
364
|
+
role,
|
|
365
|
+
customInstructions: combinedInstructions || undefined,
|
|
366
|
+
logTrajectory,
|
|
367
|
+
sessionId: sessionID,
|
|
368
|
+
preselectedItemIds,
|
|
369
|
+
})) {
|
|
370
|
+
yield { result: JSON.stringify(output) };
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
},
|
|
374
|
+
});
|
|
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
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { QueryType } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface StrategyConfig {
|
|
4
|
+
queryType: QueryType;
|
|
5
|
+
/** How many agent loop iterations are allowed */
|
|
6
|
+
stepBudget: number;
|
|
7
|
+
/** Which tool names from createRetrievalTools() are exposed */
|
|
8
|
+
retrieval_tools: string[];
|
|
9
|
+
/** Whether bash tools should be included */
|
|
10
|
+
include_bash: boolean;
|
|
11
|
+
instructions: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Per-strategy instructions
|
|
16
|
+
// These are intentionally in separate exported strings so they can be tuned
|
|
17
|
+
// without touching the rest of the code.
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const BASE_INSTRUCTIONS = `
|
|
21
|
+
You are an intelligent retrieval assistant. Your only job is to retrieve relevant information from
|
|
22
|
+
the available knowledge bases and return it. You do NOT answer the user's question yourself —
|
|
23
|
+
another component will do that based on what you retrieve.
|
|
24
|
+
|
|
25
|
+
Always respond in the SAME LANGUAGE as the user's query.
|
|
26
|
+
Always write search queries in the SAME LANGUAGE as the user's query — do NOT translate to English.
|
|
27
|
+
|
|
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.
|
|
32
|
+
2. After finding a relevant document, use get_more_content_from_{item} dynamic tools to load
|
|
33
|
+
additional pages/sections. The specific answer is often NOT in the first retrieved chunk —
|
|
34
|
+
always explore adjacent content before concluding.
|
|
35
|
+
3. If your first search returned related but not specific enough content, run a follow-up search
|
|
36
|
+
with more targeted terms or an alternative phrasing of the key concept.
|
|
37
|
+
|
|
38
|
+
Never give up after a single search — always try at least one follow-up before finishing.
|
|
39
|
+
|
|
40
|
+
When retrieval is complete (sufficient content found OR all reasonable strategies exhausted),
|
|
41
|
+
you MUST call the finish_retrieval tool — do NOT write a text conclusion.
|
|
42
|
+
`.trim();
|
|
43
|
+
|
|
44
|
+
export const AGGREGATE_INSTRUCTIONS = `
|
|
45
|
+
${BASE_INSTRUCTIONS}
|
|
46
|
+
|
|
47
|
+
STRATEGY: This is a COUNTING or AGGREGATION query.
|
|
48
|
+
- Use count_items_or_chunks exclusively — it accepts multiple knowledge bases in one call for efficiency
|
|
49
|
+
- Do NOT use search_content — it loads unnecessary data
|
|
50
|
+
- Return immediately after counting — one step is sufficient
|
|
51
|
+
- If the count needs a content filter, use content_query parameter
|
|
52
|
+
`.trim();
|
|
53
|
+
|
|
54
|
+
export const LIST_INSTRUCTIONS = `
|
|
55
|
+
${BASE_INSTRUCTIONS}
|
|
56
|
+
|
|
57
|
+
STRATEGY: This is a LISTING query — the user wants a list of matching items/documents.
|
|
58
|
+
|
|
59
|
+
Decision tree:
|
|
60
|
+
- "List documents BY NAME/TITLE" → search_items_by_name
|
|
61
|
+
- "List documents ABOUT a topic/subject" → search_content with includeContent: false
|
|
62
|
+
|
|
63
|
+
Always prefer search_content with includeContent: false for content-based listing:
|
|
64
|
+
- This searches actual document content and returns matching document names
|
|
65
|
+
- It does NOT load chunk text, keeping token use minimal
|
|
66
|
+
- Dynamic page-content tools will be created if the user needs to drill into specific documents
|
|
67
|
+
|
|
68
|
+
When to use search_items_by_name:
|
|
69
|
+
- Query explicitly mentions document titles or filename patterns
|
|
70
|
+
- User asks for documents whose NAME contains a keyword
|
|
71
|
+
|
|
72
|
+
Never set includeContent: true for a listing query unless explicitly asked for the actual text.
|
|
73
|
+
`.trim();
|
|
74
|
+
|
|
75
|
+
export const TARGETED_INSTRUCTIONS = `
|
|
76
|
+
${BASE_INSTRUCTIONS}
|
|
77
|
+
|
|
78
|
+
STRATEGY: This is a TARGETED query — the user wants specific information from a document.
|
|
79
|
+
|
|
80
|
+
Search language:
|
|
81
|
+
- Always write search queries in the SAME LANGUAGE as the user's query.
|
|
82
|
+
- Do NOT translate the query to English — the documents are indexed in their original language.
|
|
83
|
+
|
|
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"
|
|
101
|
+
|
|
102
|
+
Step 2+ — depth and follow-up:
|
|
103
|
+
- For any relevant document found with fewer than 5 chunks, use get_more_content_from_{item}
|
|
104
|
+
to load adjacent sections. The specific answer is often in a nearby chunk, not the top result.
|
|
105
|
+
- If the topic was found but the exact detail is missing, search again with more specific terms
|
|
106
|
+
(e.g., add a key technical term, parameter name, or section keyword to the query).
|
|
107
|
+
- Try alternative phrasings if the first query doesn't surface the right answer.
|
|
108
|
+
|
|
109
|
+
Product-specific filtering:
|
|
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
|
|
112
|
+
wide search. Never start with item_names filtering alone.
|
|
113
|
+
`.trim();
|
|
114
|
+
|
|
115
|
+
export const EXPLORATORY_INSTRUCTIONS = `
|
|
116
|
+
${BASE_INSTRUCTIONS}
|
|
117
|
+
|
|
118
|
+
STRATEGY: This is an EXPLORATORY query — general question requiring broad search.
|
|
119
|
+
|
|
120
|
+
Recommended approach:
|
|
121
|
+
1. Search each relevant knowledge base separately with hybrid search (includeContent: true, limit: 20) — one call per knowledge base
|
|
122
|
+
2. If results are insufficient: try alternative search terms or different search method
|
|
123
|
+
3. Use save_search_results + bash grep when you need to scan many results without context bloat
|
|
124
|
+
4. Use dynamic get_more_content_from_{item} tools to read adjacent pages when a relevant item is found
|
|
125
|
+
|
|
126
|
+
When to declare done:
|
|
127
|
+
- You have retrieved chunks that cover the key aspects of the query from all relevant knowledge bases
|
|
128
|
+
- OR you have tried 3+ different search strategies and found nothing relevant
|
|
129
|
+
|
|
130
|
+
Do NOT use count_items_or_chunks for exploratory queries — the user wants content, not statistics.
|
|
131
|
+
`.trim();
|
|
132
|
+
|
|
133
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
// Strategy map
|
|
135
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export const STRATEGIES: Record<QueryType, StrategyConfig> = {
|
|
138
|
+
aggregate: {
|
|
139
|
+
queryType: "aggregate",
|
|
140
|
+
stepBudget: 1,
|
|
141
|
+
retrieval_tools: ["count_items_or_chunks"],
|
|
142
|
+
include_bash: false,
|
|
143
|
+
instructions: AGGREGATE_INSTRUCTIONS,
|
|
144
|
+
},
|
|
145
|
+
list: {
|
|
146
|
+
queryType: "list",
|
|
147
|
+
stepBudget: 3,
|
|
148
|
+
retrieval_tools: ["count_items_or_chunks", "search_items_by_name", "search_content"],
|
|
149
|
+
include_bash: false,
|
|
150
|
+
instructions: LIST_INSTRUCTIONS,
|
|
151
|
+
},
|
|
152
|
+
targeted: {
|
|
153
|
+
queryType: "targeted",
|
|
154
|
+
stepBudget: 5,
|
|
155
|
+
retrieval_tools: ["search_items_by_name", "search_content", "save_search_results"],
|
|
156
|
+
include_bash: true,
|
|
157
|
+
instructions: TARGETED_INSTRUCTIONS,
|
|
158
|
+
},
|
|
159
|
+
exploratory: {
|
|
160
|
+
queryType: "exploratory",
|
|
161
|
+
stepBudget: 5,
|
|
162
|
+
retrieval_tools: [
|
|
163
|
+
"count_items_or_chunks",
|
|
164
|
+
"search_items_by_name",
|
|
165
|
+
"search_content",
|
|
166
|
+
"save_search_results",
|
|
167
|
+
],
|
|
168
|
+
include_bash: true,
|
|
169
|
+
instructions: EXPLORATORY_INSTRUCTIONS,
|
|
170
|
+
},
|
|
171
|
+
};
|