* implemented user specification and system specification

* managed to go end-to-end with a simple use of the new fluent DSL: build up the pipeline, all the way to a ChatResponse.
This commit is contained in:
Josh Long
2024-05-18 14:11:12 +02:00
parent 479949688b
commit 8bd1dac496
40 changed files with 374 additions and 333 deletions

View File

@@ -117,7 +117,7 @@ public class AnthropicChatConnector extends
* @param retryTemplate the retry template used to retry the Anthropic API calls.
*/
public AnthropicChatConnector(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate) {
RetryTemplate retryTemplate) {
this(anthropicApi, defaultOptions, retryTemplate, null);
}
@@ -130,7 +130,7 @@ public class AnthropicChatConnector extends
* state of the function calls.
*/
public AnthropicChatConnector(AnthropicApi anthropicApi, AnthropicChatOptions defaultOptions,
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext) {
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext) {
super(functionCallbackContext);

View File

@@ -107,7 +107,7 @@ public class AzureOpenAiChatConnector
}
public AzureOpenAiChatConnector(OpenAIClient microsoftOpenAiClient, AzureOpenAiChatOptions options,
FunctionCallbackContext functionCallbackContext) {
FunctionCallbackContext functionCallbackContext) {
super(functionCallbackContext);
Assert.notNull(microsoftOpenAiClient, "com.azure.ai.openai.OpenAIClient must not be null");
Assert.notNull(options, "AzureOpenAiChatOptions must not be null");

View File

@@ -59,7 +59,7 @@ public class MockAzureOpenAiTestConfiguration {
}
@Bean
AzureOpenAiChatConnector azureOpenAiChatClient(OpenAIClient microsoftAzureOpenAiClient) {
AzureOpenAiChatConnector azureOpenAiChatClient(OpenAIClient microsoftAzureOpenAiClient) {
return new AzureOpenAiChatConnector(microsoftAzureOpenAiClient);
}

View File

@@ -41,7 +41,7 @@ public class BedrockAi21Jurassic2ChatConnector implements ChatConnector {
private final BedrockAi21Jurassic2ChatOptions defaultOptions;
public BedrockAi21Jurassic2ChatConnector(Ai21Jurassic2ChatBedrockApi chatApi,
BedrockAi21Jurassic2ChatOptions options) {
BedrockAi21Jurassic2ChatOptions options) {
Assert.notNull(chatApi, "Ai21Jurassic2ChatBedrockApi must not be null");
Assert.notNull(options, "BedrockAi21Jurassic2ChatOptions must not be null");

View File

@@ -83,7 +83,7 @@ public class MistralAiChatConnector extends
}
public MistralAiChatConnector(MistralAiApi mistralAiApi, MistralAiChatOptions options,
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
super(functionCallbackContext);
Assert.notNull(mistralAiApi, "MistralAiApi must not be null");
Assert.notNull(options, "Options must not be null");

View File

@@ -123,7 +123,7 @@ public class OpenAiChatConnector extends
* @param retryTemplate The retry template.
*/
public OpenAiChatConnector(OpenAiApi openAiApi, OpenAiChatOptions options,
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
super(functionCallbackContext);
Assert.notNull(openAiApi, "OpenAiApi must not be null");
Assert.notNull(options, "Options must not be null");

View File

@@ -1,56 +0,0 @@
package org.springframework.ai.openai;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
class ChatConnectorTest {
@Configuration
static class ChatClientTestConfiguration {
@Bean
ChatClient client(OpenAiChatConnector openAiChatConnector) {
return ChatClient.builder(openAiChatConnector).defaultSystem("""
you are customer service agent designed to answer questions
about a the user, {userName}'s, orders. Here are their outstanding orders.
{orders}
""").defaultFunctions("cancelOrder", "refundOrder").build();
}
}
private final ChatClient singularity;
ChatConnectorTest(@Autowired ChatClient singularity) {
this.singularity = singularity;
}
@Test
void products() throws Exception {
var product0 = this.client.userPrompt("tell me about this product from the merchant {merchant}")
.userPromptParams(Map.of("merchant", "24u92"))
.execute(Product.class);
/*
* var product1 = this.client .build() .userPromptParam("a", "b")
* .functions("cancelOrder", "refundOrder") .execute(new
* ParameterizedTypeReference<Product>() { });
*
* var product2 = this.client
* .userPrompt("tell me about this product from the merchant {merchant}",
* Map.of("merchant", "232")) .execute(Product.class);
*/
}
record Product(String sku) {
}
}

View File

@@ -99,7 +99,7 @@ public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
@Bean
public ChatService memoryChatService(OpenAiChatConnector chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
@@ -111,7 +111,7 @@ public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatConnector streamingChatClient,
VectorStore vectorStore, TokenCountEstimator tokenCountEstimator) {
VectorStore vectorStore, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))

View File

@@ -75,7 +75,7 @@ public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
@Bean
public ChatService memoryChatService(OpenAiChatConnector chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
@@ -87,7 +87,7 @@ public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatConnector streamingChatClient,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))

View File

@@ -76,7 +76,7 @@ public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
@Bean
public ChatService memoryChatService(OpenAiChatConnector chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
@@ -88,7 +88,7 @@ public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatConnector streamingChatClient,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))

View File

@@ -187,7 +187,7 @@ public class LongShortTermChatMemoryWithRagIT {
@Bean
public ChatService memoryChatService(OpenAiChatConnector chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator, ChatMemory chatHistory) {
TokenCountEstimator tokenCountEstimator, ChatMemory chatHistory) {
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults()),

View File

@@ -82,7 +82,7 @@ public class OpenAiPromptTransformingChatServiceIT {
@Autowired
public OpenAiPromptTransformingChatServiceIT(ChatConnector chatConnector, ChatService chatService,
VectorStore vectorStore) {
VectorStore vectorStore) {
this.chatConnector = chatConnector;
this.chatService = chatService;
this.vectorStore = vectorStore;

View File

@@ -130,7 +130,7 @@ public class VertexAiGeminiChatConnector
}
public VertexAiGeminiChatConnector(VertexAI vertexAI, VertexAiGeminiChatOptions options,
FunctionCallbackContext functionCallbackContext) {
FunctionCallbackContext functionCallbackContext) {
super(functionCallbackContext);

View File

@@ -3,14 +3,23 @@ package org.springframework.ai.chat;
import org.springframework.ai.chat.connector.ChatConnector;
import org.springframework.ai.chat.messages.Media;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallbackWrapper;
import org.springframework.ai.model.function.FunctionCallingOptionsBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.*;
import java.util.function.Consumer;
@@ -93,229 +102,312 @@ public class DemoApplication {
*/
public interface ChatClient {
static ChatClientBuilder builder(ChatConnector connector) {
return new ChatClientBuilder(connector);
}
ChatClientRequest build();
static ChatClientBuilder builder(ChatConnector connector) {
return new ChatClientBuilder(connector);
}
ChatResponse call(Prompt prompt);
ChatResponse call(Prompt prompt);
ChatClientRequest user(Consumer<UserSpec> consumer);
ChatClientRequest call();
public static class UserSpec {
interface PromptSpec<T> {
T text(String text);
public UserSpec media(List<Media> media) {
return this;
}
T text(Resource text, Charset charset);
public UserSpec media(URL url, MimeType mimeType) {
return this;
}
T text(Resource text);
public UserSpec media(Resource resource, MimeType type) {
return this;
}
T params(Map<String, Object> p);
public UserSpec media(Media... m) {
return this;
}
T param(String k, String v);
public UserSpec params(Map<String, Object> p) {
return this;
}
}
public UserSpec param(String k, String v) {
return this;
}
}
abstract class AbstractPromptSpec<T extends AbstractPromptSpec<T>> implements PromptSpec<T> {
public static class ChatClientRequest {
private String text = "";
private String userPrompt = "";
private final Map<String, Object> params = new HashMap<>();
private String systemPrompt = "";
@Override
public T text(String text) {
this.text = (text);
return self();
}
private final List<Media> media = new ArrayList<>();
@Override
public T text(Resource text, Charset charset) {
try {
this.text(text.getContentAsString(charset));
}
catch (IOException e) {
throw new RuntimeException(e);
}
return self();
}
private final List<String> functions = new ArrayList<>();
@Override
public T text(Resource text) {
this.text(text, Charset.defaultCharset());
return self();
}
private final Map<String, String> userPromptParams = new HashMap<>();
@Override
public T param(String k, String v) {
this.params.put(k, v);
return self();
}
private final Map<String, String> systemPromptParams = new HashMap<>();
@Override
public T params(Map<String, Object> p) {
this.params.putAll(p);
return self();
}
List<Media> userMedia() {
return this.media;
}
protected abstract T self();
String systemText() {
return this.systemPrompt;
}
protected String text() {
return this.text;
}
String userText() {
return this.userPrompt;
}
protected Map<String, Object> params() {
return this.params;
}
List<String> functions() {
return this.functions;
}
}
public ChatClientRequest(String userPrompt, String systemPrompt, List<String> functions, List<Media> media) {
this.userPrompt = userPrompt;
this.systemPrompt = systemPrompt;
this.functions.addAll(functions);
this.media.addAll(media);
}
class UserSpec extends AbstractPromptSpec<UserSpec> implements PromptSpec<UserSpec> {
public ChatClientRequest messages(Message... messages) {
return null;
}
//
// public ChatClientRequest userParam(String key, String value) {
// this.userPromptParams.put(key, value);
// return this;
// }
//
// public ChatClientRequest systemParam(String key, String value) {
// this.systemPromptParams.put(key, value);
// return this;
// }
private final List<Media> media = new ArrayList<>();
public <T extends ChatOptions> ChatClientRequest options(T options) {
return this;
}
//
// public ChatClientRequest systemParams(Map<String, String> systemPromptParams) {
// this.systemPromptParams.putAll(systemPromptParams);
// return this;
// }
//
// public ChatClientRequest userParams(Map<String, String> userPromptParams) {
// this.userPromptParams.putAll(userPromptParams);
// return this;
// }
//
// public ChatClientRequest userText(Resource resource) {
// return userText(resource, Charset.defaultCharset());
// }
public UserSpec media(Media... media) {
this.media.addAll(Arrays.asList(media));
return self();
}
// public ChatClientRequest userText(Resource resource, Charset charset) {
// try {
// this.userText(resource.getContentAsString(charset));
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// return this;
// }
//
//
// public ChatClientRequest userText(String userPrompt) {
// this.userPrompt = userPrompt;
// return this;
// }
//
// public ChatClientRequest systemText(Resource systemPrompt) {
// return systemText(systemPrompt, Charset.defaultCharset());
// }
//
// public ChatClientRequest systemText(Resource systemPrompt, Charset charset) {
// try {
// this.systemText(systemPrompt.getContentAsString(charset));
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// return this;
// }
//
// public ChatClientRequest systemText(String systemPrompt) {
// this.systemPrompt = systemPrompt;
// return this;
// }
//
// public ChatClientRequest userMedia(Media... media) {
// this.media.addAll(Arrays.asList(media));
// return this;
// }
public UserSpec media(MimeType mimeType, URL url) {
this.media.add(new Media(mimeType, url));
return self();
}
public ChatClientRequest functions(String... functions) {
this.functions.addAll(Arrays.asList(functions));
return this;
}
public UserSpec media(MimeType mimeType, Resource resource) {
this.media.add(new Media(mimeType, resource));
return self();
}
protected List<Media> media() {
return this.media;
}
public static class ChatResponseSpec {
@Override
protected UserSpec self() {
return this;
}
public <T> T single(ParameterizedTypeReference<T> t) {
return null;
}
}
class SystemSpec extends AbstractPromptSpec<SystemSpec> implements PromptSpec<SystemSpec> {
public <T> T single(Class<T> clzz) {
return null;
}
@Override
protected SystemSpec self() {
return this;
}
public ChatResponse chatResponse() {
return null;
}
}
public <T> Flux<T> stream(Class<T> t) {
return null;
}
class ChatClientRequest {
public <T> Flux<T> stream(ParameterizedTypeReference<T> t) {
return Flux.empty();
}
private final ChatConnector connector;
public <T> Collection<T> list(Class<T> clzz) {
return null;
}
private String userText = "";
public <T> Collection<T> list(ParameterizedTypeReference<Collection<T>> ptr) {
return List.of();
}
private String systemText = "";
}
private ChatOptions chatOptions;
public ChatResponseSpec chat() {
return null;
}
private final List<Media> media = new ArrayList<>();
private final Set<String> functionNames = new HashSet<>();
}
private final List<FunctionCallback> functionCallbacks = new ArrayList<>();
public static class ChatClientBuilder {
private final Map<String, Object> userParams = new HashMap<>();
private final ChatConnector connector;
private final List<Message> messages = new ArrayList<>();
private final List<Media> defaultMedia = new ArrayList<>();
private final Map<String, Object> systemParams = new HashMap<>();
private final List<String> defaultFunctions = new ArrayList<>();
public ChatClientRequest(ChatConnector connector, String userText, String systemText,
List<String> functionNames, List<Media> media, ChatOptions chatOptions) {
this.userText = userText;
this.systemText = systemText;
this.connector = connector;
this.functionNames.addAll(functionNames);
this.media.addAll(media);
this.chatOptions = chatOptions;
}
private String defaultSystemPrompt;
public ChatClientRequest messages(Message... messages) {
this.messages.addAll(List.of(messages));
return this;
}
private String defaultUserPrompt;
public <T extends ChatOptions> ChatClientRequest options(T options) {
this.chatOptions = options;
return this;
}
ChatClientBuilder(ChatConnector connector) {
this.connector = connector;
}
public <I, O> ChatClientRequest function(String name, String description,
java.util.function.Function<I, O> function) {
var fcw = FunctionCallbackWrapper.builder(function)
.withDescription(description)
.withName(name)
.withResponseConverter(Object::toString)
.build();
this.functionCallbacks.add(fcw);
return this;
}
public ChatClient build() {
return new DefaultChatClient(this.connector, this.defaultSystemPrompt, this.defaultUserPrompt,
this.defaultFunctions, this.defaultMedia);
}
public ChatClientRequest functions(String... functions) {
this.functionNames.addAll(List.of(functions));
return this;
}
public ChatClientBuilder defaultSystem(String systemPrompt) {
return this;
}
public ChatClientRequest system(Consumer<SystemSpec> consumer) {
var ss = new SystemSpec();
consumer.accept(ss);
this.systemText = ss.text();
this.systemParams.putAll(ss.params());
return this;
}
public ChatClientBuilder defaultFunctions(String... functionNames) {
return this;
}
public ChatClientRequest user(Consumer<UserSpec> consumer) {
var us = new UserSpec();
consumer.accept(us);
this.userText = us.text();
this.userParams.putAll(us.params());
this.media.addAll(us.media());
return this;
}
public ChatClientBuilder defaultUserPrompt(String userPrompt) {
return this;
}
public static class ChatResponseSpec {
private final ChatClientRequest request;
private final ChatConnector chatConnector;
public ChatResponseSpec(ChatConnector chatConnector, ChatClientRequest request) {
this.chatConnector = chatConnector;
this.request = request;
}
public <T> T single(ParameterizedTypeReference<T> t) {
return null;
}
public <T> T single(Class<T> clzz) {
return null;
}
public ChatResponse chatResponse() {
var userMessage = new UserMessage(
new PromptTemplate(this.request.userText, this.request.userParams).render(),
this.request.media);
var systemMessage = new SystemMessage(
new PromptTemplate(this.request.systemText, this.request.systemParams).render());
if (request.chatOptions instanceof FunctionCallingOptionsBuilder.PortableFunctionCallingOptions functionCallingOptions) {
if (!request.functionNames.isEmpty()) {
functionCallingOptions.setFunctions(request.functionNames);
}
if (!request.functionCallbacks.isEmpty()) {
functionCallingOptions.setFunctionCallbacks(request.functionCallbacks);
}
}
var prompt = new Prompt(List.of(systemMessage, userMessage), request.chatOptions);
return this.chatConnector.call(prompt);
}
public <T> Flux<T> stream(Class<T> t) {
return null;
}
public <T> Flux<T> stream(ParameterizedTypeReference<T> t) {
return Flux.empty();
}
public <T> Collection<T> list(Class<T> clzz) {
return null;
}
public <T> Collection<T> list(ParameterizedTypeReference<Collection<T>> ptr) {
return List.of();
}
}
public ChatResponseSpec chat() {
return new ChatResponseSpec(this.connector, this);
}
}
class ChatClientBuilder {
private final ChatConnector connector;
private final List<Media> defaultMedia = new ArrayList<>();
private final List<String> defaultFunctions = new ArrayList<>();
private String defaultSystem;
private String defaultUser;
ChatClientBuilder(ChatConnector connector) {
Assert.notNull(connector, "the " + ChatConnector.class.getName() + " must be non-null!");
this.connector = connector;
}
public ChatClient build() {
return new DefaultChatClient(this.connector, this.defaultSystem, this.defaultUser, this.defaultFunctions,
this.defaultMedia);
}
public ChatClientBuilder defaultSystem(String systemPrompt) {
this.defaultSystem = systemPrompt;
return this;
}
public ChatClientBuilder defaultFunctions(String... functionNames) {
this.defaultFunctions.addAll(List.of(functionNames));
return this;
}
public ChatClientBuilder defaultUser(String userPrompt) {
this.defaultUser = userPrompt;
return this;
}
}
@Deprecated(since = "1.0.0 M1", forRemoval = true)
default String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getContent() : "";
}
@Deprecated(since = "1.0.0 M1", forRemoval = true)
default String call(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getContent() : "";
}
}
}

View File

@@ -5,52 +5,48 @@ import org.springframework.ai.chat.messages.Media;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.List;
import java.util.function.Consumer;
/**
* todo follow WebClient -> DefaultWebClient
* todo make sure ChatConnector also supports call(Prompt) and then mark as deprecated
*
* @author Mark Pollack
* @author Christian Tzolov
* @author Josh Long
* @author Arjen Poutsma
*/
public class DefaultChatClient implements ChatClient {
class DefaultChatClient implements ChatClient {
private final ChatConnector connector;
private final String userPrompt, systemPrompt;
private final String userText, systemText;
private final List<String> functions;
private final List<String> functionNames;
private final List<Media> media;
public DefaultChatClient(ChatConnector connector, String defaultSystemPrompt, String defaultUserPrompt,
List<String> defaultFunctions, List<Media> defaultMedia) {
List<String> defaultFunctions, List<Media> defaultMedia) {
this.connector = connector;
this.userPrompt = defaultUserPrompt;
this.systemPrompt = defaultSystemPrompt;
this.functions = defaultFunctions;
this.userText = defaultUserPrompt;
this.systemText = defaultSystemPrompt;
this.functionNames = defaultFunctions;
this.media = defaultMedia;
}
@Override
public ChatClientRequest build() {
return new ChatClientRequest(this.userPrompt, this.systemPrompt, this.functions, this.media);
public ChatClientRequest call() {
return new ChatClientRequest(this.connector, this.userText, this.systemText, this.functionNames, this.media,
null);
}
/**
* use the new fluid DSL starting in {@link #call()}
* @param prompt the {@link Prompt prompt} object
* @return a {@link ChatResponse chat response}
*/
@Deprecated(forRemoval = true, since = "1.0.0 M1")
@Override
public ChatResponse call(Prompt prompt) {
return null;
}
@Override
public ChatClientRequest user(Consumer<UserSpec> consumer) {
return null;
return this.connector.call(prompt);
}
}

View File

@@ -16,19 +16,14 @@
package org.springframework.ai.chat.connector;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
public interface ChatConnector {
import java.util.Arrays;
/*
* default String call(String message) { Prompt prompt = new Prompt(new
* UserMessage(message)); Generation generation = call(prompt).getResult(); return
* (generation != null) ? generation.getOutput().getContent() : ""; }
*
* public String call(Message... messages) { Prompt prompt = new
* Prompt(Arrays.asList(messages)); Generation generation = call(prompt).getResult();
* return (generation != null) ? generation.getOutput().getContent() : ""; }
*/
public interface ChatConnector {
ChatResponse call(Prompt prompt);

View File

@@ -66,9 +66,9 @@ public abstract class AbstractMessage implements Message {
protected AbstractMessage(MessageType messageType, String textContent, Collection<Media> media,
Map<String, Object> metadata) {
Assert.notNull(messageType, "Message type must not be null");
Assert.notNull(messageType, "MessageType must not be null");
Assert.notNull(textContent, "Content must not be null");
Assert.notNull(media, "media data must not be null");
Assert.notNull(media, "Media must not be null");
this.messageType = messageType;
this.textContent = textContent;

View File

@@ -46,8 +46,8 @@ public class PromptTransformingChatService implements ChatService {
private List<ChatServiceListener> chatServiceListeners;
public PromptTransformingChatService(ChatConnector chatConnector, List<PromptTransformer> retrievers,
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatServiceListener> chatServiceListeners) {
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatServiceListener> chatServiceListeners) {
Objects.requireNonNull(chatConnector, "chatConnector must not be null");
this.chatConnector = chatConnector;
this.retrievers = retrievers;

View File

@@ -26,18 +26,18 @@ public interface FunctionCallback {
/**
* @return Returns the Function name. Unique within the model.
*/
public String getName();
String getName();
/**
* @return Returns the function description. This description is used by the model do
* decide if the function should be called or not.
*/
public String getDescription();
String getDescription();
/**
* @return Returns the JSON schema of the function input type.
*/
public String getInputTypeSchema();
String getInputTypeSchema();
/**
* Called when a model detects and triggers a function call. The model is responsible
@@ -47,6 +47,6 @@ public interface FunctionCallback {
* model.
* @return String containing the function call response.
*/
public String call(String functionInput);
String call(String functionInput);
}

View File

@@ -15,11 +15,7 @@
*/
package org.springframework.ai.model.function;
import java.lang.reflect.Type;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType;
import org.springframework.beans.BeansException;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
@@ -32,6 +28,9 @@ import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import java.lang.reflect.Type;
import java.util.function.Function;
/**
* A Spring {@link ApplicationContextAware} implementation that provides a way to retrieve
* a {@link Function} from the Spring context and wrap it into a {@link FunctionCallback}.
@@ -47,6 +46,7 @@ import org.springframework.util.StringUtils;
*
* @author Christian Tzolov
* @author Christopher Smith
* @author Josh Long
*/
public class FunctionCallbackContext implements ApplicationContextAware {
@@ -63,6 +63,19 @@ public class FunctionCallbackContext implements ApplicationContextAware {
this.applicationContext = (GenericApplicationContext) applicationContext;
}
public <I, O> FunctionCallback getFunctionCallback(String beanName, String defaultDescription,
Function<I, O> function, SchemaType schemaType) {
var beanType = FunctionTypeUtils.discoverFunctionTypeFromClass(function.getClass());
var functionInputType = TypeResolverHelper.getFunctionArgumentType(beanType, 0);
var functionInputClass = FunctionTypeUtils.getRawType(functionInputType);
return FunctionCallbackWrapper.builder(function)
.withName(beanName)
.withSchemaType(schemaType)
.withDescription(defaultDescription)
.withInputType(functionInputClass)
.build();
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public FunctionCallback getFunctionCallback(@NonNull String beanName, @Nullable String defaultDescription) {

View File

@@ -81,7 +81,7 @@ public class SummaryMetadataEnricher implements DocumentTransformer {
}
public SummaryMetadataEnricher(ChatConnector chatConnector, List<SummaryType> summaryTypes, String summaryTemplate,
MetadataMode metadataMode) {
MetadataMode metadataMode) {
Assert.notNull(chatConnector, "ChatConnector must not be null");
Assert.hasText(summaryTemplate, "Summary template must not be empty");

View File

@@ -48,7 +48,7 @@ class ChatConnectorTests {
String userMessage = "Zero Wing";
String responseMessage = "All your bases are belong to us";
ChatConnector mockClient = Mockito.mock(ChatConnector.class);
ChatClient mockClient = Mockito.mock(ChatClient.class);
AssistantMessage mockAssistantMessage = Mockito.mock(AssistantMessage.class);
when(mockAssistantMessage.getContent()).thenReturn(responseMessage);

View File

@@ -48,7 +48,7 @@ import static org.mockito.Mockito.when;
public class ChatMemoryTests {
@Mock
ChatConnector chatConnector;
ChatConnector chatConnector;
@Mock
StreamingChatClient streamingChatClient;

View File

@@ -58,8 +58,8 @@ public class AnthropicAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public AnthropicChatConnector anthropicChatClient(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext,
List<FunctionCallback> toolFunctionCallbacks) {
RetryTemplate retryTemplate, FunctionCallbackContext functionCallbackContext,
List<FunctionCallback> toolFunctionCallbacks) {
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks);

View File

@@ -59,8 +59,8 @@ public class AzureOpenAiAutoConfiguration {
@ConditionalOnProperty(prefix = AzureOpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public AzureOpenAiChatConnector azureOpenAiChatClient(OpenAIClient openAIClient,
AzureOpenAiChatProperties chatProperties, List<FunctionCallback> toolFunctionCallbacks,
FunctionCallbackContext functionCallbackContext) {
AzureOpenAiChatProperties chatProperties, List<FunctionCallback> toolFunctionCallbacks,
FunctionCallbackContext functionCallbackContext) {
if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) {
chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks);

View File

@@ -60,7 +60,7 @@ public class BedrockAnthropicChatAutoConfiguration {
@Bean
@ConditionalOnBean(AnthropicChatBedrockApi.class)
public BedrockAnthropicChatConnector anthropicChatClient(AnthropicChatBedrockApi anthropicApi,
BedrockAnthropicChatProperties properties) {
BedrockAnthropicChatProperties properties) {
return new BedrockAnthropicChatConnector(anthropicApi, properties.getOptions());
}

View File

@@ -60,7 +60,7 @@ public class BedrockAnthropic3ChatAutoConfiguration {
@Bean
@ConditionalOnBean(Anthropic3ChatBedrockApi.class)
public BedrockAnthropic3ChatConnector anthropic3ChatClient(Anthropic3ChatBedrockApi anthropicApi,
BedrockAnthropic3ChatProperties properties) {
BedrockAnthropic3ChatProperties properties) {
return new BedrockAnthropic3ChatConnector(anthropicApi, properties.getOptions());
}

View File

@@ -58,7 +58,7 @@ public class BedrockCohereChatAutoConfiguration {
@Bean
@ConditionalOnBean(CohereChatBedrockApi.class)
public BedrockCohereChatConnector cohereChatClient(CohereChatBedrockApi cohereChatApi,
BedrockCohereChatProperties properties) {
BedrockCohereChatProperties properties) {
return new BedrockCohereChatConnector(cohereChatApi, properties.getOptions());
}

View File

@@ -60,7 +60,7 @@ public class BedrockLlamaChatAutoConfiguration {
@Bean
@ConditionalOnBean(LlamaChatBedrockApi.class)
public BedrockLlamaChatConnector llamaChatClient(LlamaChatBedrockApi llamaApi,
BedrockLlamaChatProperties properties) {
BedrockLlamaChatProperties properties) {
return new BedrockLlamaChatConnector(llamaApi, properties.getOptions());
}

View File

@@ -58,7 +58,7 @@ public class BedrockTitanChatAutoConfiguration {
@Bean
@ConditionalOnBean(TitanChatBedrockApi.class)
public BedrockTitanChatConnector titanChatClient(TitanChatBedrockApi titanChatApi,
BedrockTitanChatProperties properties) {
BedrockTitanChatProperties properties) {
return new BedrockTitanChatConnector(titanChatApi, properties.getOptions());
}

View File

@@ -70,9 +70,9 @@ public class MistralAiAutoConfiguration {
@ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public MistralAiChatConnector mistralAiChatClient(MistralAiCommonProperties commonProperties,
MistralAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
MistralAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), restClientBuilder, responseErrorHandler);

View File

@@ -55,9 +55,9 @@ public class OpenAiAutoConfiguration {
@ConditionalOnProperty(prefix = OpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public OpenAiChatConnector openAiChatClient(OpenAiConnectionProperties commonProperties,
OpenAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
OpenAiChatProperties chatProperties, RestClient.Builder restClientBuilder,
List<FunctionCallback> toolFunctionCallbacks, FunctionCallbackContext functionCallbackContext,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) {
var openAiApi = openAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler);

View File

@@ -75,8 +75,8 @@ public class VertexAiGeminiAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public VertexAiGeminiChatConnector vertexAiGeminiChat(VertexAI vertexAi,
VertexAiGeminiChatProperties chatProperties, List<FunctionCallback> toolFunctionCallbacks,
ApplicationContext context) {
VertexAiGeminiChatProperties chatProperties, List<FunctionCallback> toolFunctionCallbacks,
ApplicationContext context) {
FunctionCallbackContext functionCallbackContext = springAiFunctionManager(context);

View File

@@ -48,7 +48,7 @@ public class VertexAiPalm2AutoConfiguration {
@ConditionalOnProperty(prefix = VertexAiPlam2ChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
matchIfMissing = true)
public VertexAiPaLm2ChatConnector vertexAiChatClient(VertexAiPaLm2Api vertexAiApi,
VertexAiPlam2ChatProperties chatProperties) {
VertexAiPlam2ChatProperties chatProperties) {
return new VertexAiPaLm2ChatConnector(vertexAiApi, chatProperties.getOptions());
}

View File

@@ -15,17 +15,13 @@
*/
package org.springframework.ai.autoconfigure.anthropic;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.anthropic.AnthropicChatConnector;
import reactor.core.publisher.Flux;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.chat.messages.AssistantMessage;
@@ -34,6 +30,10 @@ import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -50,7 +50,7 @@ public class AnthropicAutoConfigurationIT {
@Test
void generate() {
contextRunner.run(context -> {
AnthropicChatConnector chatClient = context.getBean(AnthropicChatConnector.class);
ChatClient chatClient = ChatClient.builder(context.getBean(AnthropicChatConnector.class)).build();
String response = chatClient.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);

View File

@@ -15,14 +15,10 @@
*/
package org.springframework.ai.autoconfigure.anthropic.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.anthropic.AnthropicChatConnector;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
@@ -40,6 +36,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import java.util.List;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")

View File

@@ -15,25 +15,25 @@
*/
package org.springframework.ai.autoconfigure.mistralai;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.mistralai.MistralAiChatConnector;
import reactor.core.publisher.Flux;
import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.mistralai.MistralAiChatConnector;
import org.springframework.ai.mistralai.MistralAiEmbeddingClient;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -54,7 +54,7 @@ public class MistralAiAutoConfigurationIT {
@Test
void generate() {
contextRunner.run(context -> {
MistralAiChatConnector client = context.getBean(MistralAiChatConnector.class);
ChatClient client = ChatClient.builder(context.getBean(MistralAiChatConnector.class)).build();
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);

View File

@@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.image.ImagePrompt;
@@ -55,7 +56,7 @@ public class OpenAiAutoConfigurationIT {
@Test
void generate() {
contextRunner.run(context -> {
OpenAiChatConnector client = context.getBean(OpenAiChatConnector.class);
ChatClient client = ChatClient.builder(context.getBean(OpenAiChatConnector.class)).build();
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);

View File

@@ -15,20 +15,20 @@
*/
package org.springframework.ai.autoconfigure.vertexai.gemini;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatConnector;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import reactor.core.publisher.Flux;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
@@ -46,7 +46,7 @@ public class VertexAiGeminiAutoConfigurationIT {
@Test
void generate() {
contextRunner.run(context -> {
VertexAiGeminiChatConnector client = context.getBean(VertexAiGeminiChatConnector.class);
ChatClient client = ChatClient.builder(context.getBean(VertexAiGeminiChatConnector.class)).build();
String response = client.call("Hello");
assertThat(response).isNotEmpty();
logger.info("Response: " + response);

View File

@@ -22,6 +22,7 @@ import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.vertexai.palm2.VertexAiPaLm2ChatConnector;
import org.springframework.ai.vertexai.palm2.VertexAiPaLm2EmbeddingClient;
@@ -48,8 +49,8 @@ public class VertexAiPaLm2AutoConfigurationIT {
@Test
void generate() {
contextRunner.run(context -> {
VertexAiPaLm2ChatConnector client = context.getBean(VertexAiPaLm2ChatConnector.class);
VertexAiPaLm2ChatConnector connector = context.getBean(VertexAiPaLm2ChatConnector.class);
ChatClient client = ChatClient.builder(connector).build();
String response = client.call("Hello");
assertThat(response).isNotEmpty();