mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 12:27:53 +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 CLI local artifacts
|
||||||
.codex/
|
.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.*;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一工具执行器(共享于 ActionNode 和 StepExecutionNode)
|
* 统一工具执行器(共享于 ActionNode 和 StepExecutionNode)
|
||||||
@ -114,6 +115,16 @@ public class ToolExecutionExecutor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final Map<String, ToolCallback> toolCallbackMap;
|
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 ToolGuardService toolGuardService;
|
||||||
private final ToolGuard toolGuard; // legacy fallback
|
private final ToolGuard toolGuard; // legacy fallback
|
||||||
private final ApprovalWorkflowService approvalService;
|
private final ApprovalWorkflowService approvalService;
|
||||||
@ -165,6 +176,7 @@ public class ToolExecutionExecutor {
|
|||||||
public ToolExecutionExecutor(AgentToolSet toolSet, ToolGuard toolGuard,
|
public ToolExecutionExecutor(AgentToolSet toolSet, ToolGuard toolGuard,
|
||||||
ApprovalWorkflowService approvalService, ChatStreamTracker streamTracker) {
|
ApprovalWorkflowService approvalService, ChatStreamTracker streamTracker) {
|
||||||
this.toolCallbackMap = toolSet.callbackByName();
|
this.toolCallbackMap = toolSet.callbackByName();
|
||||||
|
this.normalizedNameLookup = buildNormalizedLookup(this.toolCallbackMap.keySet());
|
||||||
this.toolGuardService = null;
|
this.toolGuardService = null;
|
||||||
this.toolGuard = toolGuard;
|
this.toolGuard = toolGuard;
|
||||||
this.approvalService = approvalService;
|
this.approvalService = approvalService;
|
||||||
@ -181,6 +193,7 @@ public class ToolExecutionExecutor {
|
|||||||
ToolResultStorage resultStorage,
|
ToolResultStorage resultStorage,
|
||||||
vip.mate.tool.ToolConcurrencyRegistry concurrencyRegistry) {
|
vip.mate.tool.ToolConcurrencyRegistry concurrencyRegistry) {
|
||||||
this.toolCallbackMap = toolSet.callbackByName();
|
this.toolCallbackMap = toolSet.callbackByName();
|
||||||
|
this.normalizedNameLookup = buildNormalizedLookup(this.toolCallbackMap.keySet());
|
||||||
this.toolGuardService = toolGuardService;
|
this.toolGuardService = toolGuardService;
|
||||||
this.toolGuard = toolGuard;
|
this.toolGuard = toolGuard;
|
||||||
this.approvalService = approvalService;
|
this.approvalService = approvalService;
|
||||||
@ -268,7 +281,10 @@ public class ToolExecutionExecutor {
|
|||||||
|
|
||||||
for (int i = 0; i < toolCalls.size(); i++) {
|
for (int i = 0; i < toolCalls.size(); i++) {
|
||||||
AssistantMessage.ToolCall toolCall = toolCalls.get(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();
|
String arguments = toolCall.arguments();
|
||||||
|
|
||||||
events.add(GraphEventPublisher.toolStart(toolCall.id(), toolName, arguments));
|
events.add(GraphEventPublisher.toolStart(toolCall.id(), toolName, arguments));
|
||||||
@ -410,7 +426,7 @@ public class ToolExecutionExecutor {
|
|||||||
List<GraphEventPublisher.GraphEvent> events,
|
List<GraphEventPublisher.GraphEvent> events,
|
||||||
String conversationId, String workspaceBasePath,
|
String conversationId, String workspaceBasePath,
|
||||||
List<DirectToolOutput> directOutputs) {
|
List<DirectToolOutput> directOutputs) {
|
||||||
String toolName = toolCall.name();
|
String toolName = resolveToolName(toolCall.name());
|
||||||
String callArguments = storedArguments != null ? storedArguments : toolCall.arguments();
|
String callArguments = storedArguments != null ? storedArguments : toolCall.arguments();
|
||||||
|
|
||||||
ToolCallback callback = toolCallbackMap.get(toolName);
|
ToolCallback callback = toolCallbackMap.get(toolName);
|
||||||
@ -842,6 +858,64 @@ public class ToolExecutionExecutor {
|
|||||||
* <p>Case-insensitive match because LLMs sometimes change the case of
|
* <p>Case-insensitive match because LLMs sometimes change the case of
|
||||||
* skill names mid-conversation.
|
* 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) {
|
private String skillAwareNotFoundMessage(String toolName) {
|
||||||
if (skillRuntimeService != null && toolName != null && !toolName.isBlank()) {
|
if (skillRuntimeService != null && toolName != null && !toolName.isBlank()) {
|
||||||
try {
|
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_stream_failed=ChatGPT \u6d41\u5f0f\u8c03\u7528\u5931\u8d25
|
||||||
err.llm.chatgpt_error=ChatGPT \u8fd4\u56de\u9519\u8bef
|
err.llm.chatgpt_error=ChatGPT \u8fd4\u56de\u9519\u8bef
|
||||||
err.llm.chatgpt_account_missing=chatgpt-account-id \u7f3a\u5931
|
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
|
# datasource
|
||||||
err.datasource.not_found=\u6570\u636e\u6e90\u4e0d\u5b58\u5728
|
err.datasource.not_found=\u6570\u636e\u6e90\u4e0d\u5b58\u5728
|
||||||
err.datasource.sql_empty=SQL \u4e0d\u80fd\u4e3a\u7a7a
|
err.datasource.sql_empty=SQL \u4e0d\u80fd\u4e3a\u7a7a
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user