diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java index aa88700e3..68ae47349 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatOptions.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ResponseFormat; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice; +import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.mistralai.api.MistralAiApi.FunctionTool; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallingOptions; @@ -139,6 +140,11 @@ public class MistralAiChatOptions implements FunctionCallingOptions, ChatOptions return this; } + public Builder withModel(MistralAiApi.ChatModel chatModel) { + this.options.setModel(chatModel.getModelName()); + return this; + } + public Builder withMaxTokens(Integer maxTokens) { this.options.setMaxTokens(maxTokens); return this; diff --git a/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java new file mode 100644 index 000000000..e3c1b92c4 --- /dev/null +++ b/models/spring-ai-mistral-ai/src/test/java/org/springframework/ai/mistralai/MistralAiChatClientIT.java @@ -0,0 +1,252 @@ +/* + * Copyright 2023 - 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.mistralai; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.ai.converter.ListOutputConverter; +import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = MistralAiTestConfiguration.class) +@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".+") +class MistralAiChatClientIT { + + private static final Logger logger = LoggerFactory.getLogger(MistralAiChatClientIT.class); + + @Autowired + MistralAiChatModel chatModel; + + @Value("classpath:/prompts/system-message.st") + private Resource systemTextResource; + + record ActorsFilms(String actor, List movies) { + } + + @Test + void call() { + // @formatter:off + ChatResponse response = ChatClient.create(chatModel).prompt() + .system(s -> s.text(systemTextResource) + .param("name", "Bob") + .param("voice", "pirate")) + .user("Tell me about 3 famous pirates from the Golden Age of Piracy and what they did") + .call() + .chatResponse(); + // @formatter:on + + logger.info("" + response); + assertThat(response.getResults()).hasSize(1); + assertThat(response.getResults().get(0).getOutput().getContent()).contains("Blackbeard"); + } + + @Test + void listOutputConverterString() { + // @formatter:off + List collection = ChatClient.create(chatModel).prompt() + .user(u -> u.text("List five {subject}") + .param("subject", "ice cream flavors")) + .call() + .entity(new ParameterizedTypeReference>() {}); + // @formatter:on + + logger.info(collection.toString()); + assertThat(collection).hasSize(5); + } + + @Test + void listOutputConverterBean() { + + // @formatter:off + List actorsFilms = ChatClient.create(chatModel).prompt() + .user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.") + .call() + .entity(new ParameterizedTypeReference>() { + }); + // @formatter:on + + logger.info("" + actorsFilms); + assertThat(actorsFilms).hasSize(2); + } + + @Test + void customOutputConverter() { + + var toStringListConverter = new ListOutputConverter(new DefaultConversionService()); + + // @formatter:off + List flavors = ChatClient.create(chatModel).prompt() + .user(u -> u.text("List 10 {subject}") + .param("subject", "ice cream flavors")) + .call() + .entity(toStringListConverter); + // @formatter:on + + logger.info("ice cream flavors" + flavors); + assertThat(flavors).hasSize(10); + assertThat(flavors).containsAnyOf("Vanilla", "vanilla"); + } + + @Test + void mapOutputConverter() { + // @formatter:off + Map result = ChatClient.create(chatModel).prompt() + .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'")) + .call() + .entity(new ParameterizedTypeReference>() { + }); + // @formatter:on + + assertThat(result.get("numbers")).isEqualTo(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + } + + @Test + void beanOutputConverter() { + + // @formatter:off + ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt() + .user("Generate the filmography for a random actor.") + .call() + .entity(ActorsFilms.class); + // @formatter:on + + logger.info("" + actorsFilms); + assertThat(actorsFilms.actor()).isNotBlank(); + } + + @Test + void beanOutputConverterRecords() { + + // @formatter:off + ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt() + .user("Generate the filmography of 5 movies for Tom Hanks.") + .call() + .entity(ActorsFilms.class); + // @formatter:on + + logger.info("" + actorsFilms); + assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks"); + assertThat(actorsFilms.movies()).hasSize(5); + } + + @Test + void beanStreamOutputConverterRecords() { + + BeanOutputConverter outputConverter = new BeanOutputConverter<>(ActorsFilms.class); + + // @formatter:off + Flux chatResponse = ChatClient.create(chatModel) + .prompt() + .user(u -> u + .text("Generate the filmography of 5 movies for Tom Hanks. " + System.lineSeparator() + + "{format}") + .param("format", outputConverter.getFormat())) + .stream() + .content(); + + String generationTextFromStream = chatResponse.collectList() + .block() + .stream() + .collect(Collectors.joining()); + // @formatter:on + + ActorsFilms actorsFilms = outputConverter.convert(generationTextFromStream); + + logger.info("" + actorsFilms); + assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks"); + assertThat(actorsFilms.movies()).hasSize(5); + } + + @Test + void functionCallTest() { + + // @formatter:off + String response = ChatClient.create(chatModel).prompt() + .options(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).withToolChoice(ToolChoice.AUTO).build()) + .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius.")) + .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .call() + .content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).containsAnyOf("30.0", "30"); + assertThat(response).containsAnyOf("10.0", "10"); + assertThat(response).containsAnyOf("15.0", "15"); + } + + @Test + void defaultFunctionCallTest() { + + // @formatter:off + String response = ChatClient.builder(chatModel) + .defaultOptions(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).build()) + .defaultFunction("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .defaultUser(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius.")) + .build() + .prompt().call().content(); + // @formatter:on + + logger.info("Response: {}", response); + + assertThat(response).containsAnyOf("30.0", "30"); + assertThat(response).containsAnyOf("10.0", "10"); + assertThat(response).containsAnyOf("15.0", "15"); + } + + @Test + void streamFunctionCallTest() { + + // @formatter:off + Flux response = ChatClient.create(chatModel).prompt() + .options(MistralAiChatOptions.builder().withModel(MistralAiApi.ChatModel.SMALL).build()) + .user("What's the weather like in San Francisco, Tokyo, and Paris? Use parallel function calling if required. Response should be in Celsius.") + .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) + .stream() + .content(); + // @formatter:on + + String content = response.collectList().block().stream().collect(Collectors.joining()); + logger.info("Response: {}", content); + + assertThat(content).containsAnyOf("30.0", "30"); + assertThat(content).containsAnyOf("10.0", "10"); + assertThat(content).containsAnyOf("15.0", "15"); + } + +} \ No newline at end of file diff --git a/models/spring-ai-mistral-ai/src/test/resources/test.png b/models/spring-ai-mistral-ai/src/test/resources/test.png new file mode 100644 index 000000000..8abb4c81a Binary files /dev/null and b/models/spring-ai-mistral-ai/src/test/resources/test.png differ diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java index ee6ee9870..c6bee6a44 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientIT.java @@ -280,7 +280,6 @@ class OpenAiChatClientIT extends AbstractIT { // @formatter:off String response = ChatClient.create(chatModel).prompt() // 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)) .call() diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java index 65ac3c4c3..34d42a8aa 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -86,8 +86,6 @@ public interface ChatClient { PromptUserSpec media(MimeType mimeType, Resource resource); - List media(); - } interface PromptSystemSpec { @@ -228,8 +226,6 @@ public interface ChatClient { Builder defaultAdvisors(List advisors); - ChatClient build(); - Builder defaultOptions(ChatOptions chatOptions); Builder defaultUser(String text); @@ -252,51 +248,8 @@ public interface ChatClient { Builder defaultFunctions(String... functionNames); - } + ChatClient build(); - /** - * Calls the underlying chat model with a prompt message and returns the output - * content of the first generation. - * @param message The message to be used as a prompt for the chat model. - * @return The output content of the first generation. - * @deprecated This method is deprecated as of version 1.0.0 M1 and will be removed in - * a future release. Use the method - * builder(chatModel).build().prompt().user(message).call().content() instead - * - */ - @Deprecated(since = "1.0.0 M1", forRemoval = true) - default String call(String message) { - var prompt = new Prompt(new UserMessage(message)); - var generation = call(prompt).getResult(); - return (generation != null) ? generation.getOutput().getContent() : ""; } - /** - * Calls the underlying chat model with a prompt message and returns the output - * content of the first generation. - * @param messages The messages to be used as a prompt for the chat model. - * @return The output content of the first generation. - * @deprecated This method is deprecated as of version 1.0.0 M1 and will be removed in - * a future release. Use the method - * builder(chatModel).build().prompt().messages(messages).call().content() instead. - */ - @Deprecated(since = "1.0.0 M1", forRemoval = true) - default String call(Message... messages) { - var prompt = new Prompt(Arrays.asList(messages)); - var generation = call(prompt).getResult(); - return (generation != null) ? generation.getOutput().getContent() : ""; - } - - /** - * Calls the underlying chat model with a prompt and returns the corresponding chat - * response. - * @param prompt The prompt to be used for the chat model. - * @return The chat response containing the generated messages. - * @deprecated This method is deprecated as of version 1.0.0 M1 and will be removed in - * a future release. Use the method builder(chatModel).build().prompt(prompt).call() - * instead. - */ - @Deprecated(since = "1.0.0 M1", forRemoval = true) - ChatResponse call(Prompt prompt); - } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java index 3748b42a6..a54d11fe4 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java @@ -145,8 +145,7 @@ public class DefaultChatClient implements ChatClient { return this.params; } - @Override - public List media() { + protected List media() { return this.media; } @@ -447,7 +446,7 @@ public class DefaultChatClient implements ChatClient { return ""; } return r.getResult().getOutput().getContent(); - }).filter(v -> StringUtils.hasText(v)); + }).filter(v -> StringUtils.hasLength(v)); } } @@ -824,15 +823,4 @@ public class DefaultChatClient implements ChatClient { } - /** - * use the new fluid DSL starting in {@link #prompt()} - * @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 this.chatModel.call(prompt); - } - }