mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): tolerate LLM-mangled tool names
This commit is contained in:
parent
f9fda29170
commit
c4f2117a23
3
.gitignore
vendored
3
.gitignore
vendored
@ -98,3 +98,6 @@ CLAUDE.md
|
||||
|
||||
# Codex CLI local artifacts
|
||||
.codex/
|
||||
|
||||
# QwenPaw sync state (generated each run; report is intentionally tracked)
|
||||
scripts/.qwenpaw-sync-state.json
|
||||
|
||||
@ -23,6 +23,7 @@ import vip.mate.tool.guard.service.ToolGuardService;
|
||||
import java.util.*;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 统一工具执行器(共享于 ActionNode 和 StepExecutionNode)
|
||||
@ -114,6 +115,16 @@ public class ToolExecutionExecutor {
|
||||
}
|
||||
|
||||
private final Map<String, ToolCallback> toolCallbackMap;
|
||||
/**
|
||||
* Maps a normalized tool name (lowercase snake_case, with `_tool`/`_function`
|
||||
* suffixes stripped) to the canonical name registered in {@link #toolCallbackMap}.
|
||||
* Lets us resolve names the LLM sometimes mangles (e.g. {@code WebSearch},
|
||||
* {@code web_search_tool}, {@code Read_File}) back to the registered tool
|
||||
* before guard / lookup / event reporting run, so guard rules keyed on the
|
||||
* canonical name aren't silently bypassed.
|
||||
*/
|
||||
private final Map<String, String> normalizedNameLookup;
|
||||
private static final Pattern CAMEL_BOUNDARY = Pattern.compile("([a-z0-9])([A-Z])");
|
||||
private final ToolGuardService toolGuardService;
|
||||
private final ToolGuard toolGuard; // legacy fallback
|
||||
private final ApprovalWorkflowService approvalService;
|
||||
@ -165,6 +176,7 @@ public class ToolExecutionExecutor {
|
||||
public ToolExecutionExecutor(AgentToolSet toolSet, ToolGuard toolGuard,
|
||||
ApprovalWorkflowService approvalService, ChatStreamTracker streamTracker) {
|
||||
this.toolCallbackMap = toolSet.callbackByName();
|
||||
this.normalizedNameLookup = buildNormalizedLookup(this.toolCallbackMap.keySet());
|
||||
this.toolGuardService = null;
|
||||
this.toolGuard = toolGuard;
|
||||
this.approvalService = approvalService;
|
||||
@ -181,6 +193,7 @@ public class ToolExecutionExecutor {
|
||||
ToolResultStorage resultStorage,
|
||||
vip.mate.tool.ToolConcurrencyRegistry concurrencyRegistry) {
|
||||
this.toolCallbackMap = toolSet.callbackByName();
|
||||
this.normalizedNameLookup = buildNormalizedLookup(this.toolCallbackMap.keySet());
|
||||
this.toolGuardService = toolGuardService;
|
||||
this.toolGuard = toolGuard;
|
||||
this.approvalService = approvalService;
|
||||
@ -268,7 +281,10 @@ public class ToolExecutionExecutor {
|
||||
|
||||
for (int i = 0; i < toolCalls.size(); i++) {
|
||||
AssistantMessage.ToolCall toolCall = toolCalls.get(i);
|
||||
String toolName = toolCall.name();
|
||||
// Resolve LLM-emitted name to canonical BEFORE guard / lookup so a
|
||||
// mangled name (Read_File, web_search_tool, BrowserUseTool) can't
|
||||
// bypass guard rules keyed on the canonical name.
|
||||
String toolName = resolveToolName(toolCall.name());
|
||||
String arguments = toolCall.arguments();
|
||||
|
||||
events.add(GraphEventPublisher.toolStart(toolCall.id(), toolName, arguments));
|
||||
@ -410,7 +426,7 @@ public class ToolExecutionExecutor {
|
||||
List<GraphEventPublisher.GraphEvent> events,
|
||||
String conversationId, String workspaceBasePath,
|
||||
List<DirectToolOutput> directOutputs) {
|
||||
String toolName = toolCall.name();
|
||||
String toolName = resolveToolName(toolCall.name());
|
||||
String callArguments = storedArguments != null ? storedArguments : toolCall.arguments();
|
||||
|
||||
ToolCallback callback = toolCallbackMap.get(toolName);
|
||||
@ -842,6 +858,64 @@ public class ToolExecutionExecutor {
|
||||
* <p>Case-insensitive match because LLMs sometimes change the case of
|
||||
* skill names mid-conversation.
|
||||
*/
|
||||
/**
|
||||
* Resolve the LLM-emitted tool name to a registered canonical name.
|
||||
* Tries exact match first (the hot path); on miss, normalizes the input
|
||||
* (camelCase→snake_case, lowercase, strip {@code _tool}/{@code _function}
|
||||
* suffix) and looks up the canonical equivalent. Returns the original
|
||||
* string when no match is found, so the caller's downstream "tool not
|
||||
* found" path still fires.
|
||||
*/
|
||||
String resolveToolName(String requested) {
|
||||
if (requested == null || requested.isBlank()) {
|
||||
return requested;
|
||||
}
|
||||
if (toolCallbackMap.containsKey(requested)) {
|
||||
return requested;
|
||||
}
|
||||
String normalized = normalizeToolName(requested);
|
||||
String canonical = normalizedNameLookup.get(normalized);
|
||||
if (canonical != null) {
|
||||
log.info("[ToolExecutor] Tool name normalized: '{}' -> '{}' (via '{}')",
|
||||
requested, canonical, normalized);
|
||||
return canonical;
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
|
||||
static String normalizeToolName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String snake = CAMEL_BOUNDARY.matcher(name).replaceAll("$1_$2");
|
||||
String collapsed = snake.toLowerCase(Locale.ROOT)
|
||||
.replaceAll("[\\s\\-.]+", "_")
|
||||
.replaceAll("_+", "_");
|
||||
if (collapsed.endsWith("_tool")) {
|
||||
collapsed = collapsed.substring(0, collapsed.length() - 5);
|
||||
} else if (collapsed.endsWith("_function")) {
|
||||
collapsed = collapsed.substring(0, collapsed.length() - 9);
|
||||
}
|
||||
return collapsed.replaceAll("^_+|_+$", "");
|
||||
}
|
||||
|
||||
private static Map<String, String> buildNormalizedLookup(Set<String> canonicalNames) {
|
||||
Map<String, String> result = new HashMap<>(canonicalNames.size() * 2);
|
||||
for (String name : canonicalNames) {
|
||||
String norm = normalizeToolName(name);
|
||||
if (norm.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String previous = result.putIfAbsent(norm, name);
|
||||
if (previous != null && !previous.equals(name)) {
|
||||
log.warn("[ToolExecutor] Two registered tools normalize to the same key '{}': "
|
||||
+ "'{}' and '{}' — only '{}' will resolve from mangled LLM emissions",
|
||||
norm, previous, name, previous);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private String skillAwareNotFoundMessage(String toolName) {
|
||||
if (skillRuntimeService != null && toolName != null && !toolName.isBlank()) {
|
||||
try {
|
||||
|
||||
@ -233,7 +233,6 @@ err.llm.pkce_failed=PKCE \u751f\u6210\u5931\u8d25
|
||||
err.llm.chatgpt_stream_failed=ChatGPT \u6d41\u5f0f\u8c03\u7528\u5931\u8d25
|
||||
err.llm.chatgpt_error=ChatGPT \u8fd4\u56de\u9519\u8bef
|
||||
err.llm.chatgpt_account_missing=chatgpt-account-id \u7f3a\u5931
|
||||
err.llm.model_not_supported=\u6a21\u578b\u0020\u0049\u0044\u0020\u0020\u4e0d\u652f\u6301\u5728\u0020\u0044\u0061\u0073\u0068\u0053\u0063\u006f\u0070\u0065\u0020\u539f\u751f\u534f\u8bae\u4e2d\u4f7f\u7528\u3002\u70b9\u7248\u672c\u683c\u5f0f\u7684\u7cfb\u5217\uff08\u4f8b\u5982\u0020\u0071\u0077\u0065\u006e\u0033\u002e\u0035\u002d\u002a\u3001\u0020\u0071\u0077\u0065\u006e\u0033\u002e\u0036\u002d\u002a\uff09\u53ea\u80fd\u901a\u8fc7\u517c\u5bb9\u6a21\u5f0f\u4f7f\u7528\u3002\u8bf7\u4f7f\u7528\u5141\u8bb8\u7684\u0020\u0049\u0044\uff0c\u5982\u0020\u0071\u0077\u0065\u006e\u002d\u006d\u0061\u0078\u0020\u002f\u0020\u0071\u0077\u0065\u006e\u002d\u0070\u006c\u0075\u0073\u0020\u002f\u0020\u0071\u0077\u0065\u006e\u0033\u002d\u006d\u0061\u0078\u3002
|
||||
# datasource
|
||||
err.datasource.not_found=\u6570\u636e\u6e90\u4e0d\u5b58\u5728
|
||||
err.datasource.sql_empty=SQL \u4e0d\u80fd\u4e3a\u7a7a
|
||||
|
||||
Loading…
Reference in New Issue
Block a user