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:
Chris Smith
2023-09-06 17:05:50 -04:00
committed by Christian Tzolov
parent 57c66b91d7
commit b2a4f01761
6 changed files with 285 additions and 8 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}