@librechat/agents 3.0.66 → 3.0.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/enum.cjs +1 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/main.cjs +8 -7
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/messages/tools.cjs +2 -2
- package/dist/cjs/messages/tools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +1 -1
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/{ToolSearchRegex.cjs → ToolSearch.cjs} +154 -66
- package/dist/cjs/tools/ToolSearch.cjs.map +1 -0
- package/dist/esm/common/enum.mjs +1 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/messages/tools.mjs +2 -2
- package/dist/esm/messages/tools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +1 -1
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/{ToolSearchRegex.mjs → ToolSearch.mjs} +153 -66
- package/dist/esm/tools/ToolSearch.mjs.map +1 -0
- package/dist/types/common/enum.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/tools/{ToolSearchRegex.d.ts → ToolSearch.d.ts} +33 -23
- package/dist/types/types/tools.d.ts +5 -1
- package/package.json +2 -2
- package/src/common/enum.ts +1 -1
- package/src/index.ts +1 -1
- package/src/messages/__tests__/tools.test.ts +21 -21
- package/src/messages/tools.ts +2 -2
- package/src/scripts/programmatic_exec_agent.ts +4 -4
- package/src/scripts/{tool_search_regex.ts → tool_search.ts} +5 -5
- package/src/tools/ToolNode.ts +1 -1
- package/src/tools/{ToolSearchRegex.ts → ToolSearch.ts} +195 -74
- package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts} +6 -6
- package/src/tools/__tests__/{ToolSearchRegex.test.ts → ToolSearch.test.ts} +212 -3
- package/src/types/tools.ts +6 -1
- package/dist/cjs/tools/ToolSearchRegex.cjs.map +0 -1
- package/dist/esm/tools/ToolSearchRegex.mjs.map +0 -1
|
@@ -41,7 +41,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
41
41
|
return new ToolMessage({
|
|
42
42
|
content: `Found ${discoveredTools.length} tools`,
|
|
43
43
|
tool_call_id: toolCallId,
|
|
44
|
-
name: Constants.
|
|
44
|
+
name: Constants.TOOL_SEARCH,
|
|
45
45
|
artifact: {
|
|
46
46
|
tool_references: discoveredTools.map((name) => ({
|
|
47
47
|
tool_name: name,
|
|
@@ -79,7 +79,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
79
79
|
createAIMessage('Searching...', [
|
|
80
80
|
{
|
|
81
81
|
id: 'call_1',
|
|
82
|
-
name: Constants.
|
|
82
|
+
name: Constants.TOOL_SEARCH,
|
|
83
83
|
args: { pattern: 'database' },
|
|
84
84
|
},
|
|
85
85
|
]),
|
|
@@ -97,12 +97,12 @@ describe('Tool Discovery Functions', () => {
|
|
|
97
97
|
createAIMessage('Searching...', [
|
|
98
98
|
{
|
|
99
99
|
id: 'call_1',
|
|
100
|
-
name: Constants.
|
|
100
|
+
name: Constants.TOOL_SEARCH,
|
|
101
101
|
args: { pattern: 'database' },
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
id: 'call_2',
|
|
105
|
-
name: Constants.
|
|
105
|
+
name: Constants.TOOL_SEARCH,
|
|
106
106
|
args: { pattern: 'file' },
|
|
107
107
|
},
|
|
108
108
|
]),
|
|
@@ -137,7 +137,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
137
137
|
new ToolMessage({
|
|
138
138
|
content: 'Some result',
|
|
139
139
|
tool_call_id: 'orphan_call',
|
|
140
|
-
name: Constants.
|
|
140
|
+
name: Constants.TOOL_SEARCH,
|
|
141
141
|
}),
|
|
142
142
|
];
|
|
143
143
|
|
|
@@ -153,7 +153,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
153
153
|
createAIMessage('Searching...', [
|
|
154
154
|
{
|
|
155
155
|
id: 'old_call',
|
|
156
|
-
name: Constants.
|
|
156
|
+
name: Constants.TOOL_SEARCH,
|
|
157
157
|
args: { pattern: 'old' },
|
|
158
158
|
},
|
|
159
159
|
]),
|
|
@@ -163,7 +163,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
163
163
|
createAIMessage('Searching again...', [
|
|
164
164
|
{
|
|
165
165
|
id: 'new_call',
|
|
166
|
-
name: Constants.
|
|
166
|
+
name: Constants.TOOL_SEARCH,
|
|
167
167
|
args: { pattern: 'new' },
|
|
168
168
|
},
|
|
169
169
|
]),
|
|
@@ -182,7 +182,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
182
182
|
createAIMessage('Working...', [
|
|
183
183
|
{
|
|
184
184
|
id: 'search_call',
|
|
185
|
-
name: Constants.
|
|
185
|
+
name: Constants.TOOL_SEARCH,
|
|
186
186
|
args: { pattern: 'test' },
|
|
187
187
|
},
|
|
188
188
|
{ id: 'other_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
@@ -202,14 +202,14 @@ describe('Tool Discovery Functions', () => {
|
|
|
202
202
|
createAIMessage('Searching...', [
|
|
203
203
|
{
|
|
204
204
|
id: 'call_1',
|
|
205
|
-
name: Constants.
|
|
205
|
+
name: Constants.TOOL_SEARCH,
|
|
206
206
|
args: { pattern: 'xyz' },
|
|
207
207
|
},
|
|
208
208
|
]),
|
|
209
209
|
new ToolMessage({
|
|
210
210
|
content: 'No tools found',
|
|
211
211
|
tool_call_id: 'call_1',
|
|
212
|
-
name: Constants.
|
|
212
|
+
name: Constants.TOOL_SEARCH,
|
|
213
213
|
artifact: {
|
|
214
214
|
tool_references: [],
|
|
215
215
|
metadata: { total_searched: 10, pattern: 'xyz' },
|
|
@@ -228,14 +228,14 @@ describe('Tool Discovery Functions', () => {
|
|
|
228
228
|
createAIMessage('Searching...', [
|
|
229
229
|
{
|
|
230
230
|
id: 'call_1',
|
|
231
|
-
name: Constants.
|
|
231
|
+
name: Constants.TOOL_SEARCH,
|
|
232
232
|
args: { pattern: 'test' },
|
|
233
233
|
},
|
|
234
234
|
]),
|
|
235
235
|
new ToolMessage({
|
|
236
236
|
content: 'Error occurred',
|
|
237
237
|
tool_call_id: 'call_1',
|
|
238
|
-
name: Constants.
|
|
238
|
+
name: Constants.TOOL_SEARCH,
|
|
239
239
|
// No artifact
|
|
240
240
|
}),
|
|
241
241
|
];
|
|
@@ -251,7 +251,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
251
251
|
createAIMessage('Searching...', [
|
|
252
252
|
{
|
|
253
253
|
id: 'call_1',
|
|
254
|
-
name: Constants.
|
|
254
|
+
name: Constants.TOOL_SEARCH,
|
|
255
255
|
args: { pattern: 'test' },
|
|
256
256
|
},
|
|
257
257
|
]),
|
|
@@ -271,7 +271,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
271
271
|
createAIMessage('First search', [
|
|
272
272
|
{
|
|
273
273
|
id: 'first_call',
|
|
274
|
-
name: Constants.
|
|
274
|
+
name: Constants.TOOL_SEARCH,
|
|
275
275
|
args: { pattern: 'first' },
|
|
276
276
|
},
|
|
277
277
|
]),
|
|
@@ -280,7 +280,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
280
280
|
createAIMessage('Second search', [
|
|
281
281
|
{
|
|
282
282
|
id: 'second_call',
|
|
283
|
-
name: Constants.
|
|
283
|
+
name: Constants.TOOL_SEARCH,
|
|
284
284
|
args: { pattern: 'second' },
|
|
285
285
|
},
|
|
286
286
|
]),
|
|
@@ -301,7 +301,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
301
301
|
createAIMessage('Searching...', [
|
|
302
302
|
{
|
|
303
303
|
id: 'call_1',
|
|
304
|
-
name: Constants.
|
|
304
|
+
name: Constants.TOOL_SEARCH,
|
|
305
305
|
args: { pattern: 'test' },
|
|
306
306
|
},
|
|
307
307
|
]),
|
|
@@ -335,7 +335,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
335
335
|
new ToolMessage({
|
|
336
336
|
content: 'Result',
|
|
337
337
|
tool_call_id: 'orphan',
|
|
338
|
-
name: Constants.
|
|
338
|
+
name: Constants.TOOL_SEARCH,
|
|
339
339
|
}),
|
|
340
340
|
];
|
|
341
341
|
|
|
@@ -364,7 +364,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
364
364
|
createAIMessage('Working...', [
|
|
365
365
|
{
|
|
366
366
|
id: 'search_call',
|
|
367
|
-
name: Constants.
|
|
367
|
+
name: Constants.TOOL_SEARCH,
|
|
368
368
|
args: { pattern: 'test' },
|
|
369
369
|
},
|
|
370
370
|
{ id: 'weather_call', name: 'get_weather', args: { city: 'NYC' } },
|
|
@@ -384,7 +384,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
384
384
|
createAIMessage('Searching...', [
|
|
385
385
|
{
|
|
386
386
|
id: 'old_call',
|
|
387
|
-
name: Constants.
|
|
387
|
+
name: Constants.TOOL_SEARCH,
|
|
388
388
|
args: { pattern: 'old' },
|
|
389
389
|
},
|
|
390
390
|
]),
|
|
@@ -410,7 +410,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
410
410
|
createAIMessage('Searching...', [
|
|
411
411
|
{
|
|
412
412
|
id: 'call_1',
|
|
413
|
-
name: Constants.
|
|
413
|
+
name: Constants.TOOL_SEARCH,
|
|
414
414
|
args: { pattern: 'test' },
|
|
415
415
|
},
|
|
416
416
|
]),
|
|
@@ -446,7 +446,7 @@ describe('Tool Discovery Functions', () => {
|
|
|
446
446
|
createAIMessage('Searching...', [
|
|
447
447
|
{
|
|
448
448
|
id: 'call_1',
|
|
449
|
-
name: Constants.
|
|
449
|
+
name: Constants.TOOL_SEARCH,
|
|
450
450
|
args: { pattern: 'test' },
|
|
451
451
|
},
|
|
452
452
|
]),
|
package/src/messages/tools.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function extractToolDiscoveries(messages: BaseMessage[]): string[] {
|
|
|
43
43
|
for (let i = latestAIParentIndex + 1; i < messages.length; i++) {
|
|
44
44
|
const msg = messages[i];
|
|
45
45
|
if (!(msg instanceof ToolMessage)) continue;
|
|
46
|
-
if (msg.name !== Constants.
|
|
46
|
+
if (msg.name !== Constants.TOOL_SEARCH) continue;
|
|
47
47
|
if (!toolCallIds.has(msg.tool_call_id)) continue;
|
|
48
48
|
|
|
49
49
|
// This is a tool search result from the current turn
|
|
@@ -88,7 +88,7 @@ export function hasToolSearchInCurrentTurn(messages: BaseMessage[]): boolean {
|
|
|
88
88
|
const msg = messages[i];
|
|
89
89
|
if (
|
|
90
90
|
msg instanceof ToolMessage &&
|
|
91
|
-
msg.name === Constants.
|
|
91
|
+
msg.name === Constants.TOOL_SEARCH &&
|
|
92
92
|
toolCallIds.has(msg.tool_call_id)
|
|
93
93
|
) {
|
|
94
94
|
return true;
|
|
@@ -23,7 +23,7 @@ import type { RunnableConfig } from '@langchain/core/runnables';
|
|
|
23
23
|
import type * as t from '@/types';
|
|
24
24
|
import { createCodeExecutionTool } from '@/tools/CodeExecutor';
|
|
25
25
|
import { createProgrammaticToolCallingTool } from '@/tools/ProgrammaticToolCalling';
|
|
26
|
-
import {
|
|
26
|
+
import { createToolSearch } from '@/tools/ToolSearch';
|
|
27
27
|
import { getLLMConfig } from '@/utils/llmConfig';
|
|
28
28
|
import { getArgs } from '@/scripts/args';
|
|
29
29
|
import { Run } from '@/run';
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Tool registry only needs business logic tools that require filtering.
|
|
43
|
-
* Special tools (execute_code, run_tools_with_code,
|
|
43
|
+
* Special tools (execute_code, run_tools_with_code, tool_search)
|
|
44
44
|
* are always bound directly to the LLM and don't need registry entries.
|
|
45
45
|
*/
|
|
46
46
|
function createAgentToolRegistry(): t.LCToolRegistry {
|
|
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
|
|
|
73
73
|
// Create special tools (PTC, code execution, tool search)
|
|
74
74
|
const codeExecTool = createCodeExecutionTool();
|
|
75
75
|
const ptcTool = createProgrammaticToolCallingTool();
|
|
76
|
-
const toolSearchTool =
|
|
76
|
+
const toolSearchTool = createToolSearch();
|
|
77
77
|
|
|
78
78
|
// Build complete tool list and map
|
|
79
79
|
const allTools = [...mockTools, codeExecTool, ptcTool, toolSearchTool];
|
|
@@ -199,7 +199,7 @@ Use the run_tools_with_code tool to do this efficiently - don't call each tool s
|
|
|
199
199
|
console.log('='.repeat(70));
|
|
200
200
|
console.log('\nKey observations:');
|
|
201
201
|
console.log(
|
|
202
|
-
'1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code, run_tools_with_code,
|
|
202
|
+
'1. LLM only sees tools with allowed_callers including "direct" (get_weather, execute_code, run_tools_with_code, tool_search)'
|
|
203
203
|
);
|
|
204
204
|
console.log(
|
|
205
205
|
'2. When PTC is invoked, ToolNode automatically injects programmatic tools (get_team_members, get_expenses, get_weather)'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/scripts/
|
|
1
|
+
// src/scripts/tool_search.ts
|
|
2
2
|
/**
|
|
3
3
|
* Test script for the Tool Search Regex tool.
|
|
4
|
-
* Run with: npm run
|
|
4
|
+
* Run with: npm run tool_search
|
|
5
5
|
*
|
|
6
6
|
* Demonstrates runtime registry injection - the tool registry is passed
|
|
7
7
|
* at invocation time, not at initialization time.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { config } from 'dotenv';
|
|
10
10
|
config();
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { createToolSearch } from '@/tools/ToolSearch';
|
|
13
13
|
import type { LCToolRegistry } from '@/types';
|
|
14
14
|
import { createToolSearchToolRegistry } from '@/test/mockTools';
|
|
15
15
|
|
|
@@ -22,7 +22,7 @@ interface RunTestOptions {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
async function runTest(
|
|
25
|
-
searchTool: ReturnType<typeof
|
|
25
|
+
searchTool: ReturnType<typeof createToolSearch>,
|
|
26
26
|
testName: string,
|
|
27
27
|
query: string,
|
|
28
28
|
options: RunTestOptions
|
|
@@ -82,7 +82,7 @@ async function main(): Promise<void> {
|
|
|
82
82
|
);
|
|
83
83
|
|
|
84
84
|
console.log('\nCreating Tool Search Regex tool WITH registry for testing...');
|
|
85
|
-
const searchTool =
|
|
85
|
+
const searchTool = createToolSearch({ apiKey, toolRegistry });
|
|
86
86
|
console.log('Tool created successfully!');
|
|
87
87
|
console.log(
|
|
88
88
|
'Note: In production, ToolNode injects toolRegistry via params when invoked through the graph.\n'
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -132,7 +132,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
132
132
|
toolMap,
|
|
133
133
|
toolDefs,
|
|
134
134
|
};
|
|
135
|
-
} else if (call.name === Constants.
|
|
135
|
+
} else if (call.name === Constants.TOOL_SEARCH) {
|
|
136
136
|
invokeParams = {
|
|
137
137
|
...invokeParams,
|
|
138
138
|
toolRegistry: this.toolRegistry,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/tools/
|
|
1
|
+
// src/tools/ToolSearch.ts
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { config } from 'dotenv';
|
|
4
4
|
import fetch, { RequestInit } from 'node-fetch';
|
|
@@ -20,28 +20,43 @@ const MAX_REGEX_COMPLEXITY = 5;
|
|
|
20
20
|
/** Default search timeout in milliseconds */
|
|
21
21
|
const SEARCH_TIMEOUT = 5000;
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
/** Zod schema type for tool search parameters */
|
|
24
|
+
type ToolSearchSchema = z.ZodObject<{
|
|
25
|
+
query: z.ZodString;
|
|
26
|
+
fields: z.ZodDefault<
|
|
27
|
+
z.ZodOptional<z.ZodArray<z.ZodEnum<['name', 'description', 'parameters']>>>
|
|
28
|
+
>;
|
|
29
|
+
max_results: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the Zod schema with dynamic query description based on mode.
|
|
34
|
+
* @param mode - The search mode determining query interpretation
|
|
35
|
+
* @returns Zod schema for tool search parameters
|
|
36
|
+
*/
|
|
37
|
+
function createToolSearchSchema(mode: t.ToolSearchMode): ToolSearchSchema {
|
|
38
|
+
const queryDescription =
|
|
39
|
+
mode === 'local'
|
|
40
|
+
? 'Search term to find in tool names and descriptions. Case-insensitive substring matching.'
|
|
41
|
+
: 'Regex pattern to search tool names and descriptions. Special regex characters will be sanitized for safety.';
|
|
42
|
+
|
|
43
|
+
return z.object({
|
|
44
|
+
query: z.string().min(1).max(MAX_PATTERN_LENGTH).describe(queryDescription),
|
|
45
|
+
fields: z
|
|
46
|
+
.array(z.enum(['name', 'description', 'parameters']))
|
|
47
|
+
.optional()
|
|
48
|
+
.default(['name', 'description'])
|
|
49
|
+
.describe('Which fields to search. Default: name and description'),
|
|
50
|
+
max_results: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(1)
|
|
54
|
+
.max(50)
|
|
55
|
+
.optional()
|
|
56
|
+
.default(10)
|
|
57
|
+
.describe('Maximum number of matching tools to return'),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
45
60
|
|
|
46
61
|
/**
|
|
47
62
|
* Escapes special regex characters in a string to use as a literal pattern.
|
|
@@ -170,6 +185,80 @@ function simplifyParametersForSearch(
|
|
|
170
185
|
return { type: parameters.type };
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Performs safe local substring search without regex.
|
|
190
|
+
* Uses case-insensitive String.includes() for complete safety against ReDoS.
|
|
191
|
+
* @param tools - Array of tool metadata to search
|
|
192
|
+
* @param query - The search term (treated as literal substring)
|
|
193
|
+
* @param fields - Which fields to search
|
|
194
|
+
* @param maxResults - Maximum results to return
|
|
195
|
+
* @returns Search response with matching tools
|
|
196
|
+
*/
|
|
197
|
+
function performLocalSearch(
|
|
198
|
+
tools: t.ToolMetadata[],
|
|
199
|
+
query: string,
|
|
200
|
+
fields: string[],
|
|
201
|
+
maxResults: number
|
|
202
|
+
): t.ToolSearchResponse {
|
|
203
|
+
const lowerQuery = query.toLowerCase();
|
|
204
|
+
const results: t.ToolSearchResult[] = [];
|
|
205
|
+
|
|
206
|
+
for (const tool of tools) {
|
|
207
|
+
let bestScore = 0;
|
|
208
|
+
let matchedField = '';
|
|
209
|
+
let snippet = '';
|
|
210
|
+
|
|
211
|
+
if (fields.includes('name')) {
|
|
212
|
+
const lowerName = tool.name.toLowerCase();
|
|
213
|
+
if (lowerName.includes(lowerQuery)) {
|
|
214
|
+
const isExactMatch = lowerName === lowerQuery;
|
|
215
|
+
const startsWithQuery = lowerName.startsWith(lowerQuery);
|
|
216
|
+
bestScore = isExactMatch ? 1.0 : startsWithQuery ? 0.95 : 0.85;
|
|
217
|
+
matchedField = 'name';
|
|
218
|
+
snippet = tool.name;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (fields.includes('description') && tool.description) {
|
|
223
|
+
const lowerDesc = tool.description.toLowerCase();
|
|
224
|
+
if (lowerDesc.includes(lowerQuery) && bestScore === 0) {
|
|
225
|
+
bestScore = 0.7;
|
|
226
|
+
matchedField = 'description';
|
|
227
|
+
snippet = tool.description.substring(0, 100);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (fields.includes('parameters') && tool.parameters?.properties) {
|
|
232
|
+
const paramNames = Object.keys(tool.parameters.properties)
|
|
233
|
+
.join(' ')
|
|
234
|
+
.toLowerCase();
|
|
235
|
+
if (paramNames.includes(lowerQuery) && bestScore === 0) {
|
|
236
|
+
bestScore = 0.55;
|
|
237
|
+
matchedField = 'parameters';
|
|
238
|
+
snippet = Object.keys(tool.parameters.properties).join(' ');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (bestScore > 0) {
|
|
243
|
+
results.push({
|
|
244
|
+
tool_name: tool.name,
|
|
245
|
+
match_score: bestScore,
|
|
246
|
+
matched_field: matchedField,
|
|
247
|
+
snippet,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
results.sort((a, b) => b.match_score - a.match_score);
|
|
253
|
+
const topResults = results.slice(0, maxResults);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
tool_references: topResults,
|
|
257
|
+
total_tools_searched: tools.length,
|
|
258
|
+
pattern_used: query,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
173
262
|
/**
|
|
174
263
|
* Generates the JavaScript search script to be executed in the sandbox.
|
|
175
264
|
* Uses plain JavaScript for maximum compatibility with the Code API.
|
|
@@ -307,11 +396,15 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
|
|
|
307
396
|
}
|
|
308
397
|
|
|
309
398
|
/**
|
|
310
|
-
* Creates a Tool Search
|
|
399
|
+
* Creates a Tool Search tool for discovering tools from a large registry.
|
|
311
400
|
*
|
|
312
401
|
* This tool enables AI agents to dynamically discover tools from a large library
|
|
313
402
|
* without loading all tool definitions into the LLM context window. The agent
|
|
314
|
-
* can search for relevant tools on-demand
|
|
403
|
+
* can search for relevant tools on-demand.
|
|
404
|
+
*
|
|
405
|
+
* **Modes:**
|
|
406
|
+
* - `code_interpreter` (default): Uses external sandbox for regex search. Safer for complex patterns.
|
|
407
|
+
* - `local`: Uses safe substring matching locally. No network call, faster, completely safe from ReDoS.
|
|
315
408
|
*
|
|
316
409
|
* The tool registry can be provided either:
|
|
317
410
|
* 1. At initialization time via params.toolRegistry
|
|
@@ -321,36 +414,52 @@ function formatSearchResults(searchResponse: t.ToolSearchResponse): string {
|
|
|
321
414
|
* @returns A LangChain DynamicStructuredTool for tool searching
|
|
322
415
|
*
|
|
323
416
|
* @example
|
|
324
|
-
* // Option 1:
|
|
325
|
-
* const tool =
|
|
326
|
-
* await tool.invoke({ query: 'expense' });
|
|
417
|
+
* // Option 1: Code interpreter mode (regex via sandbox)
|
|
418
|
+
* const tool = createToolSearch({ apiKey, toolRegistry });
|
|
419
|
+
* await tool.invoke({ query: 'expense.*report' });
|
|
327
420
|
*
|
|
328
421
|
* @example
|
|
329
|
-
* // Option 2:
|
|
330
|
-
* const tool =
|
|
331
|
-
* await tool.invoke(
|
|
332
|
-
* { query: 'expense' },
|
|
333
|
-
* { configurable: { toolRegistry, onlyDeferred: true } }
|
|
334
|
-
* );
|
|
422
|
+
* // Option 2: Local mode (safe substring search, no API key needed)
|
|
423
|
+
* const tool = createToolSearch({ mode: 'local', toolRegistry });
|
|
424
|
+
* await tool.invoke({ query: 'expense' });
|
|
335
425
|
*/
|
|
336
|
-
function
|
|
337
|
-
initParams: t.
|
|
338
|
-
): DynamicStructuredTool<typeof
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
343
|
-
'';
|
|
426
|
+
function createToolSearch(
|
|
427
|
+
initParams: t.ToolSearchParams = {}
|
|
428
|
+
): DynamicStructuredTool<ReturnType<typeof createToolSearchSchema>> {
|
|
429
|
+
const mode: t.ToolSearchMode = initParams.mode ?? 'code_interpreter';
|
|
430
|
+
const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
|
|
431
|
+
const schema = createToolSearchSchema(mode);
|
|
344
432
|
|
|
345
|
-
|
|
346
|
-
|
|
433
|
+
const apiKey: string =
|
|
434
|
+
mode === 'code_interpreter'
|
|
435
|
+
? ((initParams[EnvVar.CODE_API_KEY] as string | undefined) ??
|
|
436
|
+
initParams.apiKey ??
|
|
437
|
+
getEnvironmentVariable(EnvVar.CODE_API_KEY) ??
|
|
438
|
+
'')
|
|
439
|
+
: '';
|
|
440
|
+
|
|
441
|
+
if (mode === 'code_interpreter' && !apiKey) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
'No API key provided for tool search in code_interpreter mode. Use mode: "local" to search without an API key.'
|
|
444
|
+
);
|
|
347
445
|
}
|
|
348
446
|
|
|
349
447
|
const baseEndpoint = initParams.baseUrl ?? getCodeBaseURL();
|
|
350
448
|
const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
|
|
351
|
-
const defaultOnlyDeferred = initParams.onlyDeferred ?? true;
|
|
352
449
|
|
|
353
|
-
const description =
|
|
450
|
+
const description =
|
|
451
|
+
mode === 'local'
|
|
452
|
+
? `
|
|
453
|
+
Searches through available tools to find ones matching your search term.
|
|
454
|
+
|
|
455
|
+
Usage:
|
|
456
|
+
- Provide a search term to find in tool names and descriptions.
|
|
457
|
+
- Uses case-insensitive substring matching (fast and safe).
|
|
458
|
+
- Use this when you need to discover tools for a specific task.
|
|
459
|
+
- Results include tool names, match quality scores, and snippets showing where the match occurred.
|
|
460
|
+
- Higher scores (0.95+) indicate name matches, medium scores (0.70+) indicate description matches.
|
|
461
|
+
`.trim()
|
|
462
|
+
: `
|
|
354
463
|
Searches through available tools to find ones matching your query pattern.
|
|
355
464
|
|
|
356
465
|
Usage:
|
|
@@ -360,7 +469,7 @@ Usage:
|
|
|
360
469
|
- Higher scores (0.9+) indicate name matches, medium scores (0.7+) indicate description matches.
|
|
361
470
|
`.trim();
|
|
362
471
|
|
|
363
|
-
return tool<typeof
|
|
472
|
+
return tool<typeof schema>(
|
|
364
473
|
async (params, config) => {
|
|
365
474
|
const {
|
|
366
475
|
query,
|
|
@@ -368,21 +477,11 @@ Usage:
|
|
|
368
477
|
max_results = 10,
|
|
369
478
|
} = params;
|
|
370
479
|
|
|
371
|
-
// Extra params injected by ToolNode (follows web_search pattern)
|
|
372
480
|
const {
|
|
373
481
|
toolRegistry: paramToolRegistry,
|
|
374
482
|
onlyDeferred: paramOnlyDeferred,
|
|
375
483
|
} = config.toolCall ?? {};
|
|
376
484
|
|
|
377
|
-
const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
|
|
378
|
-
|
|
379
|
-
let warningMessage = '';
|
|
380
|
-
if (wasEscaped) {
|
|
381
|
-
warningMessage =
|
|
382
|
-
'Note: The provided pattern was converted to a literal search for safety.\n\n';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Priority: ToolNode injection (via config.toolCall) > initialization params
|
|
386
485
|
const toolRegistry = paramToolRegistry ?? initParams.toolRegistry;
|
|
387
486
|
const onlyDeferred =
|
|
388
487
|
paramOnlyDeferred !== undefined
|
|
@@ -391,12 +490,12 @@ Usage:
|
|
|
391
490
|
|
|
392
491
|
if (toolRegistry == null) {
|
|
393
492
|
return [
|
|
394
|
-
|
|
493
|
+
'Error: No tool registry provided. Configure toolRegistry at agent level or initialization.',
|
|
395
494
|
{
|
|
396
495
|
tool_references: [],
|
|
397
496
|
metadata: {
|
|
398
497
|
total_searched: 0,
|
|
399
|
-
pattern:
|
|
498
|
+
pattern: query,
|
|
400
499
|
error: 'No tool registry provided',
|
|
401
500
|
},
|
|
402
501
|
},
|
|
@@ -404,14 +503,10 @@ Usage:
|
|
|
404
503
|
}
|
|
405
504
|
|
|
406
505
|
const toolsArray: t.LCTool[] = Array.from(toolRegistry.values());
|
|
407
|
-
|
|
408
506
|
const deferredTools: t.ToolMetadata[] = toolsArray
|
|
409
|
-
.filter((lcTool) =>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
return true;
|
|
414
|
-
})
|
|
507
|
+
.filter((lcTool) =>
|
|
508
|
+
onlyDeferred === true ? lcTool.defer_loading === true : true
|
|
509
|
+
)
|
|
415
510
|
.map((lcTool) => ({
|
|
416
511
|
name: lcTool.name,
|
|
417
512
|
description: lcTool.description ?? '',
|
|
@@ -420,17 +515,42 @@ Usage:
|
|
|
420
515
|
|
|
421
516
|
if (deferredTools.length === 0) {
|
|
422
517
|
return [
|
|
423
|
-
|
|
518
|
+
'No tools available to search. The tool registry is empty or no deferred tools are registered.',
|
|
424
519
|
{
|
|
425
520
|
tool_references: [],
|
|
521
|
+
metadata: { total_searched: 0, pattern: query },
|
|
522
|
+
},
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (mode === 'local') {
|
|
527
|
+
const searchResponse = performLocalSearch(
|
|
528
|
+
deferredTools,
|
|
529
|
+
query,
|
|
530
|
+
fields,
|
|
531
|
+
max_results
|
|
532
|
+
);
|
|
533
|
+
const formattedOutput = formatSearchResults(searchResponse);
|
|
534
|
+
|
|
535
|
+
return [
|
|
536
|
+
formattedOutput,
|
|
537
|
+
{
|
|
538
|
+
tool_references: searchResponse.tool_references,
|
|
426
539
|
metadata: {
|
|
427
|
-
total_searched:
|
|
428
|
-
pattern:
|
|
540
|
+
total_searched: searchResponse.total_tools_searched,
|
|
541
|
+
pattern: searchResponse.pattern_used,
|
|
429
542
|
},
|
|
430
543
|
},
|
|
431
544
|
];
|
|
432
545
|
}
|
|
433
546
|
|
|
547
|
+
const { safe: sanitizedPattern, wasEscaped } = sanitizeRegex(query);
|
|
548
|
+
let warningMessage = '';
|
|
549
|
+
if (wasEscaped) {
|
|
550
|
+
warningMessage =
|
|
551
|
+
'Note: The provided pattern was converted to a literal search for safety.\n\n';
|
|
552
|
+
}
|
|
553
|
+
|
|
434
554
|
const searchScript = generateSearchScript(
|
|
435
555
|
deferredTools,
|
|
436
556
|
fields,
|
|
@@ -468,7 +588,7 @@ Usage:
|
|
|
468
588
|
|
|
469
589
|
if (result.stderr && result.stderr.trim()) {
|
|
470
590
|
// eslint-disable-next-line no-console
|
|
471
|
-
console.warn('[
|
|
591
|
+
console.warn('[ToolSearch] stderr:', result.stderr);
|
|
472
592
|
}
|
|
473
593
|
|
|
474
594
|
if (!result.stdout || !result.stdout.trim()) {
|
|
@@ -499,7 +619,7 @@ Usage:
|
|
|
499
619
|
];
|
|
500
620
|
} catch (error) {
|
|
501
621
|
// eslint-disable-next-line no-console
|
|
502
|
-
console.error('[
|
|
622
|
+
console.error('[ToolSearch] Error:', error);
|
|
503
623
|
|
|
504
624
|
const errorMessage =
|
|
505
625
|
error instanceof Error ? error.message : String(error);
|
|
@@ -517,16 +637,17 @@ Usage:
|
|
|
517
637
|
}
|
|
518
638
|
},
|
|
519
639
|
{
|
|
520
|
-
name: Constants.
|
|
640
|
+
name: Constants.TOOL_SEARCH,
|
|
521
641
|
description,
|
|
522
|
-
schema
|
|
642
|
+
schema,
|
|
523
643
|
responseFormat: Constants.CONTENT_AND_ARTIFACT,
|
|
524
644
|
}
|
|
525
645
|
);
|
|
526
646
|
}
|
|
527
647
|
|
|
528
648
|
export {
|
|
529
|
-
|
|
649
|
+
createToolSearch,
|
|
650
|
+
performLocalSearch,
|
|
530
651
|
sanitizeRegex,
|
|
531
652
|
escapeRegexSpecialChars,
|
|
532
653
|
isDangerousPattern,
|