Add ChatCaller#getDefultOptions() breaking the funcitonal contract.

- Implement fromOptions copty factory for eavery chatoptions implementation.
 - Extend ChatClient to use the caller default options if not provided explicitely.
 - Fix system/user text overdidign default system/user texts. Only non empty
   user/system text can overrid defult system/user text.
 - Rename chat() method to collect().
 - Add OpenAI FunctionCallbackWrapper2IT auto-config tests.
This commit is contained in:
Christian Tzolov
2024-05-21 14:08:26 +02:00
parent a764d5b247
commit 9db0ea7775
57 changed files with 689 additions and 223 deletions

View File

@@ -223,4 +223,17 @@ public class AnthropicChatOptions implements ChatOptions, FunctionCallingOptions
this.functions = functions;
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withModel(fromOptions.getModel())
.withMaxTokens(fromOptions.getMaxTokens())
.withMetadata(fromOptions.getMetadata())
.withStopSequences(fromOptions.getStopSequences())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -457,4 +457,9 @@ public class AnthropicModelCaller extends
"Streaming (stream=true) is not yet supported. We plan to add streaming support in a future beta version.");
}
@Override
public ChatOptions getDefaultOptions() {
return AnthropicChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -356,4 +356,22 @@ public class AzureOpenAiChatOptions implements FunctionCallingOptions, ChatOptio
this.functions = functions;
}
public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOptions) {
return builder().withDeploymentName(fromOptions.getDeploymentName())
.withFrequencyPenalty(
fromOptions.getFrequencyPenalty() != null ? fromOptions.getFrequencyPenalty().floatValue() : null)
.withLogitBias(fromOptions.getLogitBias())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(
fromOptions.getPresencePenalty() != null ? fromOptions.getPresencePenalty().floatValue() : null)
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withUser(fromOptions.getUser())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -127,7 +127,7 @@ public class AzureOpenAiModelCaller
}
public AzureOpenAiChatOptions getDefaultOptions() {
return this.defaultOptions;
return AzureOpenAiChatOptions.fromOptions(this.defaultOptions);
}
@Override

View File

@@ -164,4 +164,14 @@ public class AnthropicChatOptions implements ChatOptions {
this.anthropicVersion = anthropicVersion;
}
public static AnthropicChatOptions fromOptions(AnthropicChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokensToSample(fromOptions.getMaxTokensToSample())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
}
}

View File

@@ -117,4 +117,9 @@ public class BedrockAnthropicModelCaller implements ChatCaller, StreamingChatCal
return request;
}
@Override
public ChatOptions getDefaultOptions() {
return AnthropicChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -163,4 +163,14 @@ public class Anthropic3ChatOptions implements ChatOptions {
this.anthropicVersion = anthropicVersion;
}
public static Anthropic3ChatOptions fromOptions(Anthropic3ChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withMaxTokens(fromOptions.getMaxTokens())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withStopSequences(fromOptions.getStopSequences())
.withAnthropicVersion(fromOptions.getAnthropicVersion())
.build();
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.bedrock.anthropic3;
import org.springframework.ai.bedrock.anthropic.AnthropicChatOptions;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatRequest;
import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatResponse;
@@ -187,4 +188,9 @@ public class BedrockAnthropic3ModelCaller implements ChatCaller, StreamingChatCa
}
}
@Override
public ChatOptions getDefaultOptions() {
return Anthropic3ChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -213,4 +213,17 @@ public class BedrockCohereChatOptions implements ChatOptions {
this.truncate = truncate;
}
public static BedrockCohereChatOptions fromOptions(BedrockCohereChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withMaxTokens(fromOptions.getMaxTokens())
.withStopSequences(fromOptions.getStopSequences())
.withReturnLikelihoods(fromOptions.getReturnLikelihoods())
.withNumGenerations(fromOptions.getNumGenerations())
.withLogitBias(fromOptions.getLogitBias())
.withTruncate(fromOptions.getTruncate())
.build();
}
}

View File

@@ -114,4 +114,9 @@ public class BedrockCohereModelCaller implements ChatCaller, StreamingChatCaller
return request;
}
@Override
public ChatOptions getDefaultOptions() {
return BedrockCohereChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -413,4 +413,19 @@ public class BedrockAi21Jurassic2ChatOptions implements ChatOptions {
}
}
public static BedrockAi21Jurassic2ChatOptions fromOptions(BedrockAi21Jurassic2ChatOptions fromOptions) {
return builder().withPrompt(fromOptions.getPrompt())
.withNumResults(fromOptions.getNumResults())
.withMaxTokens(fromOptions.getMaxTokens())
.withMinTokens(fromOptions.getMinTokens())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withStopSequences(fromOptions.getStopSequences())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withCountPenalty(fromOptions.getCountPenalty())
.build();
}
}

View File

@@ -121,4 +121,9 @@ public class BedrockAi21Jurassic2ModelCaller implements ChatCaller {
}
@Override
public ChatOptions getDefaultOptions() {
return BedrockAi21Jurassic2ChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -109,4 +109,11 @@ public class BedrockLlamaChatOptions implements ChatOptions {
throw new UnsupportedOperationException("Unsupported option: 'TopK'");
}
public static BedrockLlamaChatOptions fromOptions(BedrockLlamaChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxGenLen(fromOptions.getMaxGenLen())
.build();
}
}

View File

@@ -130,4 +130,9 @@ public class BedrockLlamaModelCaller implements ChatCaller, StreamingChatCaller
return request;
}
@Override
public ChatOptions getDefaultOptions() {
return BedrockLlamaChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -128,4 +128,12 @@ public class BedrockTitanChatOptions implements ChatOptions {
throw new UnsupportedOperationException("Bedrock Titan Chat does not support the 'TopK' option.'");
}
public static BedrockTitanChatOptions fromOptions(BedrockTitanChatOptions fromOptions) {
return builder().withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withMaxTokenCount(fromOptions.getMaxTokenCount())
.withStopSequences(fromOptions.getStopSequences())
.build();
}
}

View File

@@ -146,4 +146,9 @@ public class BedrockTitanModelCaller implements ChatCaller, StreamingChatCaller
};
}
@Override
public ChatOptions getDefaultOptions() {
return BedrockTitanChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -31,6 +31,8 @@ import org.springframework.ai.huggingface.model.AllOfGenerateResponseDetails;
import org.springframework.ai.huggingface.model.GenerateParameters;
import org.springframework.ai.huggingface.model.GenerateRequest;
import org.springframework.ai.huggingface.model.GenerateResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
/**
@@ -120,4 +122,9 @@ public class HuggingfaceModelCaller implements ChatCaller {
this.maxNewTokens = maxNewTokens;
}
@Override
public ChatOptions getDefaultOptions() {
return ChatOptionsBuilder.builder().build();
}
}

View File

@@ -58,11 +58,11 @@ import java.util.concurrent.ConcurrentHashMap;
* @see MiniMaxApi
* @since 1.0.0 M1
*/
public class MiniMaxChatClient extends
public class MiniMaxChatCaller extends
AbstractFunctionCallSupport<MiniMaxApi.ChatCompletionMessage, MiniMaxApi.ChatCompletionRequest, ResponseEntity<MiniMaxApi.ChatCompletion>>
implements ChatCaller, StreamingChatCaller {
private static final Logger logger = LoggerFactory.getLogger(MiniMaxChatClient.class);
private static final Logger logger = LoggerFactory.getLogger(MiniMaxChatCaller.class);
/**
* The default options used for the chat completion requests.
@@ -85,7 +85,7 @@ public class MiniMaxChatClient extends
* MiniMax Chat API.
* @throws IllegalArgumentException if MiniMaxApi is null
*/
public MiniMaxChatClient(MiniMaxApi miniMaxApi) {
public MiniMaxChatCaller(MiniMaxApi miniMaxApi) {
this(miniMaxApi,
MiniMaxChatOptions.builder().withModel(MiniMaxApi.DEFAULT_CHAT_MODEL).withTemperature(0.7f).build());
}
@@ -96,7 +96,7 @@ public class MiniMaxChatClient extends
* MiniMax Chat API.
* @param options The MiniMaxChatOptions to configure the chat client.
*/
public MiniMaxChatClient(MiniMaxApi miniMaxApi, MiniMaxChatOptions options) {
public MiniMaxChatCaller(MiniMaxApi miniMaxApi, MiniMaxChatOptions options) {
this(miniMaxApi, options, null, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
@@ -108,7 +108,7 @@ public class MiniMaxChatClient extends
* @param functionCallbackContext The function callback context.
* @param retryTemplate The retry template.
*/
public MiniMaxChatClient(MiniMaxApi miniMaxApi, MiniMaxChatOptions options,
public MiniMaxChatCaller(MiniMaxApi miniMaxApi, MiniMaxChatOptions options,
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
super(functionCallbackContext);
Assert.notNull(miniMaxApi, "MiniMaxApi must not be null");
@@ -281,22 +281,6 @@ public class MiniMaxChatClient extends
return request;
}
private String fromMediaData(MimeType mimeType, Object mediaContentData) {
if (mediaContentData instanceof byte[] bytes) {
// Assume the bytes are an image. So, convert the bytes to a base64 encoded
// following the prefix pattern.
return String.format("data:%s;base64,%s", mimeType.toString(), Base64.getEncoder().encodeToString(bytes));
}
else if (mediaContentData instanceof String text) {
// Assume the text is a URLs or a base64 encoded image prefixed by the user.
return text;
}
else {
throw new IllegalArgumentException(
"Unsupported media data type: " + mediaContentData.getClass().getSimpleName());
}
}
private List<MiniMaxApi.FunctionTool> getFunctionTools(Set<String> functionNames) {
return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> {
var function = new MiniMaxApi.FunctionTool.Function(functionCallback.getDescription(),
@@ -376,4 +360,9 @@ public class MiniMaxChatClient extends
&& choice.finishReason() == MiniMaxApi.ChatCompletionFinishReason.TOOL_CALLS;
}
@Override
public ChatOptions getDefaultOptions() {
return MiniMaxChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -467,4 +467,22 @@ public class MiniMaxChatOptions implements FunctionCallingOptions, ChatOptions {
return true;
}
public static MiniMaxChatOptions fromOptions(MiniMaxChatOptions fromOptions) {
return builder().withModel(fromOptions.getModel())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withResponseFormat(fromOptions.getResponseFormat())
.withSeed(fromOptions.getSeed())
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTools(fromOptions.getTools())
.withToolChoice(fromOptions.getToolChoice())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -33,7 +33,7 @@ public class ChatCompletionRequestTests {
@Test
public void createRequestWithChatOptions() {
var client = new MiniMaxChatClient(new MiniMaxApi("TEST"),
var client = new MiniMaxChatCaller(new MiniMaxApi("TEST"),
MiniMaxChatOptions.builder().withModel("DEFAULT_MODEL").withTemperature(66.6f).build());
var request = client.createRequest(new Prompt("Test message content"), false);
@@ -59,7 +59,7 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new MiniMaxChatClient(new MiniMaxApi("TEST"),
var client = new MiniMaxChatCaller(new MiniMaxApi("TEST"),
MiniMaxChatOptions.builder().withModel("DEFAULT_MODEL").build());
var request = client.createRequest(new Prompt("Test message content",
@@ -89,7 +89,7 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new MiniMaxChatClient(new MiniMaxApi("TEST"),
var client = new MiniMaxChatCaller(new MiniMaxApi("TEST"),
MiniMaxChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())

View File

@@ -42,8 +42,8 @@ public class MiniMaxTestConfiguration {
}
@Bean
public MiniMaxChatClient miniMaxChatClient(MiniMaxApi api) {
return new MiniMaxChatClient(api);
public MiniMaxChatCaller miniMaxChatClient(MiniMaxApi api) {
return new MiniMaxChatCaller(api);
}
@Bean

View File

@@ -22,7 +22,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.minimax.MiniMaxChatClient;
import org.springframework.ai.minimax.MiniMaxChatCaller;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingClient;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
@@ -83,7 +83,7 @@ public class MiniMaxRetryTests {
private @Mock MiniMaxApi miniMaxApi;
private MiniMaxChatClient chatClient;
private MiniMaxChatCaller chatClient;
private MiniMaxEmbeddingClient embeddingClient;
@@ -93,7 +93,7 @@ public class MiniMaxRetryTests {
retryListener = new TestRetryListener();
retryTemplate.registerListener(retryListener);
chatClient = new MiniMaxChatClient(miniMaxApi, MiniMaxChatOptions.builder().build(), null, retryTemplate);
chatClient = new MiniMaxChatCaller(miniMaxApi, MiniMaxChatOptions.builder().build(), null, retryTemplate);
embeddingClient = new MiniMaxEmbeddingClient(miniMaxApi, MetadataMode.EMBED,
MiniMaxEmbeddingOptions.builder().build(), retryTemplate);
}

View File

@@ -139,7 +139,7 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
return this;
}
public Builder withMaxToken(Integer maxTokens) {
public Builder withMaxTokens(Integer maxTokens) {
this.options.setMaxTokens(maxTokens);
return this;
}
@@ -309,4 +309,19 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions
this.functions = functions;
}
public static MistralAiChatOptions fromOptions(MistralAiChatOptions fromOptions) {
return builder().withModel(fromOptions.getModel())
.withMaxTokens(fromOptions.getMaxTokens())
.withSafePrompt(fromOptions.getSafePrompt())
.withRandomSeed(fromOptions.getRandomSeed())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withResponseFormat(fromOptions.getResponseFormat())
.withTools(fromOptions.getTools())
.withToolChoice(fromOptions.getToolChoice())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -324,4 +324,9 @@ public class MistralAiModelCaller extends
return !CollectionUtils.isEmpty(choices.get(0).message().toolCalls());
}
@Override
public ChatOptions getDefaultOptions() {
return MistralAiChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -205,4 +205,9 @@ public class OllamaModelCaller implements ChatCaller, StreamingChatCaller {
}
}
@Override
public ChatOptions getDefaultOptions() {
return OllamaOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -714,6 +714,43 @@ public class OllamaOptions implements ChatOptions, EmbeddingOptions {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
public static OllamaOptions fromOptions(OllamaOptions fromOptions) {
return new OllamaOptions()
.withModel(fromOptions.getModel())
.withFormat(fromOptions.getFormat())
.withKeepAlive(fromOptions.getKeepAlive())
.withUseNUMA(fromOptions.getUseNUMA())
.withNumCtx(fromOptions.getNumCtx())
.withNumBatch(fromOptions.getNumBatch())
.withNumGQA(fromOptions.getNumGQA())
.withNumGPU(fromOptions.getNumGPU())
.withMainGPU(fromOptions.getMainGPU())
.withLowVRAM(fromOptions.getLowVRAM())
.withF16KV(fromOptions.getF16KV())
.withLogitsAll(fromOptions.getLogitsAll())
.withVocabOnly(fromOptions.getVocabOnly())
.withUseMMap(fromOptions.getUseMMap())
.withUseMLock(fromOptions.getUseMLock())
.withNumThread(fromOptions.getNumThread())
.withNumKeep(fromOptions.getNumKeep())
.withSeed(fromOptions.getSeed())
.withNumPredict(fromOptions.getNumPredict())
.withTopK(fromOptions.getTopK())
.withTopP(fromOptions.getTopP())
.withTfsZ(fromOptions.getTfsZ())
.withTypicalP(fromOptions.getTypicalP())
.withRepeatLastN(fromOptions.getRepeatLastN())
.withTemperature(fromOptions.getTemperature())
.withRepeatPenalty(fromOptions.getRepeatPenalty())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withMirostat(fromOptions.getMirostat())
.withMirostatTau(fromOptions.getMirostatTau())
.withMirostatEta(fromOptions.getMirostatEta())
.withPenalizeNewline(fromOptions.getPenalizeNewline())
.withStop(fromOptions.getStop());
}
// @formatter:on

View File

@@ -567,4 +567,27 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
public static OpenAiChatOptions fromOptions(OpenAiChatOptions fromOptions) {
return OpenAiChatOptions.builder()
.withModel(fromOptions.getModel())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withLogitBias(fromOptions.getLogitBias())
.withLogprobs(fromOptions.getLogprobs())
.withTopLogprobs(fromOptions.getTopLogprobs())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withResponseFormat(fromOptions.getResponseFormat())
.withSeed(fromOptions.getSeed())
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTools(fromOptions.getTools())
.withToolChoice(fromOptions.getToolChoice())
.withUser(fromOptions.getUser())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -394,4 +394,9 @@ public class OpenAiModelCaller extends
&& choice.finishReason() == ChatCompletionFinishReason.TOOL_CALLS;
}
@Override
public ChatOptions getDefaultOptions() {
return OpenAiChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -62,10 +62,13 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void roleTest() {
ChatResponse response = ChatClient.builder(modelCaller).build().call()
.system(s -> s.text(systemTextResource).param("name", "Bob").param("voice", "pirate"))
.user(u -> u.text("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did"))
.chat().chatResponse();
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
.system(s -> s.text(systemTextResource).param("name", "Bob").param("voice", "pirate"))
.user(u -> u.text("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did"))
.collect()
.chatResponse();
System.out.println(response);
// UserMessage userMessage = new UserMessage(
@@ -86,9 +89,12 @@ class OpenAiChatClientIT extends AbstractIT {
void listOutputConverter() {
// TODO: there is a problem here.
Collection<String> list = ChatClient.builder(modelCaller).build().call()
.user(u -> u.text("List five {subject}").param("subject", "ice cream flavors"))
.chat().list(String.class);
Collection<String> list = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("List five {subject}").param("subject", "ice cream flavors"))
.collect()
.list(String.class);
// DefaultConversionService conversionService = new DefaultConversionService();
// ListOutputConverter outputConverter = new
@@ -113,11 +119,14 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void mapOutputConverter() {
Map<String, Object> result = ChatClient.builder(modelCaller).build().call()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.chat().single(new ParameterizedTypeReference<Map<String, Object>>() {
});
Map<String, Object> result = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.collect()
.single(new ParameterizedTypeReference<Map<String, Object>>() {
});
// MapOutputConverter outputConverter = new MapOutputConverter();
@@ -140,9 +149,12 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void beanOutputConverter() {
ActorsFilms actorsFilms = ChatClient.builder(modelCaller).build().call()
.user(u -> u.text("Generate the filmography for a random actor."))
.chat().single(ActorsFilms.class);
ActorsFilms actorsFilms = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("Generate the filmography for a random actor."))
.collect()
.single(ActorsFilms.class);
// BeanOutputConverter<ActorsFilms> outputConverter = new
// BeanOutputConverter<>(ActorsFilms.class);
@@ -170,9 +182,11 @@ class OpenAiChatClientIT extends AbstractIT {
void beanOutputConverterRecords() {
ActorsFilmsRecord actorsFilms = ChatClient.builder(modelCaller)
.build().call()
.user(u -> u.text("Generate the filmography of 5 movies for Tom Hanks."))
.chat().single(ActorsFilmsRecord.class);
.build()
.call()
.user(u -> u.text("Generate the filmography of 5 movies for Tom Hanks."))
.collect()
.single(ActorsFilmsRecord.class);
// BeanOutputConverter<ActorsFilmsRecord> outputConverter = new
// BeanOutputConverter<>(ActorsFilmsRecord.class);
@@ -199,31 +213,38 @@ class OpenAiChatClientIT extends AbstractIT {
BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
Flux<ChatResponse> chatResponse = ChatClient.builder(modelCaller).build()
.call().user(u -> u.text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator() + "{format}")
.param("format", outputConverter.getFormat()))
.stream().chatResponse();
Flux<ChatResponse> chatResponse = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u
.text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator() + "{format}")
.param("format", outputConverter.getFormat()))
.stream()
.chatResponse();
String generationTextFromStream = chatResponse.collectList().block()
.stream()
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
String generationTextFromStream = chatResponse.collectList()
.block()
.stream()
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
// String generationTextFromStream = chatResponse.collectList()
// .block()
// .stream()
// .collect(Collectors.joining());
// BeanOutputConverter<ActorsFilmsRecord> outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class);
// BeanOutputConverter<ActorsFilmsRecord> outputConverter = new
// BeanOutputConverter<>(ActorsFilmsRecord.class);
// String format = outputConverter.getFormat();
// String template = """
// Generate the filmography of 5 movies for Tom Hanks.
// {format}
// """;
// PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format", format));
// PromptTemplate promptTemplate = new PromptTemplate(template, Map.of("format",
// format));
// Prompt prompt = new Prompt(promptTemplate.createMessage());
// String generationTextFromStream = streamingChatClient.stream(prompt)
@@ -245,13 +266,13 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void functionCallTest() {
ChatResponse response = ChatClient.builder(modelCaller).build().call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
// TODO how to use the protable function calling options internally.
// Perhaps the ModelCaller a emptyOptions() method needs to be provided.
.options(OpenAiChatOptions.builder().build())
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.chat().chatResponse();
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.collect()
.chatResponse();
// UserMessage userMessage = new UserMessage("What's the weather like in San
// Francisco, Tokyo, and Paris?");
@@ -280,37 +301,40 @@ class OpenAiChatClientIT extends AbstractIT {
@Test
void streamFunctionCallTest() {
Flux<ChatResponse> response = ChatClient.builder(modelCaller).build().call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
// TODO how to use the protable function calling options internally.
// Perhaps the ModelCaller a emptyOptions() method needs to be provided.
.options(OpenAiChatOptions.builder().build())
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.stream().chatResponse();
Flux<ChatResponse> response = ChatClient.builder(modelCaller)
.build()
.call()
.user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?"))
.function("getCurrentWeather", "Get the weather in location", new MockWeatherService())
.stream()
.chatResponse();
// UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
// UserMessage userMessage = new UserMessage("What's the weather like in San
// Francisco, Tokyo, and Paris?");
// List<Message> messages = new ArrayList<>(List.of(userMessage));
// var promptOptions = OpenAiChatOptions.builder()
// // .withModel(OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW.getValue())
// .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
// .withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new
// MockWeatherService())
// .withName("getCurrentWeather")
// .withDescription("Get the weather in location")
// .withResponseConverter((response) -> "" + response.temp() + response.unit())
// .build()))
// .build();
// Flux<ChatResponse> response = streamingChatClient.stream(new Prompt(messages, promptOptions));
// Flux<ChatResponse> response = streamingChatClient.stream(new Prompt(messages,
// promptOptions));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).containsAnyOf("30.0", "30");
@@ -323,15 +347,15 @@ class OpenAiChatClientIT extends AbstractIT {
void multiModalityEmbeddedImage(String modelName) throws IOException {
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
.chat()
.chatResponse();
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png")))
.collect()
.chatResponse();
// var imageData = new ClassPathResource("/test.png");
@@ -354,13 +378,15 @@ class OpenAiChatClientIT extends AbstractIT {
// TODO: add url method that wrapps the checked exception.
URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png");
ChatResponse response = ChatClient.builder(modelCaller).build().call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, url))
.chat().chatResponse();
ChatResponse response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(modelName).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.collect()
.chatResponse();
// var userMessage = new UserMessage("Explain what do you see on this picture?",
// List
@@ -383,30 +409,34 @@ class OpenAiChatClientIT extends AbstractIT {
// TODO: add url method that wrapps the checked exception.
URL url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png");
Flux<ChatResponse> response = ChatClient.builder(modelCaller).build().call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue())
.build())
.user(u -> u.text("Explain what do you see on this picture?")
.media(MimeTypeUtils.IMAGE_PNG, url))
.stream().chatResponse();
Flux<ChatResponse> response = ChatClient.builder(modelCaller)
.build()
.call()
// TODO consider adding model(...) method to ChatClient as a shortcut to
// OpenAiChatOptions.builder().withModel(modelName).build()
.options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build())
.user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url))
.stream()
.chatResponse();
// var userMessage = new UserMessage("Explain what do you see on this picture?", List
// var userMessage = new UserMessage("Explain what do you see on this picture?",
// List
// .of(new Media(MimeTypeUtils.IMAGE_PNG,
// new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));
// new
// URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"))));
// Flux<ChatResponse> response = streamingChatClient.stream(new Prompt(List.of(userMessage),
// Flux<ChatResponse> response = streamingChatClient.stream(new
// Prompt(List.of(userMessage),
// OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));
String content = response.collectList()
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
.block()
.stream()
.map(ChatResponse::getResults)
.flatMap(List::stream)
.map(Generation::getOutput)
.map(AssistantMessage::getContent)
.collect(Collectors.joining());
logger.info("Response: {}", content);
assertThat(content).contains("bananas", "apple");
assertThat(content).containsAnyOf("bowl", "basket");

View File

@@ -336,4 +336,18 @@ public class VertexAiGeminiChatOptions implements FunctionCallingOptions, ChatOp
return true;
}
public static VertexAiGeminiChatOptions fromOptions(VertexAiGeminiChatOptions fromOptions) {
VertexAiGeminiChatOptions options = new VertexAiGeminiChatOptions();
options.setStopSequences(fromOptions.getStopSequences());
options.setTemperature(fromOptions.getTemperature());
options.setTopP(fromOptions.getTopP());
options.setTopK(fromOptions.getTopK());
options.setCandidateCount(fromOptions.getCandidateCount());
options.setMaxOutputTokens(fromOptions.getMaxOutputTokens());
options.setModel(fromOptions.getModel());
options.setFunctionCallbacks(fromOptions.getFunctionCallbacks());
options.setFunctions(fromOptions.getFunctions());
return options;
}
}

View File

@@ -478,4 +478,9 @@ public class VertexAiGeminiModelCaller
return response.getCandidatesList().get(0).getContent().getPartsList().get(0).hasFunctionCall();
}
@Override
public ChatOptions getDefaultOptions() {
return VertexAiGeminiChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -127,4 +127,13 @@ public class VertexAiPaLm2ChatOptions implements ChatOptions {
this.topK = topK;
}
public static VertexAiPaLm2ChatOptions fromOptions(VertexAiPaLm2ChatOptions fromOptions) {
return VertexAiPaLm2ChatOptions.builder()
.withTemperature(fromOptions.getTemperature())
.withCandidateCount(fromOptions.getCandidateCount())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.build();
}
}

View File

@@ -111,4 +111,9 @@ public class VertexAiPaLm2ModelCaller implements ChatCaller {
return request;
}
@Override
public ChatOptions getDefaultOptions() {
return VertexAiPaLm2ChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -324,5 +324,21 @@ public class WatsonxAiChatOptions implements ChatOptions {
return input != null ? input.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase() : null;
}
public static WatsonxAiChatOptions fromOptions(WatsonxAiChatOptions fromOptions) {
return WatsonxAiChatOptions.builder()
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTopK(fromOptions.getTopK())
.withDecodingMethod(fromOptions.getDecodingMethod())
.withMaxNewTokens(fromOptions.getMaxNewTokens())
.withMinNewTokens(fromOptions.getMinNewTokens())
.withStopSequences(fromOptions.getStopSequences())
.withRepetitionPenalty(fromOptions.getRepetitionPenalty())
.withRandomSeed(fromOptions.getRandomSeed())
.withModel(fromOptions.getModel())
.withAdditionalProperties(fromOptions.getAdditionalProperties())
.build();
}
}
// @formatter:on

View File

@@ -140,4 +140,9 @@ public class WatsonxAiModelCaller implements ChatCaller, StreamingChatCaller {
return WatsonxAiRequest.builder(convertedPrompt).withParameters(parameters).build();
}
@Override
public ChatOptions getDefaultOptions() {
return WatsonxAiChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -66,11 +66,11 @@ import java.util.concurrent.ConcurrentHashMap;
* @see ZhiPuAiApi
* @since 1.0.0 M1
*/
public class ZhiPuAiChatClient extends
public class ZhiPuAiChatCaller extends
AbstractFunctionCallSupport<ChatCompletionMessage, ZhiPuAiApi.ChatCompletionRequest, ResponseEntity<ChatCompletion>>
implements ChatCaller, StreamingChatCaller {
private static final Logger logger = LoggerFactory.getLogger(ZhiPuAiChatClient.class);
private static final Logger logger = LoggerFactory.getLogger(ZhiPuAiChatCaller.class);
/**
* The default options used for the chat completion requests.
@@ -93,7 +93,7 @@ public class ZhiPuAiChatClient extends
* ZhiPuAI Chat API.
* @throws IllegalArgumentException if zhiPuAiApi is null
*/
public ZhiPuAiChatClient(ZhiPuAiApi zhiPuAiApi) {
public ZhiPuAiChatCaller(ZhiPuAiApi zhiPuAiApi) {
this(zhiPuAiApi,
ZhiPuAiChatOptions.builder().withModel(ZhiPuAiApi.DEFAULT_CHAT_MODEL).withTemperature(0.7f).build());
}
@@ -104,7 +104,7 @@ public class ZhiPuAiChatClient extends
* ZhiPuAI Chat API.
* @param options The ZhiPuAiChatOptions to configure the chat client.
*/
public ZhiPuAiChatClient(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options) {
public ZhiPuAiChatCaller(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options) {
this(zhiPuAiApi, options, null, RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
@@ -116,7 +116,7 @@ public class ZhiPuAiChatClient extends
* @param functionCallbackContext The function callback context.
* @param retryTemplate The retry template.
*/
public ZhiPuAiChatClient(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options,
public ZhiPuAiChatCaller(ZhiPuAiApi zhiPuAiApi, ZhiPuAiChatOptions options,
FunctionCallbackContext functionCallbackContext, RetryTemplate retryTemplate) {
super(functionCallbackContext);
Assert.notNull(zhiPuAiApi, "ZhiPuAiApi must not be null");
@@ -381,4 +381,9 @@ public class ZhiPuAiChatClient extends
&& choice.finishReason() == ChatCompletionFinishReason.TOOL_CALLS;
}
@Override
public ChatOptions getDefaultOptions() {
return ZhiPuAiChatOptions.fromOptions(this.defaultOptions);
}
}

View File

@@ -490,4 +490,24 @@ public class ZhiPuAiChatOptions implements FunctionCallingOptions, ChatOptions {
throw new UnsupportedOperationException("Unimplemented method 'setTopK'");
}
public static ZhiPuAiChatOptions fromOptions(ZhiPuAiChatOptions fromOptions) {
return ZhiPuAiChatOptions.builder()
.withModel(fromOptions.getModel())
.withFrequencyPenalty(fromOptions.getFrequencyPenalty())
.withMaxTokens(fromOptions.getMaxTokens())
.withN(fromOptions.getN())
.withPresencePenalty(fromOptions.getPresencePenalty())
.withResponseFormat(fromOptions.getResponseFormat())
.withSeed(fromOptions.getSeed())
.withStop(fromOptions.getStop())
.withTemperature(fromOptions.getTemperature())
.withTopP(fromOptions.getTopP())
.withTools(fromOptions.getTools())
.withToolChoice(fromOptions.getToolChoice())
.withUser(fromOptions.getUser())
.withFunctionCallbacks(fromOptions.getFunctionCallbacks())
.withFunctions(fromOptions.getFunctions())
.build();
}
}

View File

@@ -33,7 +33,7 @@ public class ChatCompletionRequestTests {
@Test
public void createRequestWithChatOptions() {
var client = new ZhiPuAiChatClient(new ZhiPuAiApi("TEST"),
var client = new ZhiPuAiChatCaller(new ZhiPuAiApi("TEST"),
ZhiPuAiChatOptions.builder().withModel("DEFAULT_MODEL").withTemperature(66.6f).build());
var request = client.createRequest(new Prompt("Test message content"), false);
@@ -59,7 +59,7 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new ZhiPuAiChatClient(new ZhiPuAiApi("TEST"),
var client = new ZhiPuAiChatCaller(new ZhiPuAiApi("TEST"),
ZhiPuAiChatOptions.builder().withModel("DEFAULT_MODEL").build());
var request = client.createRequest(new Prompt("Test message content",
@@ -89,7 +89,7 @@ public class ChatCompletionRequestTests {
final String TOOL_FUNCTION_NAME = "CurrentWeather";
var client = new ZhiPuAiChatClient(new ZhiPuAiApi("TEST"),
var client = new ZhiPuAiChatCaller(new ZhiPuAiApi("TEST"),
ZhiPuAiChatOptions.builder()
.withModel("DEFAULT_MODEL")
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())

View File

@@ -48,8 +48,8 @@ public class ZhiPuAiTestConfiguration {
}
@Bean
public ZhiPuAiChatClient zhiPuAiChatClient(ZhiPuAiApi api) {
return new ZhiPuAiChatClient(api);
public ZhiPuAiChatCaller zhiPuAiChatClient(ZhiPuAiApi api) {
return new ZhiPuAiChatCaller(api);
}
@Bean

View File

@@ -26,7 +26,7 @@ import org.springframework.ai.image.ImageMessage;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.retry.TransientAiException;
import org.springframework.ai.zhipuai.ZhiPuAiChatClient;
import org.springframework.ai.zhipuai.ZhiPuAiChatCaller;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingClient;
import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions;
@@ -93,7 +93,7 @@ public class ZhiPuAiRetryTests {
private @Mock ZhiPuAiImageApi zhiPuAiImageApi;
private ZhiPuAiChatClient chatClient;
private ZhiPuAiChatCaller chatClient;
private ZhiPuAiEmbeddingClient embeddingClient;
@@ -105,7 +105,7 @@ public class ZhiPuAiRetryTests {
retryListener = new TestRetryListener();
retryTemplate.registerListener(retryListener);
chatClient = new ZhiPuAiChatClient(zhiPuAiApi, ZhiPuAiChatOptions.builder().build(), null, retryTemplate);
chatClient = new ZhiPuAiChatCaller(zhiPuAiApi, ZhiPuAiChatOptions.builder().build(), null, retryTemplate);
embeddingClient = new ZhiPuAiEmbeddingClient(zhiPuAiApi, MetadataMode.EMBED,
ZhiPuAiEmbeddingOptions.builder().build(), retryTemplate);
imageClient = new ZhiPuAiImageClient(zhiPuAiImageApi, ZhiPuAiImageOptions.builder().build(), retryTemplate);