@exulu/backend 1.46.1 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.agents/skills/mintlify/SKILL.md +347 -0
  2. package/.editorconfig +15 -0
  3. package/.eslintrc.json +52 -0
  4. package/.jscpd.json +18 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc.json +12 -0
  7. package/CHANGELOG.md +15 -2
  8. package/README.md +747 -0
  9. package/SECURITY.md +5 -0
  10. package/dist/index.cjs +12015 -10496
  11. package/dist/index.d.cts +725 -667
  12. package/dist/index.d.ts +725 -667
  13. package/dist/index.js +12034 -10508
  14. package/ee/LICENSE.md +62 -0
  15. package/ee/agentic-retrieval/index.ts +1109 -0
  16. package/ee/documents/THIRD_PARTY_LICENSES/docling.txt +31 -0
  17. package/ee/documents/processing/build_pdf_processor.sh +35 -0
  18. package/ee/documents/processing/chunk_markdown.py +263 -0
  19. package/ee/documents/processing/doc_processor.ts +635 -0
  20. package/ee/documents/processing/pdf_processor.spec +115 -0
  21. package/ee/documents/processing/pdf_to_markdown.py +420 -0
  22. package/ee/documents/processing/requirements.txt +4 -0
  23. package/ee/entitlements.ts +49 -0
  24. package/ee/markdown.ts +686 -0
  25. package/ee/queues/decorator.ts +140 -0
  26. package/ee/queues/queues.ts +156 -0
  27. package/ee/queues/server.ts +6 -0
  28. package/ee/rbac-resolver.ts +51 -0
  29. package/ee/rbac-update.ts +111 -0
  30. package/ee/schemas.ts +347 -0
  31. package/ee/tokenizer.ts +80 -0
  32. package/ee/workers.ts +1423 -0
  33. package/eslint.config.js +88 -0
  34. package/jest.config.ts +25 -0
  35. package/license.md +73 -49
  36. package/mintlify-docs/.mintignore +7 -0
  37. package/mintlify-docs/AGENTS.md +33 -0
  38. package/mintlify-docs/CLAUDE.MD +50 -0
  39. package/mintlify-docs/CONTRIBUTING.md +32 -0
  40. package/mintlify-docs/LICENSE +21 -0
  41. package/mintlify-docs/README.md +55 -0
  42. package/mintlify-docs/ai-tools/claude-code.mdx +43 -0
  43. package/mintlify-docs/ai-tools/cursor.mdx +39 -0
  44. package/mintlify-docs/ai-tools/windsurf.mdx +39 -0
  45. package/mintlify-docs/api-reference/core-types/agent-types.mdx +110 -0
  46. package/mintlify-docs/api-reference/core-types/analytics-types.mdx +95 -0
  47. package/mintlify-docs/api-reference/core-types/configuration-types.mdx +83 -0
  48. package/mintlify-docs/api-reference/core-types/evaluation-types.mdx +106 -0
  49. package/mintlify-docs/api-reference/core-types/job-types.mdx +135 -0
  50. package/mintlify-docs/api-reference/core-types/overview.mdx +73 -0
  51. package/mintlify-docs/api-reference/core-types/prompt-types.mdx +102 -0
  52. package/mintlify-docs/api-reference/core-types/rbac-types.mdx +163 -0
  53. package/mintlify-docs/api-reference/core-types/session-types.mdx +77 -0
  54. package/mintlify-docs/api-reference/core-types/user-management.mdx +112 -0
  55. package/mintlify-docs/api-reference/core-types/workflow-types.mdx +88 -0
  56. package/mintlify-docs/api-reference/core-types.mdx +585 -0
  57. package/mintlify-docs/api-reference/dynamic-types.mdx +851 -0
  58. package/mintlify-docs/api-reference/endpoint/create.mdx +4 -0
  59. package/mintlify-docs/api-reference/endpoint/delete.mdx +4 -0
  60. package/mintlify-docs/api-reference/endpoint/get.mdx +4 -0
  61. package/mintlify-docs/api-reference/endpoint/webhook.mdx +4 -0
  62. package/mintlify-docs/api-reference/introduction.mdx +661 -0
  63. package/mintlify-docs/api-reference/mutations.mdx +1012 -0
  64. package/mintlify-docs/api-reference/openapi.json +217 -0
  65. package/mintlify-docs/api-reference/queries.mdx +1154 -0
  66. package/mintlify-docs/backend/introduction.mdx +218 -0
  67. package/mintlify-docs/changelog.mdx +293 -0
  68. package/mintlify-docs/community-edition.mdx +304 -0
  69. package/mintlify-docs/core/exulu-agent/api-reference.mdx +894 -0
  70. package/mintlify-docs/core/exulu-agent/configuration.mdx +690 -0
  71. package/mintlify-docs/core/exulu-agent/introduction.mdx +552 -0
  72. package/mintlify-docs/core/exulu-app/api-reference.mdx +481 -0
  73. package/mintlify-docs/core/exulu-app/configuration.mdx +319 -0
  74. package/mintlify-docs/core/exulu-app/introduction.mdx +117 -0
  75. package/mintlify-docs/core/exulu-authentication.mdx +810 -0
  76. package/mintlify-docs/core/exulu-chunkers/api-reference.mdx +1011 -0
  77. package/mintlify-docs/core/exulu-chunkers/configuration.mdx +596 -0
  78. package/mintlify-docs/core/exulu-chunkers/introduction.mdx +403 -0
  79. package/mintlify-docs/core/exulu-context/api-reference.mdx +911 -0
  80. package/mintlify-docs/core/exulu-context/configuration.mdx +648 -0
  81. package/mintlify-docs/core/exulu-context/introduction.mdx +394 -0
  82. package/mintlify-docs/core/exulu-database.mdx +811 -0
  83. package/mintlify-docs/core/exulu-default-agents.mdx +545 -0
  84. package/mintlify-docs/core/exulu-eval/api-reference.mdx +772 -0
  85. package/mintlify-docs/core/exulu-eval/configuration.mdx +680 -0
  86. package/mintlify-docs/core/exulu-eval/introduction.mdx +459 -0
  87. package/mintlify-docs/core/exulu-logging.mdx +464 -0
  88. package/mintlify-docs/core/exulu-otel.mdx +670 -0
  89. package/mintlify-docs/core/exulu-queues/api-reference.mdx +648 -0
  90. package/mintlify-docs/core/exulu-queues/configuration.mdx +650 -0
  91. package/mintlify-docs/core/exulu-queues/introduction.mdx +474 -0
  92. package/mintlify-docs/core/exulu-reranker/api-reference.mdx +630 -0
  93. package/mintlify-docs/core/exulu-reranker/configuration.mdx +663 -0
  94. package/mintlify-docs/core/exulu-reranker/introduction.mdx +516 -0
  95. package/mintlify-docs/core/exulu-tool/api-reference.mdx +723 -0
  96. package/mintlify-docs/core/exulu-tool/configuration.mdx +805 -0
  97. package/mintlify-docs/core/exulu-tool/introduction.mdx +539 -0
  98. package/mintlify-docs/core/exulu-variables/api-reference.mdx +699 -0
  99. package/mintlify-docs/core/exulu-variables/configuration.mdx +736 -0
  100. package/mintlify-docs/core/exulu-variables/introduction.mdx +511 -0
  101. package/mintlify-docs/development.mdx +94 -0
  102. package/mintlify-docs/docs.json +248 -0
  103. package/mintlify-docs/enterprise-edition.mdx +538 -0
  104. package/mintlify-docs/essentials/code.mdx +35 -0
  105. package/mintlify-docs/essentials/images.mdx +59 -0
  106. package/mintlify-docs/essentials/markdown.mdx +88 -0
  107. package/mintlify-docs/essentials/navigation.mdx +87 -0
  108. package/mintlify-docs/essentials/reusable-snippets.mdx +110 -0
  109. package/mintlify-docs/essentials/settings.mdx +318 -0
  110. package/mintlify-docs/favicon.svg +3 -0
  111. package/mintlify-docs/frontend/introduction.mdx +39 -0
  112. package/mintlify-docs/getting-started.mdx +267 -0
  113. package/mintlify-docs/guides/custom-agent.mdx +608 -0
  114. package/mintlify-docs/guides/first-agent.mdx +315 -0
  115. package/mintlify-docs/images/admin_ui.png +0 -0
  116. package/mintlify-docs/images/contexts.png +0 -0
  117. package/mintlify-docs/images/create_agents.png +0 -0
  118. package/mintlify-docs/images/evals.png +0 -0
  119. package/mintlify-docs/images/graphql.png +0 -0
  120. package/mintlify-docs/images/graphql_api.png +0 -0
  121. package/mintlify-docs/images/hero-dark.png +0 -0
  122. package/mintlify-docs/images/hero-light.png +0 -0
  123. package/mintlify-docs/images/hero.png +0 -0
  124. package/mintlify-docs/images/knowledge_sources.png +0 -0
  125. package/mintlify-docs/images/mcp.png +0 -0
  126. package/mintlify-docs/images/scaling.png +0 -0
  127. package/mintlify-docs/index.mdx +411 -0
  128. package/mintlify-docs/logo/dark.svg +9 -0
  129. package/mintlify-docs/logo/light.svg +9 -0
  130. package/mintlify-docs/partners.mdx +558 -0
  131. package/mintlify-docs/products.mdx +77 -0
  132. package/mintlify-docs/snippets/snippet-intro.mdx +4 -0
  133. package/mintlify-docs/styles.css +207 -0
  134. package/{documentation → old-documentation}/logging.md +3 -3
  135. package/package.json +35 -4
  136. package/skills-lock.json +10 -0
  137. package/types/context-processor.ts +45 -0
  138. package/types/exulu-table-definition.ts +79 -0
  139. package/types/file-types.ts +18 -0
  140. package/types/models/agent.ts +10 -12
  141. package/types/models/exulu-agent-tool-config.ts +11 -0
  142. package/types/models/rate-limiter-rules.ts +7 -0
  143. package/types/provider-config.ts +21 -0
  144. package/types/queue-config.ts +16 -0
  145. package/types/rbac-rights-modes.ts +1 -0
  146. package/types/statistics.ts +20 -0
  147. package/types/workflow.ts +31 -0
  148. package/changelogs/10.11.2025_03.12.2025.md +0 -316
  149. package/types/models/agent-backend.ts +0 -15
  150. /package/{documentation → old-documentation}/otel.md +0 -0
  151. /package/{documentation → old-documentation}/patch-older-releases.md +0 -0
@@ -0,0 +1,1109 @@
1
+ import { z } from "zod";
2
+ import {
3
+ stepCountIs,
4
+ tool,
5
+ type LanguageModel,
6
+ type Tool as AITool,
7
+ Output,
8
+ generateText,
9
+ } from "ai";
10
+ import { type ExuluContext } from "@SRC/exulu/context";
11
+ import type { ExuluReranker } from "@SRC/exulu/reranker";
12
+ import { ExuluTool } from "@SRC/exulu/tool";
13
+ import { sanitizeToolName } from "@SRC/utils/sanitize-tool-name.ts";
14
+ import type { User } from "@EXULU_TYPES/models/user";
15
+ import { postgresClient } from "@SRC/postgres/client";
16
+ import { zodToJsonSchema } from "zod-to-json-schema";
17
+ import { preprocessQuery } from "@SRC/utils/query-preprocessing";
18
+ import { getChunksTableName, getTableName } from "@SRC/exulu/context";
19
+ import type { SearchFilters } from "@SRC/graphql/types";
20
+ import type { VectorSearchChunkResult } from "@SRC/graphql/resolvers/vector-search";
21
+ import { convertContextToTableDefinition } from "@SRC/graphql/utilities/convert-context-to-table-definition";
22
+ import { applyFilters } from "@SRC/graphql/resolvers/apply-filters";
23
+ import { applyAccessControl } from "@SRC/graphql/utilities/access-control";
24
+ import { withRetry } from "@SRC/utils/with-retry";
25
+ import { checkLicense } from "@EE/entitlements";
26
+ /**
27
+ * Agentic Retrieval Tool
28
+ *
29
+ * This module provides a single intelligent retrieval agent that uses multiple tools
30
+ * to efficiently retrieve relevant information from ALL available Exulu knowledge bases.
31
+ *
32
+ * The agent can:
33
+ * - List and understand available contexts
34
+ * - Search chunks across contexts using different methods (vector, keyword, hybrid)
35
+ * - Pre-filter items by metadata
36
+ * - Expand chunks to get surrounding context
37
+ * - Decide when to return just metadata vs full content
38
+ */
39
+
40
+ /**
41
+ * Tool result interface for proper citation formatting
42
+ */
43
+ interface ToolResult {
44
+ item_name: string;
45
+ item_id: string;
46
+ context: string;
47
+ chunk_id?: string;
48
+ chunk_index?: number;
49
+ chunk_content?: string;
50
+ metadata?: any;
51
+ }
52
+
53
+ interface AgenticRetrievalOutput {
54
+ reasoning: {
55
+ text: string;
56
+ tools: {
57
+ name: string;
58
+ id: string;
59
+ input: any;
60
+ output: VectorSearchChunkResult[];
61
+ }[];
62
+ chunks: any[];
63
+ }[];
64
+ chunks: any[];
65
+ usage: any[];
66
+ totalTokens: number;
67
+ }
68
+
69
+ const baseInstructions = `
70
+ You are an intelligent information retrieval assistant with access to multiple knowledge bases. You MUST do all your reasoning and
71
+ outputs in the same language as the user query.
72
+
73
+ Your goal is to efficiently retrieve the most relevant information to answer user queries. You don't answer the question yourself, you only
74
+ retrieve the information and return it, another tool will answer the question based on the information you retrieve.
75
+
76
+ CRITICAL: STRUCTURED REASONING PROCESS
77
+ You MUST follow this structured thinking process for EVERY step. Keep outputs VERY SHORT and in the same language as the user query - ideally one line each:
78
+
79
+ 1. BEFORE EACH TOOL CALL - Output your search strategy in ONE CONCISE LINE:
80
+ Format: 🔍 [What you'll search]: [Tool(s)] [Method/Filters] → [Expected outcome]
81
+
82
+ Examples:
83
+ - "🔍 Searching for WPA-2 config in DPO-1 docs: search_items_products (name:DPO-1) → search_chunks_products (hybrid) → Expecting setup instructions"
84
+ - "🔍 Listing tourism items: search_items_hamburg (name contains 'tourism') → Document list"
85
+ - "🔍 Broad search for elevator data: search_chunks_* (hybrid, all contexts) → Statistical data"
86
+
87
+ 2. AFTER RECEIVING RESULTS - Reflect in ONE CONCISE LINE:
88
+ Format: 💭 [Count] results | [Relevance] | [Next action]
89
+
90
+ Examples:
91
+ - "💭 5 chunks found | Highly relevant WPA-2 instructions | Sufficient - returning results"
92
+ - "💭 12 items with 'tourism' | Complete list | Done"
93
+ - "💭 0 results in context_1 | Not found | Trying remaining contexts"
94
+
95
+ IMPORTANT:
96
+ - ONE LINE per reasoning block - be extremely concise
97
+ - Use the same language as the user query
98
+ - Focus only on: what, tool/method, outcome
99
+
100
+ Choose your strategy based on the query type:
101
+
102
+ FOR LISTING QUERIES (e.g., "list all documents about X" or "what items mention Y"):
103
+
104
+ You have two options, you can use the search_items_by_name_[context_id] tool to find the items by name, if it is likely that
105
+ the name includes a specific keyword, for example when looking for documentation regarding a specific product. Or you can use
106
+ the search_[context_id] tool with the parameter "includeContent: false" to search inside the actual content of all or specific
107
+ items, if you are looking for any items that might be relevant or contain information relevant to the query, but do not need
108
+ the actual content in your response.
109
+
110
+ IMPORTANT: For listing queries, NEVER set includeContent: true unless you need the actual text content to answer the question
111
+
112
+ FOR TARGETED QUERIES ABOUT SPECIFIC ITEMS (e.g., "how do I configure WPA-2 on my DPO-1? router" or "search in Document ABC"):
113
+ TWO-STEP PATTERN:
114
+ 1. STEP 1 - Find the items:
115
+ As described above, you have two options, you can use the search_items_by_name_[context_id] tool to find the items by name, if it is likely that
116
+ the name includes a specific keyword, for example when looking for documentation regarding a specific product. Or you can use
117
+ the search_[context_id] tool with the parameter "includeContent: false" to search inside the actual content of all or specific
118
+ items, if you are looking for any items that might be relevant or contain information relevant to the query, but do not need
119
+ the actual content in your response.
120
+ 2. STEP 2 - If step 1 returned any items, search for relevant information within those items: Use search_[context_id] with
121
+ hybrid search method. Example: search_[context_id] with hybrid search method and parameters item_name: 'DPO-1'
122
+ This searches only within the specific items you found. If no items were found in step 1 you should still
123
+ do the search_[context_id] but without pre-filtering them.
124
+
125
+ Note that the query input for the search_[context_id] tools should not be used to search for things like "Page 2",
126
+ "Section 3", "Chapter 4", etc. but rather be used to search for specific information or answers within the
127
+ content of the items.
128
+
129
+ ONLY say "no information found" if you have:
130
+ ✓ Searched ALL available contexts (not just likely ones)
131
+ ✓ Tried hybrid, keyword, AND semantic search in each context
132
+ ✓ Tried variations of the search terms
133
+ ✓ Confirmed zero results across all attempts
134
+
135
+ IMPORTANT OPTIMIZATION RULES:
136
+
137
+ ⚠️ CRITICAL: Always set includeContent: false when:
138
+ - User asks for a list, overview, or count of documents/items
139
+ - User wants to know "which documents" or "what items" without needing their content
140
+ - You only need item names/metadata to answer the query
141
+ - You can ALWAYS fetch the actual content later if needed with includeContent: true
142
+
143
+ ✓ Only set includeContent: true (or use default) when:
144
+ - User needs specific information, details, or answers from the content
145
+ - User asks "how to", "what does it say about", "explain", etc.
146
+ - You need the actual text to answer the question
147
+
148
+ Search Method Selection (for search_* tools):
149
+ - Use 'hybrid' method by default for best relevance (combines semantic understanding + keyword matching)
150
+ - Use 'keyword' method for exact term matching (technical terms, product names, IDs, specific phrases)
151
+ - Use 'semantic' method for conceptual queries where synonyms and paraphrasing matter most
152
+
153
+ Filtering and Limits:
154
+ - Limit results appropriately (don't retrieve more than needed)
155
+ `;
156
+
157
+ // Generator function
158
+ function createCustomAgenticRetrievalToolLoopAgent({
159
+ tools,
160
+ model,
161
+ customInstructions,
162
+ }: {
163
+ language?: string;
164
+ tools: Record<string, AITool>;
165
+ model: LanguageModel;
166
+ customInstructions?: string;
167
+ }): {
168
+ generate: (args: {
169
+ query: string;
170
+ onFinish: (output: AgenticRetrievalOutput) => void;
171
+ }) => AsyncGenerator<AgenticRetrievalOutput>;
172
+ } {
173
+ return {
174
+ generate: async function* ({
175
+ reranker,
176
+ query,
177
+ onFinish,
178
+ }: {
179
+ reranker?: ExuluReranker;
180
+ query: string;
181
+ onFinish: (output: any) => void;
182
+ }): AsyncGenerator<any> {
183
+ let finished = false;
184
+ let maxSteps = 2;
185
+ let currentStep = 0;
186
+ const output: AgenticRetrievalOutput = {
187
+ reasoning: [],
188
+ chunks: [],
189
+ usage: [],
190
+ totalTokens: 0,
191
+ };
192
+
193
+ // Every uneven step (1, 3, 5 etc...) we force the model
194
+ // to reason about what steps might be needed next. We use
195
+ // the generateObject function to get an output of those steps
196
+ // that looks like this:
197
+ // {
198
+ // reasoning: string;
199
+ // finished: boolean;
200
+ // }
201
+ // With "finished" being true if the agent decides no further
202
+ // steps are needed.
203
+ // For the even steps (2, 4, 6 etc...) we use generateText and
204
+ // set toolChoice: 'required' to force a tool call.
205
+
206
+ let dynamicTools: Record<string, AITool> = {};
207
+
208
+ while (!finished && currentStep < maxSteps) {
209
+ currentStep++;
210
+
211
+ console.log("[EXULU] Agentic retrieval step", currentStep);
212
+
213
+ const systemPrompt = `
214
+ ${baseInstructions}
215
+
216
+ AVAILABLE TOOLS:
217
+ ${Object.keys({ ...tools, ...dynamicTools })
218
+ .map(
219
+ (tool) => `
220
+ <tool_${tool}>
221
+ <name>${tool}</name>
222
+ <description>${tools[tool]?.description}</description>
223
+ ${
224
+ tools[tool]?.inputSchema
225
+ ? `
226
+ <inputSchema>${JSON.stringify(
227
+ zodToJsonSchema(tools[tool]?.inputSchema as z.ZodObject<any>), null, 2
228
+ )}</inputSchema>
229
+ `
230
+ : ""
231
+ }
232
+ </tool_${tool}>
233
+ `,
234
+ )
235
+ .join("\n\n")}
236
+
237
+ ${
238
+ customInstructions
239
+ ? `
240
+ CUSTOM INSTRUCTIONS: ${customInstructions}`
241
+ : ""
242
+ }
243
+ `;
244
+ // First generateText call with retry logic
245
+ let reasoningOutput: Awaited<ReturnType<typeof generateText>>;
246
+ try {
247
+ reasoningOutput = await withRetry(async () => {
248
+ console.log("[EXULU] Generating reasoning for step", currentStep);
249
+ return await generateText({
250
+ model: model,
251
+ output: Output.object({
252
+ schema: z.object({
253
+ reasoning: z
254
+ .string()
255
+ .describe(
256
+ "The reasoning for the next step and why the agent needs to take this step. It MUST start with 'I must call tool XYZ', and MUST include the inputs for that tool.",
257
+ ),
258
+ finished: z
259
+ .boolean()
260
+ .describe(
261
+ "Whether the agent has finished meaning no further steps are needed, this should only be true if the agent believes no further tool calls are needed to get the relevant information for the query.",
262
+ ),
263
+ }),
264
+ }),
265
+ toolChoice: "none",
266
+ system: systemPrompt,
267
+ prompt: `
268
+ Original query: ${query}
269
+
270
+ Previous step reasoning and output:
271
+
272
+ ${output.reasoning
273
+ .map(
274
+ (reasoning, index) => `
275
+ <step_${index + 1}>
276
+ <reasoning>
277
+ ${reasoning.text}
278
+ </reasoning>
279
+
280
+ ${
281
+ reasoning.chunks
282
+ ? `<retrieved_chunks>
283
+ ${reasoning.chunks
284
+ .map(
285
+ (chunk) => `
286
+ ${chunk.item_name} - ${chunk.item_id} - ${chunk.context} - ${chunk.chunk_id} - ${chunk.chunk_index}
287
+ `,
288
+ )
289
+ .join("\n")}
290
+ </retrieved_chunks>`
291
+ : ""
292
+ }
293
+
294
+ ${
295
+ reasoning.tools
296
+ ? `
297
+ <used_tools>
298
+ ${reasoning.tools
299
+ .map(
300
+ (tool) => `
301
+ ${tool.name} - ${tool.id} - ${tool.input}
302
+ `,
303
+ )
304
+ .join("\n")}
305
+ </used_tools>
306
+ `
307
+ : ""
308
+ }
309
+
310
+ </step_${index + 1}>
311
+ `,
312
+ )
313
+ .join("\n")}
314
+ `,
315
+ stopWhen: [stepCountIs(1)],
316
+ });
317
+ });
318
+ console.log("[EXULU] Reasoning generated for step", currentStep);
319
+ } catch (error) {
320
+ console.error("[EXULU] Failed to generate reasoning after 3 retries:", error);
321
+ throw error;
322
+ }
323
+
324
+ const { reasoning: briefing, finished } = reasoningOutput?.output || {};
325
+
326
+ const { usage: reasoningUsage } = reasoningOutput || {};
327
+
328
+ output.usage.push(reasoningUsage);
329
+
330
+ if (finished) {
331
+ console.log("[EXULU] Agentic retrieval finished for step", currentStep);
332
+ break;
333
+ }
334
+
335
+ // Second generateText call with retry logic
336
+ let toolOutput: Awaited<ReturnType<typeof generateText>>;
337
+ try {
338
+ toolOutput = await withRetry(async () => {
339
+ console.log("[EXULU] Generating tool output for step", currentStep);
340
+ return await generateText({
341
+ model: model,
342
+ tools: { ...tools, ...dynamicTools },
343
+ toolChoice: "required",
344
+ prompt: `${briefing}`,
345
+ stopWhen: [stepCountIs(1)],
346
+ });
347
+ });
348
+ console.log("[EXULU] Tool output generated for step", currentStep);
349
+ } catch (error) {
350
+ console.error("[EXULU] Failed to generate tool output after 3 retries:", error);
351
+ throw error;
352
+ }
353
+
354
+ const toolResults = toolOutput.toolResults;
355
+ const toolCalls = toolOutput.toolCalls;
356
+
357
+ let chunks: any[] = [];
358
+ console.log("[EXULU] Processing tool results for step", currentStep);
359
+ if (Array.isArray(toolResults)) {
360
+ chunks = toolResults
361
+ .map((result) => {
362
+ let chunks: any[] = [];
363
+ if (typeof result.output === "string") {
364
+ chunks = JSON.parse(result.output);
365
+ } else {
366
+ chunks = result.output as any[];
367
+ }
368
+ console.log("[EXULU] Chunks", chunks);
369
+ return chunks.map((chunk) => ({
370
+ ...chunk,
371
+ context: {
372
+ name: chunk.context ? chunk.context.replaceAll("_", " ") : "",
373
+ id: chunk.context,
374
+ },
375
+ }));
376
+ })
377
+ .flat();
378
+ }
379
+ if (chunks) {
380
+ if (reranker) {
381
+ console.log(
382
+ "[EXULU] Reranking chunks for step, using reranker",
383
+ reranker.name + "(" + reranker.id + ")",
384
+ "for step",
385
+ currentStep,
386
+ " for " + chunks?.length + " chunks",
387
+ );
388
+ chunks = await reranker.run(query, chunks);
389
+ console.log(
390
+ "[EXULU] Reranked chunks for step",
391
+ currentStep,
392
+ "using reranker",
393
+ reranker.name + "(" + reranker.id + ")",
394
+ " resulting in ",
395
+ chunks?.length + " chunks",
396
+ );
397
+ }
398
+
399
+ output.chunks.push(...chunks);
400
+ }
401
+
402
+ console.log("[EXULU] Pushing reasoning for step", currentStep);
403
+
404
+ const exludedContent = toolCalls?.some(
405
+ (toolCall) =>
406
+ toolCall.input?.includeContent === false ||
407
+ toolCall.toolName.startsWith("search_items_by_name"),
408
+ );
409
+ // Create chunk specific tools
410
+
411
+ for (const chunk of chunks) {
412
+ const getMoreToolName = sanitizeToolName("get_more_content_from_" + chunk.item_name);
413
+ const { db } = await postgresClient();
414
+
415
+ if (!dynamicTools[getMoreToolName]) {
416
+ const chunksTable = getChunksTableName(chunk.context.id);
417
+ const countChunksQuery = await db
418
+ .from(chunksTable)
419
+ .where({ source: chunk.item_id })
420
+ .count("id");
421
+ const chunksCount = Number(countChunksQuery[0].count) || 0;
422
+
423
+ if (chunksCount > 1) {
424
+ dynamicTools[getMoreToolName] = tool({
425
+ description: `The item ${chunk.item_name} has a total of${chunksCount} chunks, this tool allows you to get more content from this item across all its pages / chunks.`,
426
+ inputSchema: z.object({
427
+ from_index: z
428
+ .number()
429
+ .default(1)
430
+ .describe("The index of the chunk to start from."),
431
+ to_index: z
432
+ .number()
433
+ .max(chunksCount)
434
+ .describe("The index of the chunk to end at, max is " + chunksCount),
435
+ }),
436
+ execute: async ({ from_index, to_index }) => {
437
+ const chunks = await db(chunksTable)
438
+ .select("*")
439
+ .where("source", chunk.item_id)
440
+ .whereBetween("chunk_index", [from_index, to_index])
441
+ .orderBy("chunk_index", "asc");
442
+ return JSON.stringify(
443
+ chunks.map((resultChunk) => ({
444
+ chunk_content: resultChunk.content,
445
+ chunk_index: resultChunk.chunk_index,
446
+ chunk_id: resultChunk.id,
447
+ chunk_source: resultChunk.source,
448
+ chunk_metadata: resultChunk.metadata,
449
+ item_id: chunk.item_id,
450
+ item_name: chunk.item_name,
451
+ context: chunk.context?.id,
452
+ })),
453
+ null,
454
+ 2,
455
+ );
456
+ },
457
+ });
458
+ }
459
+ }
460
+
461
+ if (exludedContent) {
462
+ const getContentToolName = sanitizeToolName(
463
+ "get_" + chunk.item_name + "_page_" + chunk.chunk_index + "_content",
464
+ );
465
+ dynamicTools[getContentToolName] = tool({
466
+ description: `Get the content of the page ${chunk.chunk_index} for the item ${chunk.item_name}`,
467
+ inputSchema: z.object({
468
+ reasoning: z
469
+ .string()
470
+ .describe("The reasoning for why you need to get the content of the page."),
471
+ }),
472
+ execute: async ({ reasoning }) => {
473
+ const { db } = await postgresClient();
474
+ const chunksTable = getChunksTableName(chunk.context.id);
475
+ const resultChunks = await db(chunksTable)
476
+ .select("*")
477
+ .where("id", chunk.chunk_id)
478
+ .limit(1);
479
+ if (!resultChunks || !resultChunks[0]) {
480
+ return null;
481
+ }
482
+
483
+ return JSON.stringify(
484
+ [
485
+ {
486
+ reasoning: reasoning,
487
+ chunk_content: resultChunks[0].content,
488
+ chunk_index: resultChunks[0].chunk_index,
489
+ chunk_id: resultChunks[0].id,
490
+ chunk_source: resultChunks[0].source,
491
+ chunk_metadata: resultChunks[0].metadata,
492
+ chunk_created_at: resultChunks[0].chunk_created_at,
493
+ item_id: chunk.item_id,
494
+ item_name: chunk.item_name,
495
+ context: chunk.context?.id,
496
+ },
497
+ ],
498
+ null,
499
+ 2,
500
+ );
501
+ },
502
+ });
503
+ }
504
+ }
505
+
506
+ output.reasoning.push({
507
+ text: briefing,
508
+ chunks: chunks || [],
509
+ tools:
510
+ toolCalls?.length > 0
511
+ ? toolCalls.map((toolCall) => ({
512
+ name: toolCall.toolName,
513
+ id: toolCall.toolCallId,
514
+ input: toolCall.input,
515
+ output: chunks,
516
+ }))
517
+ : [],
518
+ });
519
+
520
+ const { usage: toolUsage } = toolOutput || {};
521
+ console.log("[EXULU] Pushing tool usage for step", currentStep);
522
+ output.usage.push(toolUsage);
523
+
524
+ console.log(`[EXULU] Agentic retrieval step ${currentStep} completed`);
525
+ console.log("[EXULU] Agentic retrieval step output", output);
526
+
527
+ yield output;
528
+ }
529
+ const totalTokens = output.usage.reduce((acc, usage) => acc + (usage.totalTokens || 0), 0);
530
+ output.totalTokens = totalTokens;
531
+
532
+ console.log("[EXULU] Agentic retrieval finished", output);
533
+
534
+ // yield output;
535
+ onFinish(output);
536
+ },
537
+ };
538
+ }
539
+
540
+ /**
541
+ * Creates an agentic retrieval tool that uses ToolLoopAgent to reason about
542
+ * and retrieve relevant information across ALL available knowledge bases
543
+ */
544
+ const createAgenticRetrievalAgent = ({
545
+ contexts,
546
+ user,
547
+ role,
548
+ model,
549
+ instructions: custom,
550
+ projectRetrievalTool,
551
+ language = "eng",
552
+ }: {
553
+ contexts: ExuluContext[];
554
+ user?: User;
555
+ role?: string;
556
+ model: LanguageModel; // LanguageModel from Vercel AI SDK
557
+ instructions?: string;
558
+ projectRetrievalTool?: ExuluTool;
559
+ language?: string;
560
+ }): {
561
+ generate: (args: {
562
+ query: string;
563
+ reranker?: ExuluReranker;
564
+ onFinish: (output: AgenticRetrievalOutput) => void;
565
+ }) => AsyncGenerator<AgenticRetrievalOutput>;
566
+ } => {
567
+ // Create the system instructions for the agent
568
+
569
+ const searchItemsByNameTool = {
570
+ search_items_by_name: tool({
571
+ description: `
572
+ Search for relevant items by name across the available knowledge bases.`,
573
+ inputSchema: z.object({
574
+ knowledge_base_ids: z.array(z.enum(contexts.map((ctx) => ctx.id) as [string, ...string[]]))
575
+ .describe(`
576
+ The available knowledge bases are:
577
+ ${contexts
578
+ .map(
579
+ (ctx) => `
580
+ <knowledge_base>
581
+ <id>${ctx.id}</id>
582
+ <name>${ctx.name}</name>
583
+ <description>${ctx.description}</description>
584
+ </knowledge_base>
585
+ `,
586
+ )
587
+ .join("\n")}
588
+ `),
589
+ item_name: z.string().describe("The name of the item to search for."),
590
+ limit: z
591
+ .number()
592
+ .default(100)
593
+ .describe(
594
+ "Maximum number of items to return (max 400), if searching through multiple knowledge bases, the limit is applied for each knowledge base individually.",
595
+ ),
596
+ }),
597
+ execute: async ({ item_name, limit, knowledge_base_ids }) => {
598
+ if (!knowledge_base_ids?.length) {
599
+ // Default to all
600
+ knowledge_base_ids = contexts.map((ctx) => ctx.id);
601
+ }
602
+
603
+ let itemFilters: SearchFilters = [];
604
+ if (item_name) {
605
+ itemFilters.push({ name: { contains: item_name } });
606
+ }
607
+
608
+ const { db } = await postgresClient();
609
+
610
+ const results = await Promise.all(
611
+ knowledge_base_ids.map(async (knowledge_base_id) => {
612
+ const ctx = contexts.find(
613
+ (ctx) =>
614
+ ctx.id === knowledge_base_id ||
615
+ ctx.id.toLowerCase().includes(knowledge_base_id.toLowerCase()),
616
+ );
617
+ if (!ctx) {
618
+ console.error(
619
+ "[EXULU] Knowledge base ID that was provided to search items by name not found.",
620
+ knowledge_base_id,
621
+ );
622
+ throw new Error(
623
+ "Knowledge base ID that was provided to search items by name not found.",
624
+ );
625
+ }
626
+ let itemsQuery = db(getTableName(ctx.id) + " as items").select([
627
+ "items.id as item_id",
628
+ "items.name as item_name",
629
+ "items.external_id as item_external_id",
630
+ db.raw('items."updatedAt" as item_updated_at'),
631
+ db.raw('items."createdAt" as item_created_at'),
632
+ ...ctx.fields.map((field) => `items.${field.name} as ${field.name}`),
633
+ ]);
634
+
635
+ if (!limit) {
636
+ limit = 100;
637
+ }
638
+ limit = Math.min(limit, 400);
639
+ itemsQuery = itemsQuery.limit(limit);
640
+
641
+ const tableDefinition = convertContextToTableDefinition(ctx);
642
+ itemsQuery = applyFilters(itemsQuery, itemFilters || [], tableDefinition, "items");
643
+ itemsQuery = applyAccessControl(tableDefinition, itemsQuery, user, "items");
644
+
645
+ const items = await itemsQuery;
646
+
647
+ return items?.map((item) => ({
648
+ ...item,
649
+ context: ctx.id,
650
+ }));
651
+ }),
652
+ );
653
+
654
+ const items = results.flat();
655
+
656
+ const formattedResults: (ToolResult | null)[] = await Promise.all(
657
+ items.map(async (item) => {
658
+ if (!item.item_id || !item.context) {
659
+ console.error("[EXULU] Item id and context are required to get chunks.", item);
660
+ throw new Error("Item id is required to get chunks.");
661
+ }
662
+
663
+ const chunksTable = getChunksTableName(item.context);
664
+ const chunks: any[] = await db
665
+ .from(chunksTable)
666
+ .select(["id", "source", "metadata"])
667
+ .where("source", item.item_id)
668
+ .limit(1);
669
+
670
+ if (!chunks || !chunks[0]) {
671
+ return null;
672
+ }
673
+
674
+ return {
675
+ item_name: item.item_name,
676
+ item_id: item.item_id,
677
+ context: item.context || "",
678
+ chunk_id: chunks[0].id,
679
+ chunk_index: 1,
680
+ chunk_content: undefined,
681
+ metadata: chunks[0].metadata,
682
+ };
683
+ }),
684
+ );
685
+
686
+ return JSON.stringify(
687
+ formattedResults.filter((result) => result !== null),
688
+ null,
689
+ 2,
690
+ );
691
+ },
692
+ }),
693
+ };
694
+
695
+ const searchTools = {
696
+ search_content: tool({
697
+ description: `
698
+ Search for relevant information within the actual content of the items across available knowledge bases.
699
+
700
+ This tool provides a number of strategies:
701
+ - Keyword search: search for exact terms, technical names, IDs, or specific phrases
702
+ - Semantic search: search for conceptual queries where synonyms and paraphrasing matter
703
+ - Hybrid search: best for most queries - combines semantic understanding with exact term matching
704
+
705
+ You can use the includeContent parameter to control whether to return
706
+ the full chunk content or just metadata.
707
+
708
+ Use with includeContent: true (default) when you need to:
709
+ - Find specific information or answers within documents
710
+ - Get actual text content that answers a query
711
+ - Extract details, explanations, or instructions from content
712
+
713
+ Use with includeContent: false when you need to:
714
+ - List which documents/items contain certain topics
715
+ - Count or overview items that match a content query
716
+ - Find item names/metadata without loading full content
717
+ - You can always fetch content later if needed
718
+
719
+ `,
720
+ inputSchema: z.object({
721
+ query: z
722
+ .string()
723
+ .describe(
724
+ "The search query to find relevant chunks, this must always be related to the content you are looking for, not something like 'Page 2'.",
725
+ ),
726
+ knowledge_base_ids: z.array(z.enum(contexts.map((ctx) => ctx.id) as [string, ...string[]]))
727
+ .describe(`
728
+ The available knowledge bases are:
729
+ ${contexts
730
+ .map(
731
+ (ctx) => `
732
+ <knowledge_base>
733
+ <id>${ctx.id}</id>
734
+ <name>${ctx.name}</name>
735
+ <description>${ctx.description}</description>
736
+ </knowledge_base>
737
+ `,
738
+ )
739
+ .join("\n")}
740
+ `),
741
+ keywords: z
742
+ .array(z.string())
743
+ .optional()
744
+ .describe(
745
+ "Keywords to search for. Usually extracted from the query, allowing for more precise search results.",
746
+ ),
747
+ searchMethod: z
748
+ .enum(["keyword", "semantic", "hybrid"])
749
+ .default("hybrid")
750
+ .describe(
751
+ "Search method: 'hybrid' (best for most queries - combines semantic understanding with exact term matching), 'keyword' (best for exact terms, technical names, IDs, or specific phrases), 'semantic' (best for conceptual queries where synonyms and paraphrasing matter)",
752
+ ),
753
+ includeContent: z
754
+ .boolean()
755
+ .default(true)
756
+ .describe(
757
+ "Whether to include the full chunk content in results. " +
758
+ "Set to FALSE when you only need to know WHICH documents/items are relevant (lists, overviews, counts). " +
759
+ "Set to TRUE when you need the ACTUAL content to answer the question (information, details, explanations). " +
760
+ "You can always fetch content later, so prefer FALSE for efficiency when listing documents.",
761
+ ),
762
+
763
+ item_ids: z
764
+ .array(z.string())
765
+ .optional()
766
+ .describe(
767
+ "Use if you wish to retrieve content from specific items (documents) based on the item ID.",
768
+ ),
769
+ item_names: z
770
+ .array(z.string())
771
+ .optional()
772
+ .describe(
773
+ "Use if you wish to retrieve content from specific items (documents) based on the item name. Can be a partial match.",
774
+ ),
775
+ item_external_ids: z
776
+ .array(z.string())
777
+ .optional()
778
+ .describe(
779
+ "Use if you wish to retrieve content from specific items (documents) based on the item external ID. Can be a partial match.",
780
+ ),
781
+ limit: z.number().default(10).describe("Maximum number of chunks to return (max 10)"),
782
+ }),
783
+ execute: async ({
784
+ query,
785
+ searchMethod,
786
+ limit,
787
+ includeContent,
788
+ item_ids,
789
+ item_names,
790
+ item_external_ids,
791
+ keywords,
792
+ knowledge_base_ids,
793
+ }) => {
794
+ if (!knowledge_base_ids?.length) {
795
+ // Default to all
796
+ knowledge_base_ids = contexts.map((ctx) => ctx.id);
797
+ }
798
+
799
+ const results: VectorSearchChunkResult[][] = await Promise.all(
800
+ knowledge_base_ids.map(async (knowledge_base_id) => {
801
+ const ctx = contexts.find(
802
+ (ctx) =>
803
+ ctx.id === knowledge_base_id ||
804
+ ctx.id.toLowerCase().includes(knowledge_base_id.toLowerCase()),
805
+ );
806
+
807
+ if (!ctx) {
808
+ console.error(
809
+ "[EXULU] Knowledge base ID that was provided to search content not found.",
810
+ knowledge_base_id,
811
+ );
812
+ throw new Error("Knowledge base ID that was provided to search content not found.");
813
+ }
814
+
815
+ let itemFilters: SearchFilters = [];
816
+ if (item_ids) {
817
+ itemFilters.push({ id: { in: item_ids } });
818
+ }
819
+ if (item_names) {
820
+ itemFilters.push({ name: { or: item_names.map((name) => ({ contains: name })) } });
821
+ }
822
+ if (item_external_ids) {
823
+ itemFilters.push({ external_id: { in: item_external_ids } });
824
+ }
825
+
826
+ if (!query && keywords) {
827
+ query = keywords.join(" ");
828
+ }
829
+
830
+ const results = await ctx.search({
831
+ query: query,
832
+ keywords: keywords,
833
+ method:
834
+ searchMethod === "hybrid"
835
+ ? "hybridSearch"
836
+ : searchMethod === "keyword"
837
+ ? "tsvector"
838
+ : "cosineDistance",
839
+ limit: includeContent ? Math.min(limit, 10) : Math.min(limit * 20, 400),
840
+ page: 1,
841
+ itemFilters: itemFilters || [],
842
+ chunkFilters: [],
843
+ sort: { field: "updatedAt", direction: "desc" },
844
+ user,
845
+ role,
846
+ trigger: "tool",
847
+ });
848
+
849
+ return results.chunks.map((chunk) => chunk);
850
+ }),
851
+ );
852
+
853
+ const resultsFlat: VectorSearchChunkResult[] = results.flat();
854
+
855
+ // Format results with citation info
856
+ const formattedResults: ToolResult[] = resultsFlat.map((chunk) => ({
857
+ item_name: chunk.item_name,
858
+ item_id: chunk.item_id,
859
+ context: chunk.context?.id || "",
860
+ chunk_id: chunk.chunk_id,
861
+ chunk_index: chunk.chunk_index,
862
+ chunk_content: includeContent ? chunk.chunk_content : undefined,
863
+ metadata: {
864
+ ...chunk.chunk_metadata,
865
+ cosine_distance: chunk.chunk_cosine_distance,
866
+ fts_rank: chunk.chunk_fts_rank,
867
+ hybrid_score: chunk.chunk_hybrid_score,
868
+ },
869
+ }));
870
+ return JSON.stringify(formattedResults, null, 2);
871
+ },
872
+ }),
873
+ };
874
+
875
+ console.log("[EXULU] Search tools:", Object.keys(searchTools));
876
+
877
+ // Create the agent with all tools
878
+
879
+ const agent = createCustomAgenticRetrievalToolLoopAgent({
880
+ language,
881
+ model,
882
+ customInstructions: custom,
883
+ tools: {
884
+ ...searchTools,
885
+ ...searchItemsByNameTool,
886
+ ...(projectRetrievalTool ? { [projectRetrievalTool.id]: projectRetrievalTool.tool } : {}),
887
+ },
888
+ });
889
+
890
+ return agent;
891
+ };
892
+
893
+ /**
894
+ * Generator function that can be used as the execute function for an ExuluTool
895
+ * This streams progress updates as the agent works
896
+ */
897
+ async function* executeAgenticRetrieval({
898
+ contexts,
899
+ reranker,
900
+ query,
901
+ user,
902
+ role,
903
+ model,
904
+ instructions,
905
+ projectRetrievalTool,
906
+ }: {
907
+ contexts: ExuluContext[];
908
+ reranker?: ExuluReranker;
909
+ query: string;
910
+ projectRetrievalTool?: ExuluTool;
911
+ user?: User;
912
+ role?: string;
913
+ model: LanguageModel;
914
+ instructions?: string;
915
+ }) {
916
+ const { language } = preprocessQuery(query, {
917
+ detectLanguage: true,
918
+ });
919
+
920
+ console.log("[EXULU] Language detected:", language);
921
+
922
+ // Create the agent
923
+ console.log(
924
+ "[EXULU] Creating agentic retrieval agent",
925
+ "available contexts:",
926
+ contexts.map((ctx) => ctx.id),
927
+ );
928
+ const agent = createAgenticRetrievalAgent({
929
+ contexts,
930
+ user,
931
+ role,
932
+ model,
933
+ instructions,
934
+ projectRetrievalTool,
935
+ language,
936
+ });
937
+
938
+ console.log("[EXULU] Starting agentic retrieval");
939
+
940
+ try {
941
+ // Create a promise that resolves when onFinish is called
942
+ let finishResolver: (value: any) => void;
943
+ let finishRejector: (error: Error) => void;
944
+
945
+ const finishPromise = new Promise<any>((resolve, reject) => {
946
+ finishResolver = resolve;
947
+ finishRejector = reject;
948
+ });
949
+
950
+ // Set a timeout that will reject if onFinish is never called
951
+ const timeoutId = setTimeout(() => {
952
+ finishRejector(new Error("Agentic retrieval timed out after 240 seconds"));
953
+ }, 240000);
954
+
955
+ const result = agent.generate({
956
+ reranker,
957
+ query,
958
+ onFinish: (output) => {
959
+ clearTimeout(timeoutId);
960
+ finishResolver(output);
961
+ },
962
+ });
963
+
964
+ // Yield all intermediate outputs from the generator
965
+ for await (const output of result) {
966
+ yield output;
967
+ }
968
+
969
+ // Wait for onFinish to be called (or timeout)
970
+ const finalOutput = await finishPromise;
971
+
972
+ console.log("[EXULU] Agentic retrieval output", finalOutput);
973
+
974
+ return finalOutput;
975
+ } catch (error) {
976
+ console.error("[EXULU] Agentic retrieval error:", error);
977
+ yield JSON.stringify({
978
+ status: "error",
979
+ message: error instanceof Error ? error.message : "Unknown error occurred",
980
+ });
981
+ }
982
+ }
983
+
984
+ /**
985
+ * Creates an ExuluTool instance for agentic retrieval across all contexts
986
+ * This should be added to an agent's tool set
987
+ */
988
+ export const createAgenticRetrievalTool = ({
989
+ contexts,
990
+ rerankers,
991
+ user,
992
+ role,
993
+ model,
994
+ projectRetrievalTool,
995
+ }: {
996
+ contexts: ExuluContext[];
997
+ rerankers: ExuluReranker[];
998
+ user?: User;
999
+ role?: string;
1000
+ model: any;
1001
+ projectRetrievalTool?: ExuluTool;
1002
+ }): ExuluTool | undefined => {
1003
+
1004
+ const license = checkLicense()
1005
+ if (!license["agentic-retrieval"]) {
1006
+ console.warn(`[EXULU] You are not licensed to use agentic retrieval.`);
1007
+ return undefined;
1008
+ }
1009
+
1010
+ const contextNames = contexts.map((ctx) => ctx.id).join(", ");
1011
+
1012
+ return new ExuluTool({
1013
+ id: "agentic_context_search",
1014
+ name: "Agentic Context Search",
1015
+ description: `Intelligent context search tool that uses AI to reason through and retrieve relevant information from available knowledge bases (${contextNames}). This tool can understand complex queries, search across multiple contexts, filter items by name, id, external id, and expand context as needed.`,
1016
+ category: "contexts",
1017
+ type: "context",
1018
+ // Config to enable / disable individual contexts
1019
+ config: [
1020
+ {
1021
+ name: "instructions",
1022
+ description: `Custom instructions to use when searching the knowledge bases. This is appended to the default system instructions.`,
1023
+ type: "string",
1024
+ default: "",
1025
+ },
1026
+ {
1027
+ name: "reranker",
1028
+ description: "The reranker to use for the retrieval process.",
1029
+ type: "string",
1030
+ default: "none",
1031
+ },
1032
+ ...contexts.map((ctx) => ({
1033
+ name: ctx.id,
1034
+ description: `Enable search in the ${ctx.name} context. ${ctx.description}`,
1035
+ type: "boolean" as "boolean" | "string" | "number" | "variable",
1036
+ default: true,
1037
+ })),
1038
+ ],
1039
+ inputSchema: z.object({
1040
+ query: z.string().describe("The question or query to answer using the knowledge bases"),
1041
+ userInstructions: z
1042
+ .string()
1043
+ .optional()
1044
+ .describe("Instructions provided by the user to customize the retrieval process."),
1045
+ }),
1046
+ execute: async function* ({
1047
+ query,
1048
+ userInstructions,
1049
+ toolVariablesConfig,
1050
+ }: {
1051
+ query: string;
1052
+ userInstructions?: string;
1053
+ instructions?: string;
1054
+ [key: string]: any;
1055
+ }) {
1056
+ let configInstructions = "";
1057
+ let configuredReranker: ExuluReranker | undefined;
1058
+ if (toolVariablesConfig) {
1059
+ configInstructions = toolVariablesConfig.instructions;
1060
+
1061
+ contexts = contexts.filter(
1062
+ (ctx) =>
1063
+ toolVariablesConfig[ctx.id] === true ||
1064
+ toolVariablesConfig[ctx.id] === "true" ||
1065
+ toolVariablesConfig[ctx.id] === 1,
1066
+ );
1067
+
1068
+ if (toolVariablesConfig.reranker) {
1069
+ configuredReranker = rerankers.find(
1070
+ (reranker) => reranker.id === toolVariablesConfig.reranker,
1071
+ );
1072
+ if (!configuredReranker) {
1073
+ throw new Error(
1074
+ "Reranker not found: " +
1075
+ toolVariablesConfig.reranker +
1076
+ ", check with a developer if the reranker was removed from the system.",
1077
+ );
1078
+ }
1079
+ }
1080
+ }
1081
+
1082
+ console.log("[EXULU] Executing agentic retrieval tool with data", {
1083
+ // Log only first level properties
1084
+ // Keys of all toolVariablesConfig vars
1085
+ configs: Object.keys(toolVariablesConfig),
1086
+ query,
1087
+ instructions: configInstructions,
1088
+ reranker: configuredReranker?.id || undefined,
1089
+ contexts: contexts.map((ctx) => ctx.id),
1090
+ });
1091
+
1092
+ console.log("[EXULU] Executing agentic retrieval tool");
1093
+
1094
+ // Yield each chunk from the generator
1095
+ for await (const chunk of executeAgenticRetrieval({
1096
+ contexts,
1097
+ reranker: configuredReranker,
1098
+ query,
1099
+ user,
1100
+ role,
1101
+ model,
1102
+ instructions: `${configInstructions ? `CUSTOM INSTRUCTIONS PROVIDED BY THE ADMIN: ${configInstructions}` : ""} ${userInstructions ? `INSTRUCTIONS PROVIDED BY THE USER: ${userInstructions}` : ""}`,
1103
+ projectRetrievalTool,
1104
+ })) {
1105
+ yield { result: JSON.stringify(chunk, null, 2) };
1106
+ }
1107
+ },
1108
+ });
1109
+ };