Files
gemini-cli/packages/core/src/utils/tool-utils.ts

119 lines
3.5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { AnyDeclarativeTool, AnyToolInvocation } from '../index.js';
import { isTool } from '../index.js';
import { SHELL_TOOL_NAMES } from './shell-utils.js';
import levenshtein from 'fast-levenshtein';
/**
* Generates a suggestion string for a tool name that was not found in the registry.
* It finds the closest matches based on Levenshtein distance.
* @param unknownToolName The tool name that was not found.
* @param allToolNames The list of all available tool names.
* @param topN The number of suggestions to return. Defaults to 3.
* @returns A suggestion string like " Did you mean 'tool'?" or " Did you mean one of: 'tool1', 'tool2'?", or an empty string if no suggestions are found.
*/
export function getToolSuggestion(
unknownToolName: string,
allToolNames: string[],
topN = 3,
): string {
const matches = allToolNames.map((toolName) => ({
name: toolName,
distance: levenshtein.get(unknownToolName, toolName),
}));
matches.sort((a, b) => a.distance - b.distance);
const topNResults = matches.slice(0, topN);
if (topNResults.length === 0) {
return '';
}
const suggestedNames = topNResults
.map((match) => `"${match.name}"`)
.join(', ');
if (topNResults.length > 1) {
return ` Did you mean one of: ${suggestedNames}?`;
} else {
return ` Did you mean ${suggestedNames}?`;
}
}
/**
* Checks if a tool invocation matches any of a list of patterns.
*
* @param toolOrToolName The tool object or the name of the tool being invoked.
* @param invocation The invocation object for the tool or the command invoked.
* @param patterns A list of patterns to match against.
* Patterns can be:
* - A tool name (e.g., "ReadFileTool") to match any invocation of that tool.
* - A tool name with a prefix (e.g., "ShellTool(git status)") to match
* invocations where the arguments start with that prefix.
* @returns True if the invocation matches any pattern, false otherwise.
*/
export function doesToolInvocationMatch(
toolOrToolName: AnyDeclarativeTool | string,
invocation: AnyToolInvocation | string,
patterns: string[],
): boolean {
let toolNames: string[];
if (isTool(toolOrToolName)) {
toolNames = [toolOrToolName.name, toolOrToolName.constructor.name];
} else {
toolNames = [toolOrToolName];
}
if (toolNames.some((name) => SHELL_TOOL_NAMES.includes(name))) {
toolNames = [...new Set([...toolNames, ...SHELL_TOOL_NAMES])];
}
for (const pattern of patterns) {
const openParen = pattern.indexOf('(');
if (openParen === -1) {
// No arguments, just a tool name
if (toolNames.includes(pattern)) {
return true;
}
continue;
}
const patternToolName = pattern.substring(0, openParen);
if (!toolNames.includes(patternToolName)) {
continue;
}
if (!pattern.endsWith(')')) {
continue;
}
const argPattern = pattern.substring(openParen + 1, pattern.length - 1);
let command: string;
if (typeof invocation === 'string') {
command = invocation;
} else {
if (!('command' in invocation.params)) {
// This invocation has no command - nothing to check.
continue;
}
command = String((invocation.params as { command: string }).command);
}
if (toolNames.some((name) => SHELL_TOOL_NAMES.includes(name))) {
if (command === argPattern || command.startsWith(argPattern + ' ')) {
return true;
}
}
}
return false;
}