feat(autoconfigure): Support both FunctionCallback and ToolCallback in ToolCallingAutoConfiguration
- Extends the ToolCallingAutoConfiguration to support both FunctionCallback and ToolCallback types. - The toolCallbackResolver bean now handles both callback types through ObjectProvider injection. - Added comprehensive tests to verify the resolution of multiple function and tool callbacks. - Introduce new StaticToolCallbackProvider implementation - Update ToolCallbackProvider to return FunctionCallback[] - Migrate from List to ToolCallbackProvider in configurations - Update tests to use new provider pattern - Enhance tool callback providers to support multiple clients - Refactor AsyncMcpToolCallbackProvider and SyncMcpToolCallbackProvider to handle multiple MCP clients - Add ToolCallbackProvider support to ChatClient API - Deprecate direct tool callback list methods in favor of providers - Fix typos in Closeable class names - Update MCP documentation with new examples and usage patterns Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
@@ -27,10 +27,12 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpAsyncClientConfigurer;
|
||||
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpSyncClientConfigurer;
|
||||
import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties;
|
||||
import org.springframework.ai.mcp.McpToolUtils;
|
||||
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
|
||||
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
@@ -176,9 +178,22 @@ public class McpClientAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
|
||||
matchIfMissing = true)
|
||||
public List<ToolCallback> toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
|
||||
public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
|
||||
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
|
||||
return McpToolUtils.getToolCallbacksFromSyncClients(mcpClients);
|
||||
return new SyncMcpToolCallbackProvider(mcpClients);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated replaced by {@link #toolCallbacks(ObjectProvider)} that returns a
|
||||
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
|
||||
*/
|
||||
@Deprecated
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
|
||||
matchIfMissing = true)
|
||||
public List<ToolCallback> toolCallbacksDeprecated(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
|
||||
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
|
||||
return List.of(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,7 +204,7 @@ public class McpClientAutoConfiguration {
|
||||
* This class is responsible for closing all MCP sync clients when the application
|
||||
* context is closed, preventing resource leaks.
|
||||
*/
|
||||
public record ClosebleMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
|
||||
public record CloseableMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
@@ -205,8 +220,8 @@ public class McpClientAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
|
||||
matchIfMissing = true)
|
||||
public ClosebleMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
|
||||
return new ClosebleMcpSyncClients(clients);
|
||||
public CloseableMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
|
||||
return new CloseableMcpSyncClients(clients);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,14 +278,26 @@ public class McpClientAutoConfiguration {
|
||||
return mcpSyncClients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated replaced by {@link #asyncToolCallbacks(ObjectProvider)} that returns a
|
||||
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
|
||||
*/
|
||||
@Deprecated
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
|
||||
public List<ToolCallback> asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
|
||||
public List<ToolCallback> asyncToolCallbacksDeprecated(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
|
||||
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
|
||||
return McpToolUtils.getToolCallbacksFromAsyncClinents(mcpClients);
|
||||
return List.of(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
|
||||
}
|
||||
|
||||
public record ClosebleMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
|
||||
public ToolCallbackProvider asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
|
||||
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
|
||||
return new AsyncMcpToolCallbackProvider(mcpClients);
|
||||
}
|
||||
|
||||
public record CloseableMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
|
||||
@Override
|
||||
public void close() {
|
||||
this.clients.forEach(McpAsyncClient::close);
|
||||
@@ -279,8 +306,8 @@ public class McpClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
|
||||
public ClosebleMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
|
||||
return new ClosebleMcpAsyncClients(clients);
|
||||
public CloseableMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
|
||||
return new CloseableMcpAsyncClients(clients);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -122,7 +122,7 @@ public class McpClientAutoConfigurationIT {
|
||||
@Test
|
||||
void closeableWrappersCreation() {
|
||||
this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> {
|
||||
assertThat(context).hasSingleBean(McpClientAutoConfiguration.ClosebleMcpSyncClients.class);
|
||||
assertThat(context).hasSingleBean(McpClientAutoConfiguration.CloseableMcpSyncClients.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.mcp.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.modelcontextprotocol.server.McpAsyncServer;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
@@ -39,7 +41,9 @@ import io.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.mcp.McpToolUtils;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
@@ -135,7 +139,8 @@ public class MpcServerAutoConfiguration {
|
||||
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
|
||||
ObjectProvider<List<SyncToolRegistration>> tools, ObjectProvider<List<SyncResourceRegistration>> resources,
|
||||
ObjectProvider<List<SyncPromptRegistration>> prompts,
|
||||
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers,
|
||||
List<ToolCallbackProvider> toolCallbackProvider) {
|
||||
|
||||
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
|
||||
serverProperties.getVersion());
|
||||
@@ -143,7 +148,14 @@ public class MpcServerAutoConfiguration {
|
||||
// Create the server with both tool and resource capabilities
|
||||
SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
|
||||
|
||||
List<SyncToolRegistration> toolResgistrations = tools.stream().flatMap(List::stream).toList();
|
||||
List<SyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
|
||||
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
|
||||
.map(pr -> List.of(pr.getToolCallbacks()))
|
||||
.flatMap(List::stream)
|
||||
.filter(fc -> fc instanceof ToolCallback)
|
||||
.map(fc -> (ToolCallback) fc)
|
||||
.toList();
|
||||
toolResgistrations.addAll(McpToolUtils.toSyncToolRegistration(providerToolCallbacks));
|
||||
if (!CollectionUtils.isEmpty(toolResgistrations)) {
|
||||
serverBuilder.tools(toolResgistrations);
|
||||
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
|
||||
@@ -191,7 +203,8 @@ public class MpcServerAutoConfiguration {
|
||||
ObjectProvider<List<AsyncToolRegistration>> tools,
|
||||
ObjectProvider<List<AsyncResourceRegistration>> resources,
|
||||
ObjectProvider<List<AsyncPromptRegistration>> prompts,
|
||||
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumer) {
|
||||
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumer,
|
||||
List<ToolCallbackProvider> toolCallbackProvider) {
|
||||
|
||||
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
|
||||
serverProperties.getVersion());
|
||||
@@ -199,7 +212,14 @@ public class MpcServerAutoConfiguration {
|
||||
// Create the server with both tool and resource capabilities
|
||||
AsyncSpec serverBilder = McpServer.async(transport).serverInfo(serverInfo);
|
||||
|
||||
List<AsyncToolRegistration> toolResgistrations = tools.stream().flatMap(List::stream).toList();
|
||||
List<AsyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
|
||||
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
|
||||
.map(pr -> List.of(pr.getToolCallbacks()))
|
||||
.flatMap(List::stream)
|
||||
.filter(fc -> fc instanceof ToolCallback)
|
||||
.map(fc -> (ToolCallback) fc)
|
||||
.toList();
|
||||
toolResgistrations.addAll(McpToolUtils.toAsyncToolRegistration(providerToolCallbacks));
|
||||
if (!CollectionUtils.isEmpty(toolResgistrations)) {
|
||||
serverBilder.tools(toolResgistrations);
|
||||
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
package org.springframework.ai.mcp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpAsyncClient;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
@@ -28,18 +29,20 @@ import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools
|
||||
* asynchronously.
|
||||
* asynchronously from one or more MCP servers.
|
||||
* <p>
|
||||
* This class acts as a tool provider for Spring AI, automatically discovering tools from
|
||||
* an MCP server and making them available as Spring AI tools. It:
|
||||
* multiple MCP servers and making them available as Spring AI tools. It:
|
||||
* <ul>
|
||||
* <li>Connects to an MCP server through an async client</li>
|
||||
* <li>Lists and retrieves available tools from the server</li>
|
||||
* <li>Connects to MCP servers through async clients</li>
|
||||
* <li>Lists and retrieves available tools from each server asynchronously</li>
|
||||
* <li>Creates {@link AsyncMcpToolCallback} instances for each discovered tool</li>
|
||||
* <li>Validates tool names to prevent duplicates</li>
|
||||
* <li>Validates tool names to prevent duplicates across all servers</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* Example usage with a single client:
|
||||
*
|
||||
* <pre>{@code
|
||||
* McpAsyncClient mcpClient = // obtain MCP client
|
||||
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
|
||||
*
|
||||
@@ -47,6 +50,19 @@ import org.springframework.util.CollectionUtils;
|
||||
* ToolCallback[] tools = provider.getToolCallbacks();
|
||||
* }</pre>
|
||||
*
|
||||
* Example usage with multiple clients:
|
||||
*
|
||||
* <pre>{@code
|
||||
* List<McpAsyncClient> mcpClients = // obtain multiple MCP clients
|
||||
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClients);
|
||||
*
|
||||
* // Get tools from all clients
|
||||
* ToolCallback[] tools = provider.getToolCallbacks();
|
||||
*
|
||||
* // Or use the reactive API
|
||||
* Flux<ToolCallback> toolsFlux = AsyncMcpToolCallbackProvider.asyncToolCallbacks(mcpClients);
|
||||
* }</pre>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @see ToolCallbackProvider
|
||||
@@ -55,40 +71,61 @@ import org.springframework.util.CollectionUtils;
|
||||
*/
|
||||
public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
|
||||
private final McpAsyncClient mcpClient;
|
||||
private final List<McpAsyncClient> mcpClients;
|
||||
|
||||
/**
|
||||
* Creates a new {@code AsyncMcpToolCallbackProvider} instance.
|
||||
* @param mcpClient the MCP client to use for discovering tools
|
||||
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
|
||||
* clients.
|
||||
* @param mcpClients the list of MCP clients to use for discovering tools. Each client
|
||||
* typically connects to a different MCP server, allowing tool discovery from multiple
|
||||
* sources.
|
||||
* @throws IllegalArgumentException if mcpClients is null
|
||||
*/
|
||||
public AsyncMcpToolCallbackProvider(McpAsyncClient mcpClient) {
|
||||
this.mcpClient = mcpClient;
|
||||
public AsyncMcpToolCallbackProvider(List<McpAsyncClient> mcpClients) {
|
||||
Assert.notNull(mcpClients, "McpClients must not be null");
|
||||
this.mcpClients = mcpClients;
|
||||
}
|
||||
|
||||
public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients) {
|
||||
Assert.notNull(mcpClients, "McpClients must not be null");
|
||||
this.mcpClients = List.of(mcpClients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and returns all available tools from the MCP server asynchronously.
|
||||
* Discovers and returns all available tools from the configured MCP servers.
|
||||
* <p>
|
||||
* This method:
|
||||
* <ol>
|
||||
* <li>Retrieves the list of tools from the MCP server</li>
|
||||
* <li>Creates a {@link AsyncMcpToolCallback} for each tool</li>
|
||||
* <li>Validates that there are no duplicate tool names</li>
|
||||
* <li>Retrieves the list of tools from each MCP server asynchronously</li>
|
||||
* <li>Creates a {@link AsyncMcpToolCallback} for each discovered tool</li>
|
||||
* <li>Validates that there are no duplicate tool names across all servers</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Note: While the underlying tool discovery is asynchronous, this method blocks until
|
||||
* all tools are discovered from all servers.
|
||||
* @return an array of tool callbacks, one for each discovered tool
|
||||
* @throws IllegalStateException if duplicate tool names are found
|
||||
*/
|
||||
@Override
|
||||
public ToolCallback[] getToolCallbacks() {
|
||||
var toolCallbacks = this.mcpClient.listTools()
|
||||
.map(response -> response.tools()
|
||||
.stream()
|
||||
.map(tool -> new AsyncMcpToolCallback(this.mcpClient, tool))
|
||||
.toArray(ToolCallback[]::new))
|
||||
.block();
|
||||
|
||||
validateToolCallbacks(toolCallbacks);
|
||||
List<ToolCallback> toolCallbackList = new ArrayList<>();
|
||||
|
||||
return toolCallbacks;
|
||||
for (McpAsyncClient mcpClient : this.mcpClients) {
|
||||
|
||||
ToolCallback[] toolCallbacks = mcpClient.listTools()
|
||||
.map(response -> response.tools()
|
||||
.stream()
|
||||
.map(tool -> new AsyncMcpToolCallback(mcpClient, tool))
|
||||
.toArray(ToolCallback[]::new))
|
||||
.block();
|
||||
|
||||
validateToolCallbacks(toolCallbacks);
|
||||
|
||||
toolCallbackList.addAll(List.of(toolCallbacks));
|
||||
}
|
||||
|
||||
return toolCallbackList.toArray(new ToolCallback[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,12 +147,19 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
/**
|
||||
* Creates a reactive stream of tool callbacks from multiple MCP clients.
|
||||
* <p>
|
||||
* This utility method:
|
||||
* This utility method provides a reactive way to work with tool callbacks from
|
||||
* multiple MCP clients in a single operation. It:
|
||||
* <ol>
|
||||
* <li>Takes a list of MCP clients</li>
|
||||
* <li>Creates a provider for each client</li>
|
||||
* <li>Retrieves and flattens all tool callbacks into a single stream</li>
|
||||
* <li>Takes a list of MCP clients as input</li>
|
||||
* <li>Creates a provider instance to manage all clients</li>
|
||||
* <li>Retrieves tools from all clients asynchronously</li>
|
||||
* <li>Combines them into a single reactive stream</li>
|
||||
* <li>Ensures there are no naming conflicts between tools from different clients</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* Unlike {@link #getToolCallbacks()}, this method provides a fully reactive way to
|
||||
* work with tool callbacks, making it suitable for non-blocking applications. Any
|
||||
* errors during tool discovery will be propagated through the returned Flux.
|
||||
* @param mcpClients the list of MCP clients to create callbacks from
|
||||
* @return a Flux of tool callbacks from all provided clients
|
||||
*/
|
||||
@@ -124,9 +168,7 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
return Flux.fromIterable(mcpClients)
|
||||
.flatMap(mcpClient -> Mono.just(new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks()))
|
||||
.flatMap(callbacks -> Flux.fromArray(callbacks));
|
||||
return Flux.fromArray(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -211,10 +211,7 @@ public final class McpToolUtils {
|
||||
if (CollectionUtils.isEmpty(mcpClients)) {
|
||||
return List.of();
|
||||
}
|
||||
return mcpClients.stream()
|
||||
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
return List.of((new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,10 +244,7 @@ public final class McpToolUtils {
|
||||
if (CollectionUtils.isEmpty(asynMcpClients)) {
|
||||
return List.of();
|
||||
}
|
||||
return asynMcpClients.stream()
|
||||
.map(mcpClient -> List.of((new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
return List.of((new AsyncMcpToolCallbackProvider(asynMcpClients).getToolCallbacks()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.ai.mcp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
@@ -25,18 +26,21 @@ import org.springframework.ai.tool.util.ToolUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools.
|
||||
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools
|
||||
* from one or more MCP servers.
|
||||
* <p>
|
||||
* This class acts as a tool provider for Spring AI, automatically discovering tools from
|
||||
* an MCP server and making them available as Spring AI tools. It:
|
||||
* multiple MCP servers and making them available as Spring AI tools. It:
|
||||
* <ul>
|
||||
* <li>Connects to an MCP server through a sync client</li>
|
||||
* <li>Lists and retrieves available tools from the server</li>
|
||||
* <li>Connects to one or more MCP servers through sync clients</li>
|
||||
* <li>Lists and retrieves available tools from all connected servers</li>
|
||||
* <li>Creates {@link SyncMcpToolCallback} instances for each discovered tool</li>
|
||||
* <li>Validates tool names to prevent duplicates</li>
|
||||
* <li>Validates tool names to prevent duplicates across all servers</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* Example usage with a single client:
|
||||
*
|
||||
* <pre>{@code
|
||||
* McpSyncClient mcpClient = // obtain MCP client
|
||||
* ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
|
||||
*
|
||||
@@ -44,6 +48,16 @@ import org.springframework.util.CollectionUtils;
|
||||
* ToolCallback[] tools = provider.getToolCallbacks();
|
||||
* }</pre>
|
||||
*
|
||||
* Example usage with multiple clients:
|
||||
*
|
||||
* <pre>{@code
|
||||
* List<McpSyncClient> mcpClients = // obtain multiple MCP clients
|
||||
* ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClients);
|
||||
*
|
||||
* // Get tools from all clients
|
||||
* ToolCallback[] tools = provider.getToolCallbacks();
|
||||
* }</pre>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @see ToolCallbackProvider
|
||||
@@ -53,24 +67,29 @@ import org.springframework.util.CollectionUtils;
|
||||
|
||||
public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
|
||||
private final McpSyncClient mcpClient;
|
||||
private final List<McpSyncClient> mcpClients;
|
||||
|
||||
/**
|
||||
* Creates a new {@code SyncMcpToolCallbackProvider} instance.
|
||||
* @param mcpClient the MCP client to use for discovering tools
|
||||
* Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP
|
||||
* clients.
|
||||
* @param mcpClients the list of MCP clients to use for discovering tools
|
||||
*/
|
||||
public SyncMcpToolCallbackProvider(McpSyncClient mcpClient) {
|
||||
this.mcpClient = mcpClient;
|
||||
public SyncMcpToolCallbackProvider(List<McpSyncClient> mcpClients) {
|
||||
this.mcpClients = mcpClients;
|
||||
}
|
||||
|
||||
public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients) {
|
||||
this.mcpClients = List.of(mcpClients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers and returns all available tools from the MCP server.
|
||||
* Discovers and returns all available tools from all connected MCP servers.
|
||||
* <p>
|
||||
* This method:
|
||||
* <ol>
|
||||
* <li>Retrieves the list of tools from the MCP server</li>
|
||||
* <li>Creates a {@link SyncMcpToolCallback} for each tool</li>
|
||||
* <li>Validates that there are no duplicate tool names</li>
|
||||
* <li>Retrieves the list of tools from each connected MCP server</li>
|
||||
* <li>Creates a {@link SyncMcpToolCallback} for each discovered tool</li>
|
||||
* <li>Validates that there are no duplicate tool names across all servers</li>
|
||||
* </ol>
|
||||
* @return an array of tool callbacks, one for each discovered tool
|
||||
* @throws IllegalStateException if duplicate tool names are found
|
||||
@@ -78,16 +97,18 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
@Override
|
||||
public ToolCallback[] getToolCallbacks() {
|
||||
|
||||
var toolCallbacks = this.mcpClient.listTools()
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new SyncMcpToolCallback(this.mcpClient, tool))
|
||||
.toArray(ToolCallback[]::new);
|
||||
|
||||
validateToolCallbacks(toolCallbacks);
|
||||
|
||||
return toolCallbacks;
|
||||
var toolCallbacks = new ArrayList<>();
|
||||
|
||||
mcpClients.stream().forEach(mcpClient -> {
|
||||
toolCallbacks.addAll(mcpClient.listTools()
|
||||
.tools()
|
||||
.stream()
|
||||
.map(tool -> new SyncMcpToolCallback(mcpClient, tool))
|
||||
.toList());
|
||||
});
|
||||
var array = toolCallbacks.toArray(new ToolCallback[0]);
|
||||
validateToolCallbacks(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,13 +128,15 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of tool callbacks from multiple MCP clients.
|
||||
* Creates a consolidated list of tool callbacks from multiple MCP clients.
|
||||
* <p>
|
||||
* This utility method:
|
||||
* This utility method provides a convenient way to create tool callbacks from
|
||||
* multiple MCP clients in a single operation. It:
|
||||
* <ol>
|
||||
* <li>Takes a list of MCP clients</li>
|
||||
* <li>Creates a provider for each client</li>
|
||||
* <li>Retrieves and combines all tool callbacks into a single list</li>
|
||||
* <li>Takes a list of MCP clients as input</li>
|
||||
* <li>Creates a provider instance to manage all clients</li>
|
||||
* <li>Retrieves tools from all clients and combines them into a single list</li>
|
||||
* <li>Ensures there are no naming conflicts between tools from different clients</li>
|
||||
* </ol>
|
||||
* @param mcpClients the list of MCP clients to create callbacks from
|
||||
* @return a list of tool callbacks from all provided clients
|
||||
@@ -123,10 +146,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
|
||||
if (CollectionUtils.isEmpty(mcpClients)) {
|
||||
return List.of();
|
||||
}
|
||||
return mcpClients.stream()
|
||||
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
return List.of((new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
@@ -183,9 +183,8 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
|
||||
public static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
public List<ToolCallback> paymentServiceTools() {
|
||||
var tools = List.of(ToolCallbacks.from(new PaymentService()));
|
||||
return tools;
|
||||
public ToolCallbackProvider paymentServiceTools() {
|
||||
return ToolCallbackProvider.from(List.of(ToolCallbacks.from(new PaymentService())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -221,11 +220,11 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
|
||||
|
||||
@Bean
|
||||
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
|
||||
List<ToolCallback> toolCallbacks, List<FunctionCallback> functionCallbacks,
|
||||
List<ToolCallbackProvider> tcps, List<FunctionCallback> functionCallbacks,
|
||||
ObjectProvider<ObservationRegistry> observationRegistry) {
|
||||
|
||||
List<FunctionCallback> allFunctionCallbacks = new ArrayList(functionCallbacks);
|
||||
allFunctionCallbacks.addAll(toolCallbacks.stream().map(tc -> (FunctionCallback) tc).toList());
|
||||
tcps.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionCallbacks::addAll);
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionCallbacks);
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.ai.converter.StructuredOutputConverter;
|
||||
import org.springframework.ai.model.Media;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -223,6 +224,8 @@ public interface ChatClient {
|
||||
|
||||
ChatClientRequestSpec tools(Object... toolObjects);
|
||||
|
||||
ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders);
|
||||
|
||||
@Deprecated
|
||||
<I, O> ChatClientRequestSpec functions(FunctionCallback... functionCallbacks);
|
||||
|
||||
@@ -290,6 +293,8 @@ public interface ChatClient {
|
||||
|
||||
Builder defaultTools(Object... toolObjects);
|
||||
|
||||
Builder defaultTools(ToolCallbackProvider... toolCallbackProviders);
|
||||
|
||||
/**
|
||||
* @deprecated in favor of {@link #defaultTools(String...)}
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
|
||||
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallbacks;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
@@ -871,6 +872,16 @@ public class DefaultChatClient implements ChatClient {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders) {
|
||||
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
|
||||
Assert.noNullElements(toolCallbackProviders, "toolCallbackProviders cannot contain null elements");
|
||||
for (ToolCallbackProvider toolCallbackProvider : toolCallbackProviders) {
|
||||
this.functionCallbacks.addAll(List.of(toolCallbackProvider.getToolCallbacks()));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use tools()
|
||||
public ChatClientRequestSpec functions(String... functionBeanNames) {
|
||||
return tools(functionBeanNames);
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -173,6 +174,12 @@ public class DefaultChatClientBuilder implements Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder defaultTools(ToolCallbackProvider... toolCallbackProviders) {
|
||||
this.defaultRequest.tools(toolCallbackProviders);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Deprecated // Use defaultTools()
|
||||
public <I, O> Builder defaultFunction(String name, String description, java.util.function.Function<I, O> function) {
|
||||
this.defaultRequest
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2025 - 2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ai.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A simple implementation of {@link ToolCallbackProvider} that maintains a static array
|
||||
* of {@link FunctionCallback} objects. This provider is immutable after construction and
|
||||
* provides a straightforward way to supply a fixed set of tool callbacks to AI models.
|
||||
*
|
||||
* <p>
|
||||
* This implementation is thread-safe as it maintains an immutable array of callbacks that
|
||||
* is set during construction and cannot be modified afterwards.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* FunctionCallback callback1 = new MyFunctionCallback();
|
||||
* FunctionCallback callback2 = new AnotherFunctionCallback();
|
||||
*
|
||||
* // Create provider with varargs constructor
|
||||
* ToolCallbackProvider provider1 = new StaticToolCallbackProvider(callback1, callback2);
|
||||
*
|
||||
* // Or create provider with List constructor
|
||||
* List<FunctionCallback> callbacks = Arrays.asList(callback1, callback2);
|
||||
* ToolCallbackProvider provider2 = new StaticToolCallbackProvider(callbacks);
|
||||
* }</pre>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
* @see ToolCallbackProvider
|
||||
* @see FunctionCallback
|
||||
*/
|
||||
public class StaticToolCallbackProvider implements ToolCallbackProvider {
|
||||
|
||||
private final FunctionCallback[] toolCallbacks;
|
||||
|
||||
/**
|
||||
* Constructs a new StaticToolCallbackProvider with the specified array of function
|
||||
* callbacks.
|
||||
* @param toolCallbacks the array of function callbacks to be provided by this
|
||||
* provider. Must not be null, though an empty array is permitted.
|
||||
* @throws IllegalArgumentException if the toolCallbacks array is null
|
||||
*/
|
||||
public StaticToolCallbackProvider(FunctionCallback... toolCallbacks) {
|
||||
Assert.notNull(toolCallbacks, "ToolCallbacks must not be null");
|
||||
this.toolCallbacks = toolCallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new StaticToolCallbackProvider with the specified list of function
|
||||
* callbacks. The list is converted to an array internally.
|
||||
* @param toolCallbacks the list of function callbacks to be provided by this
|
||||
* provider. Must not be null and must not contain null elements.
|
||||
* @throws IllegalArgumentException if the toolCallbacks list is null or contains null
|
||||
* elements
|
||||
*/
|
||||
public StaticToolCallbackProvider(List<? extends FunctionCallback> toolCallbacks) {
|
||||
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
|
||||
this.toolCallbacks = toolCallbacks.toArray(new FunctionCallback[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of function callbacks held by this provider.
|
||||
* @return an array containing all function callbacks provided during construction.
|
||||
* The returned array is a direct reference to the internal array, as the callbacks
|
||||
* are expected to be immutable.
|
||||
*/
|
||||
@Override
|
||||
public FunctionCallback[] getToolCallbacks() {
|
||||
return this.toolCallbacks;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.ai.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
|
||||
/**
|
||||
* Provides {@link ToolCallback} instances for tools defined in different sources.
|
||||
*
|
||||
@@ -24,6 +28,14 @@ package org.springframework.ai.tool;
|
||||
*/
|
||||
public interface ToolCallbackProvider {
|
||||
|
||||
ToolCallback[] getToolCallbacks();
|
||||
FunctionCallback[] getToolCallbacks();
|
||||
|
||||
public static ToolCallbackProvider from(List<? extends FunctionCallback> toolCallbacks) {
|
||||
return new StaticToolCallbackProvider(toolCallbacks);
|
||||
}
|
||||
|
||||
public static ToolCallbackProvider from(FunctionCallback... toolCallbacks) {
|
||||
return new StaticToolCallbackProvider(toolCallbacks);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -351,12 +351,14 @@ private List<McpSyncClient> mcpSyncClients; // For sync client
|
||||
private List<McpAsyncClient> mcpAsyncClients; // For async client
|
||||
----
|
||||
|
||||
Additionally, the registered MCP Tools with all MCP clients are provided as a list of ToolCallback instances:
|
||||
Additionally, the registered MCP Tools with all MCP clients are provided as a list of ToolCallback
|
||||
through a ToolCallbackProvider instance:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Autowired
|
||||
private List<ToolCallback> toolCallbacks;
|
||||
private SyncMcpToolCallbackProvider toolCallbackProvider;
|
||||
ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
|
||||
----
|
||||
|
||||
== Example Applications
|
||||
|
||||
@@ -126,9 +126,9 @@ Allows servers to expose tools that can be invoked by language models. The MCP S
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public List<ToolCallback> myTools(...) {
|
||||
public ToolCallbackProvider myTools(...) {
|
||||
List<ToolCallback> tools = ...
|
||||
return tools;
|
||||
return ToolCallbackProvider.from(tools);
|
||||
}
|
||||
----
|
||||
|
||||
@@ -284,15 +284,15 @@ public class McpServerApplication {
|
||||
SpringApplication.run(McpServerApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<ToolCallback> tools(WeatherService weatherService) {
|
||||
return ToolCallbacks.from(weatherService);
|
||||
}
|
||||
@Bean
|
||||
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The auto-configuration will automatically register the tool callbacks as MCP tools.
|
||||
You can have multiple beans producing lists of ToolCallbacks. The auto-configuration will merge them.
|
||||
You can have multiple beans producing ToolCallbacks. The auto-configuration will merge them.
|
||||
|
||||
== Example Applications
|
||||
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server[Weather Server (WebFlux)] - Spring AI MCP Server Boot Starter with WebFlux transport.
|
||||
|
||||
@@ -16,10 +16,15 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.chat.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
|
||||
@@ -33,12 +38,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Auto-configuration for common tool calling features of {@link ChatModel}.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@@ -48,8 +52,13 @@ public class ToolCallingAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
|
||||
List<FunctionCallback> toolCallbacks) {
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
|
||||
List<FunctionCallback> functionCallbacks, List<ToolCallbackProvider> tcbProviders) {
|
||||
|
||||
List<FunctionCallback> allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks);
|
||||
tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
|
||||
|
||||
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
|
||||
|
||||
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
|
||||
.applicationContext(applicationContext)
|
||||
.build();
|
||||
|
||||
@@ -16,15 +16,31 @@
|
||||
|
||||
package org.springframework.ai.autoconfigure.chat.model;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.model.tool.DefaultToolCallingManager;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.tool.StaticToolCallbackProvider;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.ToolCallbackProvider;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
|
||||
import org.springframework.ai.tool.function.FunctionToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallback;
|
||||
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
|
||||
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
|
||||
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -32,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* Unit tests for {@link ToolCallingAutoConfiguration}.
|
||||
*
|
||||
* @author Thomas Vitale
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
class ToolCallingAutoConfigurationTests {
|
||||
|
||||
@@ -50,4 +67,127 @@ class ToolCallingAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMultipleFuncitonAndToolCallbacks() {
|
||||
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class)
|
||||
.run(context -> {
|
||||
var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
|
||||
assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("getForecast").getName()).isEqualTo("getForecast");
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("getAlert")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("getAlert").getName()).isEqualTo("getAlert");
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("weatherFunction1")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("weatherFunction1").getName()).isEqualTo("weatherFunction1");
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather3")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getName())
|
||||
.isEqualTo("getCurrentWeather3");
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather4")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getName())
|
||||
.isEqualTo("getCurrentWeather4");
|
||||
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather5")).isNotNull();
|
||||
assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getName())
|
||||
.isEqualTo("getCurrentWeather5");
|
||||
});
|
||||
}
|
||||
|
||||
static class WeatherService {
|
||||
|
||||
@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
public String getForecast(String location) {
|
||||
return "30";
|
||||
}
|
||||
|
||||
@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
public String getForecast2(String location) {
|
||||
return "30";
|
||||
}
|
||||
|
||||
public String getAlert(String usState) {
|
||||
return "Alert";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
// Note: Currently we do not have ToolCallbackResolver implementation that can
|
||||
// resolve the ToolCallback from the Tool annotation.
|
||||
// Therefore we need to provide the ToolCallback instances explicitly using the
|
||||
// ToolCallbacks.from(...) utility method.
|
||||
@Bean
|
||||
public ToolCallbackProvider toolCallbacks() {
|
||||
return MethodToolCallbackProvider.builder().toolObjects(new WeatherService()).build();
|
||||
}
|
||||
|
||||
public record Request(String location) {
|
||||
}
|
||||
|
||||
public record Response(String temperature) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
|
||||
public Function<Request, Response> weatherFunction1() {
|
||||
return request -> new Response("30");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FunctionCallback functionCallbacks3() {
|
||||
return FunctionCallback.builder()
|
||||
.function("getCurrentWeather3", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FunctionCallback functionCallbacks4() {
|
||||
return FunctionCallback.builder()
|
||||
.function("getCurrentWeather4", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallback toolCallbacks5() {
|
||||
return FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallbackProvider blabla() {
|
||||
return new StaticToolCallbackProvider(
|
||||
FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
|
||||
.description("Gets the weather in location")
|
||||
.inputType(Request.class)
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolCallback toolCallbacks6() {
|
||||
var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getAlert", String.class);
|
||||
return MethodToolCallback.builder()
|
||||
.toolDefinition(ToolDefinition.builder(toolMethod).build())
|
||||
.toolMethod(toolMethod)
|
||||
.toolObject(new WeatherService())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user