mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix: expose plugin tools in agent picker (#604)
This commit is contained in:
parent
e9b3c1697f
commit
5b3285c78f
@ -77,6 +77,28 @@ public class ToolRegistry {
|
||||
log.info("Plugin tool unregistered: {}", toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot plugin callbacks that are currently available to the runtime.
|
||||
* Used by the agent tool picker so plugin tools can be bound per agent
|
||||
* through the same {@code mate_agent_tool.tool_name} path as other tools.
|
||||
*/
|
||||
public List<ToolCallback> listAvailablePluginTools() {
|
||||
List<ToolCallback> out = new ArrayList<>();
|
||||
for (PluginToolEntry entry : pluginTools) {
|
||||
try {
|
||||
if (entry.callback() != null && Boolean.TRUE.equals(entry.availabilityCheck().get())) {
|
||||
out.add(entry.callback());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String name = entry.callback() != null && entry.callback().getToolDefinition() != null
|
||||
? entry.callback().getToolDefinition().name()
|
||||
: "<unknown>";
|
||||
log.warn("Plugin tool availability check failed for {}: {}", name, e.getMessage());
|
||||
}
|
||||
}
|
||||
return List.copyOf(out);
|
||||
}
|
||||
|
||||
public void invalidateEnabledToolSetCache(String reason) {
|
||||
enabledToolSetCache = null;
|
||||
log.debug("Enabled AgentToolSet cache invalidated: {}", reason);
|
||||
|
||||
@ -9,7 +9,8 @@ import lombok.NoArgsConstructor;
|
||||
* Picker DTO for the unified agent tool selector.
|
||||
*
|
||||
* <p>One row per atomic tool the agent can be bound to — built-in tools
|
||||
* appear under {@code source="builtin"}, MCP tools appear under
|
||||
* appear under {@code source="builtin"}, plugin callbacks appear under
|
||||
* {@code source="plugin"}, MCP tools appear under
|
||||
* {@code source="mcp"} and are grouped by their server. The {@link #name}
|
||||
* field is the value the UI saves into {@code mate_agent_tool.tool_name};
|
||||
* for MCP tools it is the prefixed callback name returned by the resolver
|
||||
@ -29,7 +30,7 @@ public class AvailableToolDTO {
|
||||
*/
|
||||
private String rowId;
|
||||
|
||||
/** {@code "builtin"} or {@code "mcp"}. */
|
||||
/** {@code "builtin"}, {@code "channel"}, {@code "plugin"}, or {@code "mcp"}. */
|
||||
private String source;
|
||||
|
||||
/** MCP server id when {@code source == "mcp"}; null otherwise. */
|
||||
@ -123,6 +124,23 @@ public class AvailableToolDTO {
|
||||
.build();
|
||||
}
|
||||
|
||||
public static AvailableToolDTO fromPlugin(String name, String description) {
|
||||
return AvailableToolDTO.builder()
|
||||
.rowId("plugin#" + name)
|
||||
.source("plugin")
|
||||
.providerId(null)
|
||||
.providerName(null)
|
||||
.name(name)
|
||||
.rawName(name)
|
||||
.description(description != null ? description : "")
|
||||
.group("Plugin tools")
|
||||
.groupId("plugin")
|
||||
.stale(false)
|
||||
.available(true)
|
||||
.unavailableReason(null)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static String extractChannelName(String displayName) {
|
||||
if (displayName == null) return "";
|
||||
int open = displayName.lastIndexOf('(');
|
||||
|
||||
@ -5,7 +5,9 @@ import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.tool.ToolRegistry;
|
||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||
import vip.mate.tool.mcp.runtime.McpHashCollisionDetector;
|
||||
import vip.mate.tool.mcp.service.McpServerService;
|
||||
@ -19,8 +21,9 @@ import java.util.List;
|
||||
* Aggregator behind {@code GET /api/v1/tools/available}.
|
||||
*
|
||||
* <p>Returns one DTO per atomic tool the agent edit picker can offer:
|
||||
* built-in tools (from {@link ToolService#listEnabledTools()}) plus every
|
||||
* MCP tool persisted in {@link McpServerEntity#getToolsCacheJson()}.
|
||||
* built-in tools (from {@link ToolService#listEnabledTools()}), plugin
|
||||
* callbacks registered in {@link ToolRegistry}, plus every MCP tool
|
||||
* persisted in {@link McpServerEntity#getToolsCacheJson()}.
|
||||
*
|
||||
* <p>Reads the cache rather than making a live MCP {@code listTools()}
|
||||
* roundtrip so the picker stays fast and stable through brief upstream
|
||||
@ -33,17 +36,11 @@ import java.util.List;
|
||||
* this, the user could save a {@code mate_agent_tool.tool_name} that
|
||||
* resolves to nothing at chat time.
|
||||
*
|
||||
* <p><b>Scope</b>: this aggregator covers the two tool sources users can
|
||||
* bind from the agent edit screen — built-in {@code @Tool} beans
|
||||
* (persisted in {@code mate_tool}) and MCP-discovered tools (cached on
|
||||
* the server row). Plugin-registered {@code ToolCallback} beans surfaced
|
||||
* by other parts of the runtime are intentionally NOT listed here: those
|
||||
* are not user-bindable from the agent picker today, and the picker's
|
||||
* "saved name == runtime callback key" contract only needs to hold for
|
||||
* the rows the picker actually emits. If plugin tools later become
|
||||
* user-bindable, extend this aggregator (or accept that they go through
|
||||
* a separate config path) — see {@code AgentBindingService}'s
|
||||
* {@code SYSTEM_LEVEL_TOOLS} carve-out for the same reasoning.
|
||||
* <p><b>Scope</b>: this aggregator covers the tool sources users can bind
|
||||
* from the agent edit screen. Every emitted {@code name} must match the
|
||||
* runtime callback key accepted by {@code AgentToolSet}; otherwise the UI
|
||||
* could persist a {@code mate_agent_tool.tool_name} row that resolves to
|
||||
* nothing at chat time.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@ -52,10 +49,12 @@ public class AvailableToolService {
|
||||
|
||||
private final ToolService toolService;
|
||||
private final McpServerService mcpServerService;
|
||||
private final ToolRegistry toolRegistry;
|
||||
|
||||
public List<AvailableToolDTO> listAvailable() {
|
||||
List<AvailableToolDTO> out = new ArrayList<>();
|
||||
appendBuiltinTools(out);
|
||||
appendPluginTools(out);
|
||||
appendMcpTools(out);
|
||||
return out;
|
||||
}
|
||||
@ -74,6 +73,30 @@ public class AvailableToolService {
|
||||
}
|
||||
}
|
||||
|
||||
private void appendPluginTools(List<AvailableToolDTO> out) {
|
||||
List<ToolCallback> callbacks;
|
||||
try {
|
||||
callbacks = toolRegistry.listAvailablePluginTools();
|
||||
} catch (Exception e) {
|
||||
log.warn("AvailableToolService: listAvailable plugin tools failed: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
for (ToolCallback callback : callbacks) {
|
||||
try {
|
||||
if (callback == null || callback.getToolDefinition() == null) {
|
||||
continue;
|
||||
}
|
||||
String name = callback.getToolDefinition().name();
|
||||
if (name == null || name.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
out.add(AvailableToolDTO.fromPlugin(name, callback.getToolDefinition().description()));
|
||||
} catch (Exception e) {
|
||||
log.warn("AvailableToolService: skipping plugin tool due to: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendMcpTools(List<AvailableToolDTO> out) {
|
||||
List<McpServerEntity> servers;
|
||||
try {
|
||||
|
||||
@ -3,6 +3,9 @@ package vip.mate.tool.service;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import vip.mate.tool.ToolRegistry;
|
||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||
import vip.mate.tool.mcp.runtime.McpToolNameResolver;
|
||||
import vip.mate.tool.mcp.service.McpServerService;
|
||||
@ -26,15 +29,18 @@ class AvailableToolServiceTest {
|
||||
|
||||
private ToolService toolService;
|
||||
private McpServerService mcpServerService;
|
||||
private ToolRegistry toolRegistry;
|
||||
private AvailableToolService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
toolService = mock(ToolService.class);
|
||||
mcpServerService = mock(McpServerService.class);
|
||||
service = new AvailableToolService(toolService, mcpServerService);
|
||||
toolRegistry = mock(ToolRegistry.class);
|
||||
service = new AvailableToolService(toolService, mcpServerService, toolRegistry);
|
||||
when(toolService.listEnabledTools()).thenReturn(List.of());
|
||||
when(mcpServerService.listEnabled()).thenReturn(List.of());
|
||||
when(toolRegistry.listAvailablePluginTools()).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -50,6 +56,27 @@ class AvailableToolServiceTest {
|
||||
assertEquals(Set.of("builtin", "mcp"), sources);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("plugin-registered callbacks appear as bindable agent picker tools")
|
||||
void includesPluginRegisteredCallbacks() {
|
||||
ToolCallback pluginTool = pluginCallback("custom_invoice_lookup", "Look up invoices from a plugin");
|
||||
when(toolRegistry.listAvailablePluginTools()).thenReturn(List.of(pluginTool));
|
||||
|
||||
List<AvailableToolDTO> out = service.listAvailable();
|
||||
|
||||
assertEquals(1, out.size());
|
||||
AvailableToolDTO dto = out.get(0);
|
||||
assertEquals("plugin", dto.getSource());
|
||||
assertEquals("plugin#custom_invoice_lookup", dto.getRowId());
|
||||
assertEquals("custom_invoice_lookup", dto.getName());
|
||||
assertEquals("custom_invoice_lookup", dto.getRawName());
|
||||
assertEquals("Look up invoices from a plugin", dto.getDescription());
|
||||
assertEquals("Plugin tools", dto.getGroup());
|
||||
assertEquals("plugin", dto.getGroupId());
|
||||
assertTrue(dto.isAvailable());
|
||||
assertFalse(dto.isStale());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MCP entry name equals McpToolNameResolver.prefixedName(serverId, raw)")
|
||||
void mcpNameMatchesResolver() {
|
||||
@ -221,4 +248,14 @@ class AvailableToolServiceTest {
|
||||
s.setToolsCacheJson(cacheJson);
|
||||
return s;
|
||||
}
|
||||
|
||||
private static ToolCallback pluginCallback(String name, String description) {
|
||||
ToolCallback callback = mock(ToolCallback.class);
|
||||
when(callback.getToolDefinition()).thenReturn(ToolDefinition.builder()
|
||||
.name(name)
|
||||
.description(description)
|
||||
.inputSchema("{}")
|
||||
.build());
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
|
||||
@ -522,8 +522,8 @@ export const toolApi = {
|
||||
list: () => http.get('/tools'),
|
||||
listEnabled: () => http.get('/tools/enabled'),
|
||||
/**
|
||||
* Unified picker source for the agent edit tool tab — returns built-in
|
||||
* tools plus every MCP-discovered tool grouped by server. The `name`
|
||||
* Unified picker source for the agent edit tool tab — returns built-in,
|
||||
* channel, plugin, and MCP-discovered tools. The `name`
|
||||
* field is what gets saved into mate_agent_tool.tool_name.
|
||||
*/
|
||||
listAvailable: () => http.get('/tools/available'),
|
||||
|
||||
@ -1409,8 +1409,8 @@ async function openEditModal(agent: Agent) {
|
||||
// RFC-042: /skills is now paginated; binding dropdown only needs enabled skills,
|
||||
// so listEnabled() is both semantically correct and shape-stable (returns array).
|
||||
skillApi.listEnabled(),
|
||||
// /tools/available aggregates built-in tools + every MCP-discovered
|
||||
// tool grouped by server, with stale/available flags so the picker
|
||||
// /tools/available aggregates built-in, channel, plugin, and MCP-discovered
|
||||
// tools, with stale/available flags so the picker
|
||||
// matches the runtime callback set exactly.
|
||||
toolApi.listAvailable(),
|
||||
// Options, not the full provider list: /models is admin-only, and a
|
||||
|
||||
Loading…
Reference in New Issue
Block a user