@librechat/agents 3.0.65 → 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/agents/AgentContext.cjs +12 -10
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/enum.cjs +1 -1
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +22 -7
- package/dist/cjs/graphs/MultiAgentGraph.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 +19 -4
- 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/agents/AgentContext.mjs +12 -10
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/enum.mjs +1 -1
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +22 -7
- package/dist/esm/graphs/MultiAgentGraph.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 +19 -4
- 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/agents/AgentContext.d.ts +7 -3
- package/dist/types/common/enum.d.ts +1 -1
- package/dist/types/graphs/MultiAgentGraph.d.ts +3 -3
- 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/agents/AgentContext.ts +20 -12
- package/src/common/enum.ts +1 -1
- package/src/graphs/MultiAgentGraph.ts +29 -8
- 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 +23 -5
- 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
|
@@ -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,
|
package/src/tools/__tests__/{ToolSearchRegex.integration.test.ts → ToolSearch.integration.test.ts}
RENAMED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
// src/tools/__tests__/
|
|
1
|
+
// src/tools/__tests__/ToolSearch.integration.test.ts
|
|
2
2
|
/**
|
|
3
3
|
* Integration tests for Tool Search Regex.
|
|
4
4
|
* These tests hit the LIVE Code API and verify end-to-end search functionality.
|
|
5
5
|
*
|
|
6
|
-
* Run with: npm test --
|
|
6
|
+
* Run with: npm test -- ToolSearch.integration.test.ts
|
|
7
7
|
*/
|
|
8
8
|
import { config as dotenvConfig } from 'dotenv';
|
|
9
9
|
dotenvConfig();
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect, beforeAll } from '@jest/globals';
|
|
12
|
-
import {
|
|
12
|
+
import { createToolSearch } from '../ToolSearch';
|
|
13
13
|
import { createToolSearchToolRegistry } from '@/test/mockTools';
|
|
14
14
|
|
|
15
|
-
describe('
|
|
16
|
-
let searchTool: ReturnType<typeof
|
|
15
|
+
describe('ToolSearch - Live API Integration', () => {
|
|
16
|
+
let searchTool: ReturnType<typeof createToolSearch>;
|
|
17
17
|
const toolRegistry = createToolSearchToolRegistry();
|
|
18
18
|
|
|
19
19
|
beforeAll(() => {
|
|
@@ -24,7 +24,7 @@ describe('ToolSearchRegex - Live API Integration', () => {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
searchTool =
|
|
27
|
+
searchTool = createToolSearch({ apiKey, toolRegistry });
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('searches for expense-related tools', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/tools/__tests__/
|
|
1
|
+
// src/tools/__tests__/ToolSearch.test.ts
|
|
2
2
|
/**
|
|
3
3
|
* Unit tests for Tool Search Regex.
|
|
4
4
|
* Tests helper functions and sanitization logic without hitting the API.
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
isDangerousPattern,
|
|
11
11
|
countNestedGroups,
|
|
12
12
|
hasNestedQuantifiers,
|
|
13
|
-
|
|
13
|
+
performLocalSearch,
|
|
14
|
+
} from '../ToolSearch';
|
|
15
|
+
import type { ToolMetadata } from '@/types';
|
|
14
16
|
|
|
15
|
-
describe('
|
|
17
|
+
describe('ToolSearch', () => {
|
|
16
18
|
describe('escapeRegexSpecialChars', () => {
|
|
17
19
|
it('escapes special regex characters', () => {
|
|
18
20
|
expect(escapeRegexSpecialChars('hello.world')).toBe('hello\\.world');
|
|
@@ -229,4 +231,211 @@ describe('ToolSearchRegex', () => {
|
|
|
229
231
|
}
|
|
230
232
|
});
|
|
231
233
|
});
|
|
234
|
+
|
|
235
|
+
describe('performLocalSearch', () => {
|
|
236
|
+
const mockTools: ToolMetadata[] = [
|
|
237
|
+
{
|
|
238
|
+
name: 'get_weather',
|
|
239
|
+
description: 'Get current weather data',
|
|
240
|
+
parameters: undefined,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'get_forecast',
|
|
244
|
+
description: 'Get weather forecast for multiple days',
|
|
245
|
+
parameters: undefined,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'send_email',
|
|
249
|
+
description: 'Send an email message',
|
|
250
|
+
parameters: undefined,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'get_expenses',
|
|
254
|
+
description: 'Retrieve expense reports',
|
|
255
|
+
parameters: undefined,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
name: 'calculate_expense_totals',
|
|
259
|
+
description: 'Sum up expenses by category',
|
|
260
|
+
parameters: undefined,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'run_database_query',
|
|
264
|
+
description: 'Execute a database query',
|
|
265
|
+
parameters: {
|
|
266
|
+
type: 'object',
|
|
267
|
+
properties: {
|
|
268
|
+
query: { type: 'string' },
|
|
269
|
+
timeout: { type: 'number' },
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
it('finds tools by exact name match', () => {
|
|
276
|
+
const result = performLocalSearch(mockTools, 'get_weather', ['name'], 10);
|
|
277
|
+
|
|
278
|
+
expect(result.tool_references.length).toBe(1);
|
|
279
|
+
expect(result.tool_references[0].tool_name).toBe('get_weather');
|
|
280
|
+
expect(result.tool_references[0].match_score).toBe(1.0);
|
|
281
|
+
expect(result.tool_references[0].matched_field).toBe('name');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('finds tools by partial name match (starts with)', () => {
|
|
285
|
+
const result = performLocalSearch(mockTools, 'get_', ['name'], 10);
|
|
286
|
+
|
|
287
|
+
expect(result.tool_references.length).toBe(3);
|
|
288
|
+
expect(result.tool_references[0].match_score).toBe(0.95);
|
|
289
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
290
|
+
'get_weather'
|
|
291
|
+
);
|
|
292
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
293
|
+
'get_forecast'
|
|
294
|
+
);
|
|
295
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
296
|
+
'get_expenses'
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('finds tools by substring match in name', () => {
|
|
301
|
+
const result = performLocalSearch(mockTools, 'expense', ['name'], 10);
|
|
302
|
+
|
|
303
|
+
expect(result.tool_references.length).toBe(2);
|
|
304
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
305
|
+
'get_expenses'
|
|
306
|
+
);
|
|
307
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
308
|
+
'calculate_expense_totals'
|
|
309
|
+
);
|
|
310
|
+
expect(result.tool_references[0].match_score).toBe(0.85);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('performs case-insensitive search', () => {
|
|
314
|
+
const result = performLocalSearch(
|
|
315
|
+
mockTools,
|
|
316
|
+
'WEATHER',
|
|
317
|
+
['name', 'description'],
|
|
318
|
+
10
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
expect(result.tool_references.length).toBe(2);
|
|
322
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
323
|
+
'get_weather'
|
|
324
|
+
);
|
|
325
|
+
expect(result.tool_references.map((r) => r.tool_name)).toContain(
|
|
326
|
+
'get_forecast'
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('searches in description field', () => {
|
|
331
|
+
const result = performLocalSearch(
|
|
332
|
+
mockTools,
|
|
333
|
+
'email',
|
|
334
|
+
['description'],
|
|
335
|
+
10
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expect(result.tool_references.length).toBe(1);
|
|
339
|
+
expect(result.tool_references[0].tool_name).toBe('send_email');
|
|
340
|
+
expect(result.tool_references[0].matched_field).toBe('description');
|
|
341
|
+
expect(result.tool_references[0].match_score).toBe(0.7);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('searches in parameter names', () => {
|
|
345
|
+
const result = performLocalSearch(mockTools, 'query', ['parameters'], 10);
|
|
346
|
+
|
|
347
|
+
expect(result.tool_references.length).toBe(1);
|
|
348
|
+
expect(result.tool_references[0].tool_name).toBe('run_database_query');
|
|
349
|
+
expect(result.tool_references[0].matched_field).toBe('parameters');
|
|
350
|
+
expect(result.tool_references[0].match_score).toBe(0.55);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('prioritizes name matches over description matches', () => {
|
|
354
|
+
const result = performLocalSearch(
|
|
355
|
+
mockTools,
|
|
356
|
+
'weather',
|
|
357
|
+
['name', 'description'],
|
|
358
|
+
10
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const weatherTool = result.tool_references.find(
|
|
362
|
+
(r) => r.tool_name === 'get_weather'
|
|
363
|
+
);
|
|
364
|
+
const forecastTool = result.tool_references.find(
|
|
365
|
+
(r) => r.tool_name === 'get_forecast'
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
expect(weatherTool?.matched_field).toBe('name');
|
|
369
|
+
expect(forecastTool?.matched_field).toBe('description');
|
|
370
|
+
expect(weatherTool!.match_score).toBeGreaterThan(
|
|
371
|
+
forecastTool!.match_score
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('limits results to max_results', () => {
|
|
376
|
+
const result = performLocalSearch(mockTools, 'get', ['name'], 2);
|
|
377
|
+
|
|
378
|
+
expect(result.tool_references.length).toBe(2);
|
|
379
|
+
expect(result.total_tools_searched).toBe(mockTools.length);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('returns empty array when no matches found', () => {
|
|
383
|
+
const result = performLocalSearch(
|
|
384
|
+
mockTools,
|
|
385
|
+
'nonexistent_xyz_123',
|
|
386
|
+
['name', 'description'],
|
|
387
|
+
10
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
expect(result.tool_references.length).toBe(0);
|
|
391
|
+
expect(result.total_tools_searched).toBe(mockTools.length);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('sorts results by score descending', () => {
|
|
395
|
+
const result = performLocalSearch(
|
|
396
|
+
mockTools,
|
|
397
|
+
'expense',
|
|
398
|
+
['name', 'description'],
|
|
399
|
+
10
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
for (let i = 1; i < result.tool_references.length; i++) {
|
|
403
|
+
expect(
|
|
404
|
+
result.tool_references[i - 1].match_score
|
|
405
|
+
).toBeGreaterThanOrEqual(result.tool_references[i].match_score);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('handles empty tools array', () => {
|
|
410
|
+
const result = performLocalSearch([], 'test', ['name'], 10);
|
|
411
|
+
|
|
412
|
+
expect(result.tool_references.length).toBe(0);
|
|
413
|
+
expect(result.total_tools_searched).toBe(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('handles empty query gracefully', () => {
|
|
417
|
+
const result = performLocalSearch(mockTools, '', ['name'], 10);
|
|
418
|
+
|
|
419
|
+
expect(result.tool_references.length).toBe(mockTools.length);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('includes correct metadata in response', () => {
|
|
423
|
+
const result = performLocalSearch(mockTools, 'weather', ['name'], 10);
|
|
424
|
+
|
|
425
|
+
expect(result.total_tools_searched).toBe(mockTools.length);
|
|
426
|
+
expect(result.pattern_used).toBe('weather');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('provides snippet in results', () => {
|
|
430
|
+
const result = performLocalSearch(
|
|
431
|
+
mockTools,
|
|
432
|
+
'database',
|
|
433
|
+
['description'],
|
|
434
|
+
10
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
expect(result.tool_references[0].snippet).toBeTruthy();
|
|
438
|
+
expect(result.tool_references[0].snippet.length).toBeGreaterThan(0);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
232
441
|
});
|
package/src/types/tools.ts
CHANGED
|
@@ -128,12 +128,17 @@ export type LCToolRegistry = Map<string, LCTool>;
|
|
|
128
128
|
|
|
129
129
|
export type ProgrammaticCache = { toolMap: ToolMap; toolDefs: LCTool[] };
|
|
130
130
|
|
|
131
|
+
/** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
|
|
132
|
+
export type ToolSearchMode = 'code_interpreter' | 'local';
|
|
133
|
+
|
|
131
134
|
/** Parameters for creating a Tool Search Regex tool */
|
|
132
|
-
export type
|
|
135
|
+
export type ToolSearchParams = {
|
|
133
136
|
apiKey?: string;
|
|
134
137
|
toolRegistry?: LCToolRegistry;
|
|
135
138
|
onlyDeferred?: boolean;
|
|
136
139
|
baseUrl?: string;
|
|
140
|
+
/** Search mode: 'code_interpreter' (default) uses sandbox for regex, 'local' uses safe substring matching */
|
|
141
|
+
mode?: ToolSearchMode;
|
|
137
142
|
[key: string]: unknown;
|
|
138
143
|
};
|
|
139
144
|
|