@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.
- package/.agents/skills/mintlify/SKILL.md +347 -0
- package/.editorconfig +15 -0
- package/.eslintrc.json +52 -0
- package/.jscpd.json +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc.json +12 -0
- package/CHANGELOG.md +15 -2
- package/README.md +747 -0
- package/SECURITY.md +5 -0
- package/dist/index.cjs +12015 -10496
- package/dist/index.d.cts +725 -667
- package/dist/index.d.ts +725 -667
- package/dist/index.js +12034 -10508
- package/ee/LICENSE.md +62 -0
- package/ee/agentic-retrieval/index.ts +1109 -0
- package/ee/documents/THIRD_PARTY_LICENSES/docling.txt +31 -0
- package/ee/documents/processing/build_pdf_processor.sh +35 -0
- package/ee/documents/processing/chunk_markdown.py +263 -0
- package/ee/documents/processing/doc_processor.ts +635 -0
- package/ee/documents/processing/pdf_processor.spec +115 -0
- package/ee/documents/processing/pdf_to_markdown.py +420 -0
- package/ee/documents/processing/requirements.txt +4 -0
- package/ee/entitlements.ts +49 -0
- package/ee/markdown.ts +686 -0
- package/ee/queues/decorator.ts +140 -0
- package/ee/queues/queues.ts +156 -0
- package/ee/queues/server.ts +6 -0
- package/ee/rbac-resolver.ts +51 -0
- package/ee/rbac-update.ts +111 -0
- package/ee/schemas.ts +347 -0
- package/ee/tokenizer.ts +80 -0
- package/ee/workers.ts +1423 -0
- package/eslint.config.js +88 -0
- package/jest.config.ts +25 -0
- package/license.md +73 -49
- package/mintlify-docs/.mintignore +7 -0
- package/mintlify-docs/AGENTS.md +33 -0
- package/mintlify-docs/CLAUDE.MD +50 -0
- package/mintlify-docs/CONTRIBUTING.md +32 -0
- package/mintlify-docs/LICENSE +21 -0
- package/mintlify-docs/README.md +55 -0
- package/mintlify-docs/ai-tools/claude-code.mdx +43 -0
- package/mintlify-docs/ai-tools/cursor.mdx +39 -0
- package/mintlify-docs/ai-tools/windsurf.mdx +39 -0
- package/mintlify-docs/api-reference/core-types/agent-types.mdx +110 -0
- package/mintlify-docs/api-reference/core-types/analytics-types.mdx +95 -0
- package/mintlify-docs/api-reference/core-types/configuration-types.mdx +83 -0
- package/mintlify-docs/api-reference/core-types/evaluation-types.mdx +106 -0
- package/mintlify-docs/api-reference/core-types/job-types.mdx +135 -0
- package/mintlify-docs/api-reference/core-types/overview.mdx +73 -0
- package/mintlify-docs/api-reference/core-types/prompt-types.mdx +102 -0
- package/mintlify-docs/api-reference/core-types/rbac-types.mdx +163 -0
- package/mintlify-docs/api-reference/core-types/session-types.mdx +77 -0
- package/mintlify-docs/api-reference/core-types/user-management.mdx +112 -0
- package/mintlify-docs/api-reference/core-types/workflow-types.mdx +88 -0
- package/mintlify-docs/api-reference/core-types.mdx +585 -0
- package/mintlify-docs/api-reference/dynamic-types.mdx +851 -0
- package/mintlify-docs/api-reference/endpoint/create.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/delete.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/get.mdx +4 -0
- package/mintlify-docs/api-reference/endpoint/webhook.mdx +4 -0
- package/mintlify-docs/api-reference/introduction.mdx +661 -0
- package/mintlify-docs/api-reference/mutations.mdx +1012 -0
- package/mintlify-docs/api-reference/openapi.json +217 -0
- package/mintlify-docs/api-reference/queries.mdx +1154 -0
- package/mintlify-docs/backend/introduction.mdx +218 -0
- package/mintlify-docs/changelog.mdx +293 -0
- package/mintlify-docs/community-edition.mdx +304 -0
- package/mintlify-docs/core/exulu-agent/api-reference.mdx +894 -0
- package/mintlify-docs/core/exulu-agent/configuration.mdx +690 -0
- package/mintlify-docs/core/exulu-agent/introduction.mdx +552 -0
- package/mintlify-docs/core/exulu-app/api-reference.mdx +481 -0
- package/mintlify-docs/core/exulu-app/configuration.mdx +319 -0
- package/mintlify-docs/core/exulu-app/introduction.mdx +117 -0
- package/mintlify-docs/core/exulu-authentication.mdx +810 -0
- package/mintlify-docs/core/exulu-chunkers/api-reference.mdx +1011 -0
- package/mintlify-docs/core/exulu-chunkers/configuration.mdx +596 -0
- package/mintlify-docs/core/exulu-chunkers/introduction.mdx +403 -0
- package/mintlify-docs/core/exulu-context/api-reference.mdx +911 -0
- package/mintlify-docs/core/exulu-context/configuration.mdx +648 -0
- package/mintlify-docs/core/exulu-context/introduction.mdx +394 -0
- package/mintlify-docs/core/exulu-database.mdx +811 -0
- package/mintlify-docs/core/exulu-default-agents.mdx +545 -0
- package/mintlify-docs/core/exulu-eval/api-reference.mdx +772 -0
- package/mintlify-docs/core/exulu-eval/configuration.mdx +680 -0
- package/mintlify-docs/core/exulu-eval/introduction.mdx +459 -0
- package/mintlify-docs/core/exulu-logging.mdx +464 -0
- package/mintlify-docs/core/exulu-otel.mdx +670 -0
- package/mintlify-docs/core/exulu-queues/api-reference.mdx +648 -0
- package/mintlify-docs/core/exulu-queues/configuration.mdx +650 -0
- package/mintlify-docs/core/exulu-queues/introduction.mdx +474 -0
- package/mintlify-docs/core/exulu-reranker/api-reference.mdx +630 -0
- package/mintlify-docs/core/exulu-reranker/configuration.mdx +663 -0
- package/mintlify-docs/core/exulu-reranker/introduction.mdx +516 -0
- package/mintlify-docs/core/exulu-tool/api-reference.mdx +723 -0
- package/mintlify-docs/core/exulu-tool/configuration.mdx +805 -0
- package/mintlify-docs/core/exulu-tool/introduction.mdx +539 -0
- package/mintlify-docs/core/exulu-variables/api-reference.mdx +699 -0
- package/mintlify-docs/core/exulu-variables/configuration.mdx +736 -0
- package/mintlify-docs/core/exulu-variables/introduction.mdx +511 -0
- package/mintlify-docs/development.mdx +94 -0
- package/mintlify-docs/docs.json +248 -0
- package/mintlify-docs/enterprise-edition.mdx +538 -0
- package/mintlify-docs/essentials/code.mdx +35 -0
- package/mintlify-docs/essentials/images.mdx +59 -0
- package/mintlify-docs/essentials/markdown.mdx +88 -0
- package/mintlify-docs/essentials/navigation.mdx +87 -0
- package/mintlify-docs/essentials/reusable-snippets.mdx +110 -0
- package/mintlify-docs/essentials/settings.mdx +318 -0
- package/mintlify-docs/favicon.svg +3 -0
- package/mintlify-docs/frontend/introduction.mdx +39 -0
- package/mintlify-docs/getting-started.mdx +267 -0
- package/mintlify-docs/guides/custom-agent.mdx +608 -0
- package/mintlify-docs/guides/first-agent.mdx +315 -0
- package/mintlify-docs/images/admin_ui.png +0 -0
- package/mintlify-docs/images/contexts.png +0 -0
- package/mintlify-docs/images/create_agents.png +0 -0
- package/mintlify-docs/images/evals.png +0 -0
- package/mintlify-docs/images/graphql.png +0 -0
- package/mintlify-docs/images/graphql_api.png +0 -0
- package/mintlify-docs/images/hero-dark.png +0 -0
- package/mintlify-docs/images/hero-light.png +0 -0
- package/mintlify-docs/images/hero.png +0 -0
- package/mintlify-docs/images/knowledge_sources.png +0 -0
- package/mintlify-docs/images/mcp.png +0 -0
- package/mintlify-docs/images/scaling.png +0 -0
- package/mintlify-docs/index.mdx +411 -0
- package/mintlify-docs/logo/dark.svg +9 -0
- package/mintlify-docs/logo/light.svg +9 -0
- package/mintlify-docs/partners.mdx +558 -0
- package/mintlify-docs/products.mdx +77 -0
- package/mintlify-docs/snippets/snippet-intro.mdx +4 -0
- package/mintlify-docs/styles.css +207 -0
- package/{documentation → old-documentation}/logging.md +3 -3
- package/package.json +35 -4
- package/skills-lock.json +10 -0
- package/types/context-processor.ts +45 -0
- package/types/exulu-table-definition.ts +79 -0
- package/types/file-types.ts +18 -0
- package/types/models/agent.ts +10 -12
- package/types/models/exulu-agent-tool-config.ts +11 -0
- package/types/models/rate-limiter-rules.ts +7 -0
- package/types/provider-config.ts +21 -0
- package/types/queue-config.ts +16 -0
- package/types/rbac-rights-modes.ts +1 -0
- package/types/statistics.ts +20 -0
- package/types/workflow.ts +31 -0
- package/changelogs/10.11.2025_03.12.2025.md +0 -316
- package/types/models/agent-backend.ts +0 -15
- /package/{documentation → old-documentation}/otel.md +0 -0
- /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
|
+
};
|