diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java index 168d4ffd5b..9826accc90 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -365,6 +365,11 @@ public ChatClientRequest options(T options) { return this; } + public ChatClientRequest function(FunctionCallbackWrapper functionWrapper) { + this.functionCallbacks.add(functionWrapper); + return this; + } + public ChatClientRequest function(String name, String description, java.util.function.Function function) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/ToolPlayground.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/ToolPlayground.java new file mode 100644 index 0000000000..9c7168e6e4 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/ToolPlayground.java @@ -0,0 +1,71 @@ +package org.springframework.ai.chat.client.tool; + +import org.springframework.ai.chat.client.ChatClient; + +import java.util.function.Function; + +public class ToolPlayground { + + record Request(String name) { + } + + record Response(String title) { + } + + static class ToolBean implements Function { + + @Override + public Response apply(Request request) { + return null; + } + } + + /* + * TODO: It appears that we could make this work with the existing implementation by + * changing the tool methods to return a FunctionCallWrapper. + * I think it might be worth renaming the FunctionCallbackWrapper + */ + public void playground(ChatClient client) { + + client.prompt() + .function("Name", "description", (Request request) -> new Response("")); + + /* + * This seems like a pretty good interface for using the name based API + */ + client.prompt() + .function(Tools.getByName("somefunction")); + + /* + * This seems like a pretty good interface for using the bean based API + */ + client.prompt() + .function(Tools.getByBean(ToolBean.class)); + + /* + * To get proper type inference we need to add the generics here. If we do not add + * this then we do not have type information on the input or the output To me this + * looks kinda ugly and makes the caller write less elegant code + */ + client.prompt() + .function( + Tools.getByLambda( + "somefunction", + "description", + request -> new Response("") + )); + /* + * To a limited extent it is possible to address this ugly syntax by defining an + * explicit type on the input type. And it is fair to notice that this issue also + * exists with the current implementation + */ + client.prompt() + .function( + Tools.getByLambda( + "somefunction", + "description", + (Request reqest) -> new Response("") + )); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/Tools.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/Tools.java new file mode 100644 index 0000000000..4ec3c5b4ff --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/tool/Tools.java @@ -0,0 +1,56 @@ +package org.springframework.ai.chat.client.tool; + +import org.springframework.ai.model.function.FunctionCallbackWrapper; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.Assert; + +import java.util.function.Function; + +/** + * TODO: In the current implementation the application context is required to define a tool by name and by type + * At runtime this should not be an issue, however accessing a non-static attribute from static methods has obvious issues. + * + * Proposal: + * I think it would make sense to redesign how functions / tools are handled in general. Functions created via the + * FunctionCallbackWrappers do a lot of processing when they are defined in the ChatClient. However, the functions created + * via the bean name are only handled much later (if my reading of the codebase is correct this happens during execution of the call to the LLM). + * I believe it would be better to handle all functions / tools in a more uniform way, so either process them as they are defined + * or when the LLM call is being executed. This would not only make the code more consistent but also make it easier to extend the + * mechanisms by which functions / tools can be defined. + */ +public class Tools { + + private static GenericApplicationContext context; + + public Tools(GenericApplicationContext context) { + Tools.context = context; + } + + + public static FunctionCallbackWrapper getByName(String name) { + // TODO we need to get to the application context. + // Get the bean by name and then create the tool itself... + String description = context.getBeanDefinition(name).getDescription(); + return FunctionCallbackWrapper.builder(Function.identity()) + .withName(name) + .withDescription(description) + .build(); + } + + public static FunctionCallbackWrapper getByBean(Class beanType) { + String[] namesForType = context.getBeanNamesForType(beanType); + Assert.isTrue(namesForType.length == 1, "A bean must have a unique definiton"); + String name = namesForType[0]; + String description = context.getBeanDefinition(name).getDescription(); + return FunctionCallbackWrapper.builder(Function.identity()) + .withName(name) + .withDescription(description) + .build(); + } + + public static FunctionCallbackWrapper getByLambda(String name, String description, + Function func) { + return FunctionCallbackWrapper.builder(func).withDescription("description").withName(name).build(); + } + +}