Add Spring Annotation for registerding Function Calling tools.
- Built on top of the existing ToolFunctionCallback utilites. - Organized as part of auto-configuration project under the /common/function package to be reusable for different model implementations. - Add OpenAI support for the SpringAiFunction annotation. - Update the OpenAI function calling documentation. - Add ITs Co-Authored-By: Christian Tzolov <ctzolov@vmware.com>
This commit is contained in:
committed by
Christian Tzolov
parent
57c66b91d7
commit
b2a4f01761
@@ -21,11 +21,11 @@ import java.util.Map;
|
||||
public class FunctionMessage extends AbstractMessage {
|
||||
|
||||
public FunctionMessage(String content) {
|
||||
super(MessageType.FUNCTION, content);
|
||||
super(MessageType.SYSTEM, content);
|
||||
}
|
||||
|
||||
public FunctionMessage(String content, Map<String, Object> properties) {
|
||||
super(MessageType.FUNCTION, content, properties);
|
||||
super(MessageType.SYSTEM, content, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -64,16 +64,16 @@ The Spring AI auto-generates the JSON Scheme for the `MockWeatherService.Request
|
||||
|
||||
If you enable the link:../openai-chat.html#_auto_configuration[OpenAiChatClient Auto-Configuration], the easiest way to register a function is to created it as a bean in the Spring context:
|
||||
|
||||
[source,java,linenums]
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Bean
|
||||
public WeatherFunctionCallback weatherFunctionInfo() {
|
||||
return new WeatherFunctionCallback(
|
||||
"CurrentWeather", // (1) name
|
||||
"Get the weather in location", // (2) description
|
||||
MockWeatherService.Request.class); // (3) signature
|
||||
"CurrentWeather", // (1) function name
|
||||
"Get the weather in location", // (2) function description
|
||||
MockWeatherService.Request.class); // (3) function input signature
|
||||
}
|
||||
...
|
||||
}
|
||||
@@ -106,6 +106,35 @@ Here is the current weather for the requested cities:
|
||||
|
||||
The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/ToolCallWithBeanFunctionRegistrationIT.java[ToolCallWithBeanFunctionRegistrationIT.java] integration test provides a complete example of how to register a function with the `OpenAiChatClient` using the auto-configuration.
|
||||
|
||||
==== @SpringAiFunction
|
||||
|
||||
You can use the `SpringAiFunction` annotation cam be used to register a `java.util.Function<I,O>` as a `ToolFunctionCallback` bean:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
static class Config {
|
||||
|
||||
@SpringAiFunction(
|
||||
name = "CurrentWeather", // (1)
|
||||
description = "Get the weather in location", // (2)
|
||||
classType = MockWeatherService.Request.class) // (3)
|
||||
public Function<Request, Response> weatherFunction() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
The `@SpringAiFunction` annotation defines the function name (1), description (2), and input signature (3) and registers the function as a bean in the Spring context.
|
||||
|
||||
NOTE: The `SpringAiFunction` annotation supported only if the auto-configuration is enabled.
|
||||
|
||||
NOTE: The Function<I, O> implementation is responsible to convert the response into a text as expected by the model.
|
||||
By default, the `AbstractToolFunctionCallback` provides a default converter that returns the `toString()` of the response object.
|
||||
|
||||
=== Register/Call Functions with Prompt Options
|
||||
|
||||
In addition to the auto-configuration you can register callback functions, dynamically, with your Prompt requests:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.autoconfigure.common.function;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* An annotation used to define functions for use in
|
||||
*
|
||||
* @author Christopher Smith
|
||||
*/
|
||||
@Bean
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface SpringAiFunction {
|
||||
|
||||
String name();
|
||||
|
||||
String description();
|
||||
|
||||
Class<?> classType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.autoconfigure.common.function;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.ai.model.AbstractToolFunctionCallback;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Manages the chat functions that are annotated with {@link SpringAiFunction}.
|
||||
*
|
||||
* @author Christopher Smith
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
public class SpringAiFunctionAnnotationManager implements ApplicationContextAware {
|
||||
|
||||
private GenericApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = (GenericApplicationContext) applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of all the {@link java.util.Function}s annotated with
|
||||
* {@link SpringAiFunction}.
|
||||
*/
|
||||
public List<ToolFunctionCallback> getAnnotatedToolFunctionCallbacks() {
|
||||
Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(SpringAiFunction.class);
|
||||
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks = new ArrayList<>();
|
||||
|
||||
if (!CollectionUtils.isEmpty(beans)) {
|
||||
|
||||
beans.forEach((k, v) -> {
|
||||
if (v instanceof Function<?, ?> function) {
|
||||
SpringAiFunction functionAnnotation = applicationContext.findAnnotationOnBean(k,
|
||||
SpringAiFunction.class);
|
||||
|
||||
toolFunctionCallbacks.add(new SpringAiFunctionToolFunctionCallback(functionAnnotation.name(),
|
||||
functionAnnotation.description(), functionAnnotation.classType(), function));
|
||||
}
|
||||
else {
|
||||
ReflectionUtils.handleReflectionException(new IllegalArgumentException(
|
||||
"Bean annotated with @SpringAiFunction must be of type Function"));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return toolFunctionCallbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that the underlying function is responsible for converting the output into
|
||||
* format that can be consumed by the Model. The default implementation converts the
|
||||
* output into String before sending it to the Model. Provide a custom Function<O,
|
||||
* String> responseConverter implementation to override this.
|
||||
*
|
||||
*/
|
||||
public static class SpringAiFunctionToolFunctionCallback<I, O> extends AbstractToolFunctionCallback<I, O> {
|
||||
|
||||
private Function<I, O> function;
|
||||
|
||||
protected SpringAiFunctionToolFunctionCallback(String name, String description, Class<I> inputType,
|
||||
Function<I, O> function) {
|
||||
super(name, description, inputType);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
protected SpringAiFunctionToolFunctionCallback(String name, String description, Class<I> inputType,
|
||||
Function<O, String> responseConverter, Function<I, O> function) {
|
||||
super(name, description, inputType, responseConverter);
|
||||
Assert.notNull(function, "Function must not be null");
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public O apply(I input) {
|
||||
return this.function.apply(input);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package org.springframework.ai.autoconfigure.openai;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.ai.autoconfigure.NativeHints;
|
||||
import org.springframework.ai.autoconfigure.common.function.SpringAiFunctionAnnotationManager;
|
||||
import org.springframework.ai.embedding.EmbeddingClient;
|
||||
import org.springframework.ai.model.ToolFunctionCallback;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
@@ -31,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -38,7 +40,7 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
@AutoConfiguration(after = RestClientAutoConfiguration.class)
|
||||
@AutoConfiguration(after = { RestClientAutoConfiguration.class })
|
||||
@ConditionalOnClass(OpenAiApi.class)
|
||||
@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class,
|
||||
OpenAiEmbeddingProperties.class, OpenAiImageProperties.class })
|
||||
@@ -56,7 +58,7 @@ public class OpenAiAutoConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
public OpenAiChatClient openAiChatClient(OpenAiConnectionProperties commonProperties,
|
||||
OpenAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks) {
|
||||
List<ToolFunctionCallback> toolFunctionCallbacks, SpringAiFunctionAnnotationManager functionManager) {
|
||||
|
||||
String apiKey = StringUtils.hasText(chatProperties.getApiKey()) ? chatProperties.getApiKey()
|
||||
: commonProperties.getApiKey();
|
||||
@@ -73,6 +75,11 @@ public class OpenAiAutoConfiguration {
|
||||
chatProperties.getOptions().getToolCallbacks().addAll(toolFunctionCallbacks);
|
||||
}
|
||||
|
||||
var annotatedFunctionsList = functionManager.getAnnotatedToolFunctionCallbacks();
|
||||
if (!CollectionUtils.isEmpty(annotatedFunctionsList)) {
|
||||
chatProperties.getOptions().getToolCallbacks().addAll(annotatedFunctionsList);
|
||||
}
|
||||
|
||||
return new OpenAiChatClient(openAiApi, chatProperties.getOptions());
|
||||
}
|
||||
|
||||
@@ -113,4 +120,12 @@ public class OpenAiAutoConfiguration {
|
||||
return new OpenAiImageClient(openAiImageApi).withDefaultOptions(imageProperties.getOptions());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SpringAiFunctionAnnotationManager springAiFunctionManager(ApplicationContext context) {
|
||||
SpringAiFunctionAnnotationManager manager = new SpringAiFunctionAnnotationManager();
|
||||
manager.setApplicationContext(context);
|
||||
return manager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2024-2024 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.autoconfigure.openai.tool;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.ai.autoconfigure.common.function.SpringAiFunction;
|
||||
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
|
||||
import org.springframework.ai.chat.ChatResponse;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatClient;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
|
||||
class ToolCallWithSpringAIFunctionAnnotationIT {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ToolCallWithBeanFunctionRegistrationIT.class);
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
|
||||
.withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class))
|
||||
.withUserConfiguration(Config.class);
|
||||
|
||||
@Test
|
||||
void functionCallTest() {
|
||||
contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=gpt-4-1106-preview").run(context -> {
|
||||
|
||||
OpenAiChatClient chatClient = context.getBean(OpenAiChatClient.class);
|
||||
|
||||
UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
|
||||
|
||||
ChatResponse response = chatClient.call(new Prompt(List.of(userMessage),
|
||||
OpenAiChatOptions.builder().withEnabledFunction("WeatherInfo").build()));
|
||||
|
||||
logger.info("Response: {}", response);
|
||||
|
||||
assertThat(response.getResult().getOutput().getContent()).contains("30", "10", "15");
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
@SpringAiFunction(name = "WeatherInfo", description = "Get the weather in location",
|
||||
classType = MockWeatherService.Request.class)
|
||||
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction() {
|
||||
MockWeatherService weatherService = new MockWeatherService();
|
||||
return (weatherService::apply);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user