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);
|
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) {
|
public void invalidateEnabledToolSetCache(String reason) {
|
||||||
enabledToolSetCache = null;
|
enabledToolSetCache = null;
|
||||||
log.debug("Enabled AgentToolSet cache invalidated: {}", reason);
|
log.debug("Enabled AgentToolSet cache invalidated: {}", reason);
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import lombok.NoArgsConstructor;
|
|||||||
* Picker DTO for the unified agent tool selector.
|
* Picker DTO for the unified agent tool selector.
|
||||||
*
|
*
|
||||||
* <p>One row per atomic tool the agent can be bound to — built-in tools
|
* <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}
|
* {@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};
|
* 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
|
* for MCP tools it is the prefixed callback name returned by the resolver
|
||||||
@ -29,7 +30,7 @@ public class AvailableToolDTO {
|
|||||||
*/
|
*/
|
||||||
private String rowId;
|
private String rowId;
|
||||||
|
|
||||||
/** {@code "builtin"} or {@code "mcp"}. */
|
/** {@code "builtin"}, {@code "channel"}, {@code "plugin"}, or {@code "mcp"}. */
|
||||||
private String source;
|
private String source;
|
||||||
|
|
||||||
/** MCP server id when {@code source == "mcp"}; null otherwise. */
|
/** MCP server id when {@code source == "mcp"}; null otherwise. */
|
||||||
@ -123,6 +124,23 @@ public class AvailableToolDTO {
|
|||||||
.build();
|
.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) {
|
private static String extractChannelName(String displayName) {
|
||||||
if (displayName == null) return "";
|
if (displayName == null) return "";
|
||||||
int open = displayName.lastIndexOf('(');
|
int open = displayName.lastIndexOf('(');
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import cn.hutool.json.JSONObject;
|
|||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.tool.ToolCallback;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import vip.mate.tool.ToolRegistry;
|
||||||
import vip.mate.tool.mcp.model.McpServerEntity;
|
import vip.mate.tool.mcp.model.McpServerEntity;
|
||||||
import vip.mate.tool.mcp.runtime.McpHashCollisionDetector;
|
import vip.mate.tool.mcp.runtime.McpHashCollisionDetector;
|
||||||
import vip.mate.tool.mcp.service.McpServerService;
|
import vip.mate.tool.mcp.service.McpServerService;
|
||||||
@ -19,8 +21,9 @@ import java.util.List;
|
|||||||
* Aggregator behind {@code GET /api/v1/tools/available}.
|
* Aggregator behind {@code GET /api/v1/tools/available}.
|
||||||
*
|
*
|
||||||
* <p>Returns one DTO per atomic tool the agent edit picker can offer:
|
* <p>Returns one DTO per atomic tool the agent edit picker can offer:
|
||||||
* built-in tools (from {@link ToolService#listEnabledTools()}) plus every
|
* built-in tools (from {@link ToolService#listEnabledTools()}), plugin
|
||||||
* MCP tool persisted in {@link McpServerEntity#getToolsCacheJson()}.
|
* 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()}
|
* <p>Reads the cache rather than making a live MCP {@code listTools()}
|
||||||
* roundtrip so the picker stays fast and stable through brief upstream
|
* 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
|
* this, the user could save a {@code mate_agent_tool.tool_name} that
|
||||||
* resolves to nothing at chat time.
|
* resolves to nothing at chat time.
|
||||||
*
|
*
|
||||||
* <p><b>Scope</b>: this aggregator covers the two tool sources users can
|
* <p><b>Scope</b>: this aggregator covers the tool sources users can bind
|
||||||
* bind from the agent edit screen — built-in {@code @Tool} beans
|
* from the agent edit screen. Every emitted {@code name} must match the
|
||||||
* (persisted in {@code mate_tool}) and MCP-discovered tools (cached on
|
* runtime callback key accepted by {@code AgentToolSet}; otherwise the UI
|
||||||
* the server row). Plugin-registered {@code ToolCallback} beans surfaced
|
* could persist a {@code mate_agent_tool.tool_name} row that resolves to
|
||||||
* by other parts of the runtime are intentionally NOT listed here: those
|
* nothing at chat time.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -52,10 +49,12 @@ public class AvailableToolService {
|
|||||||
|
|
||||||
private final ToolService toolService;
|
private final ToolService toolService;
|
||||||
private final McpServerService mcpServerService;
|
private final McpServerService mcpServerService;
|
||||||
|
private final ToolRegistry toolRegistry;
|
||||||
|
|
||||||
public List<AvailableToolDTO> listAvailable() {
|
public List<AvailableToolDTO> listAvailable() {
|
||||||
List<AvailableToolDTO> out = new ArrayList<>();
|
List<AvailableToolDTO> out = new ArrayList<>();
|
||||||
appendBuiltinTools(out);
|
appendBuiltinTools(out);
|
||||||
|
appendPluginTools(out);
|
||||||
appendMcpTools(out);
|
appendMcpTools(out);
|
||||||
return 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) {
|
private void appendMcpTools(List<AvailableToolDTO> out) {
|
||||||
List<McpServerEntity> servers;
|
List<McpServerEntity> servers;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -3,6 +3,9 @@ package vip.mate.tool.service;
|
|||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
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.model.McpServerEntity;
|
||||||
import vip.mate.tool.mcp.runtime.McpToolNameResolver;
|
import vip.mate.tool.mcp.runtime.McpToolNameResolver;
|
||||||
import vip.mate.tool.mcp.service.McpServerService;
|
import vip.mate.tool.mcp.service.McpServerService;
|
||||||
@ -26,15 +29,18 @@ class AvailableToolServiceTest {
|
|||||||
|
|
||||||
private ToolService toolService;
|
private ToolService toolService;
|
||||||
private McpServerService mcpServerService;
|
private McpServerService mcpServerService;
|
||||||
|
private ToolRegistry toolRegistry;
|
||||||
private AvailableToolService service;
|
private AvailableToolService service;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
toolService = mock(ToolService.class);
|
toolService = mock(ToolService.class);
|
||||||
mcpServerService = mock(McpServerService.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(toolService.listEnabledTools()).thenReturn(List.of());
|
||||||
when(mcpServerService.listEnabled()).thenReturn(List.of());
|
when(mcpServerService.listEnabled()).thenReturn(List.of());
|
||||||
|
when(toolRegistry.listAvailablePluginTools()).thenReturn(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -50,6 +56,27 @@ class AvailableToolServiceTest {
|
|||||||
assertEquals(Set.of("builtin", "mcp"), sources);
|
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
|
@Test
|
||||||
@DisplayName("MCP entry name equals McpToolNameResolver.prefixedName(serverId, raw)")
|
@DisplayName("MCP entry name equals McpToolNameResolver.prefixedName(serverId, raw)")
|
||||||
void mcpNameMatchesResolver() {
|
void mcpNameMatchesResolver() {
|
||||||
@ -221,4 +248,14 @@ class AvailableToolServiceTest {
|
|||||||
s.setToolsCacheJson(cacheJson);
|
s.setToolsCacheJson(cacheJson);
|
||||||
return s;
|
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'),
|
list: () => http.get('/tools'),
|
||||||
listEnabled: () => http.get('/tools/enabled'),
|
listEnabled: () => http.get('/tools/enabled'),
|
||||||
/**
|
/**
|
||||||
* Unified picker source for the agent edit tool tab — returns built-in
|
* Unified picker source for the agent edit tool tab — returns built-in,
|
||||||
* tools plus every MCP-discovered tool grouped by server. The `name`
|
* channel, plugin, and MCP-discovered tools. The `name`
|
||||||
* field is what gets saved into mate_agent_tool.tool_name.
|
* field is what gets saved into mate_agent_tool.tool_name.
|
||||||
*/
|
*/
|
||||||
listAvailable: () => http.get('/tools/available'),
|
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,
|
// RFC-042: /skills is now paginated; binding dropdown only needs enabled skills,
|
||||||
// so listEnabled() is both semantically correct and shape-stable (returns array).
|
// so listEnabled() is both semantically correct and shape-stable (returns array).
|
||||||
skillApi.listEnabled(),
|
skillApi.listEnabled(),
|
||||||
// /tools/available aggregates built-in tools + every MCP-discovered
|
// /tools/available aggregates built-in, channel, plugin, and MCP-discovered
|
||||||
// tool grouped by server, with stale/available flags so the picker
|
// tools, with stale/available flags so the picker
|
||||||
// matches the runtime callback set exactly.
|
// matches the runtime callback set exactly.
|
||||||
toolApi.listAvailable(),
|
toolApi.listAvailable(),
|
||||||
// Options, not the full provider list: /models is admin-only, and a
|
// Options, not the full provider list: /models is admin-only, and a
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user