From bc5c47b201248a4f049400039207fbaa4e9ba2bd Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 21 May 2024 20:34:15 +0200 Subject: [PATCH] Add overload methods for defaultSytem()/defaultUer() and system() methods. Most ChatModel enums extend the new ModelDescription interface. Add ChatClientTest mockito testing --- .../ai/anthropic/api/AnthropicApi.java | 8 +- .../api/AnthropicChatBedrockApi.java | 8 +- .../api/Anthropic3ChatBedrockApi.java | 8 +- .../cohere/api/CohereChatBedrockApi.java | 8 +- .../api/Ai21Jurassic2ChatBedrockApi.java | 8 +- .../llama/api/LlamaChatBedrockApi.java | 8 +- .../titan/api/TitanChatBedrockApi.java | 8 +- .../ai/minimax/api/MiniMaxApi.java | 9 +- .../ai/mistralai/api/MistralAiApi.java | 8 +- .../ai/ollama/api/OllamaModel.java | 9 +- .../ai/openai/api/OpenAiApi.java | 8 +- .../ai/openai/chat/OpenAiChatClientIT.java | 39 ++++++-- .../gemini/VertexAiGeminiModelCaller.java | 8 +- .../ai/zhipuai/api/ZhiPuAiApi.java | 9 +- .../springframework/ai/chat/ChatClient.java | 33 +++++-- .../ai/model/ModelDescription.java | 38 ++++++++ .../ai/chat/ChatClientTest.java | 91 +++++++++++++++++++ .../org/springframework/ai/chat/Main.java | 56 ------------ .../tool/FunctionCallbackInPrompt2IT.java | 13 ++- .../tool/FunctionCallbackWrapper2IT.java | 4 + 20 files changed, 288 insertions(+), 93 deletions(-) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/model/ModelDescription.java create mode 100644 spring-ai-core/src/test/java/org/springframework/ai/chat/ChatClientTest.java delete mode 100644 spring-ai-core/src/test/java/org/springframework/ai/chat/Main.java diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java index eb5a96289..76c969a44 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.ai.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; import org.springframework.http.HttpHeaders; @@ -116,7 +117,7 @@ public class AnthropicApi { * "https://docs.anthropic.com/claude/docs/models-overview#model-comparison">model * comparison for additional details and options. */ - public enum ChatModel { + public enum ChatModel implements ModelDescription { // @formatter:off CLAUDE_3_OPUS("claude-3-opus-20240229"), @@ -140,6 +141,11 @@ public class AnthropicApi { return this.value; } + @Override + public String getModelName() { + return this.value; + } + } /** diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/api/AnthropicChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/api/AnthropicChatBedrockApi.java index 55a2d80af..8c800a382 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/api/AnthropicChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic/api/AnthropicChatBedrockApi.java @@ -29,6 +29,7 @@ import software.amazon.awssdk.regions.Region; import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatRequest; import org.springframework.ai.bedrock.anthropic.api.AnthropicChatBedrockApi.AnthropicChatResponse; import org.springframework.ai.bedrock.api.AbstractBedrockApi; +import org.springframework.ai.model.ModelDescription; import org.springframework.util.Assert; /** @@ -225,7 +226,7 @@ public class AnthropicChatBedrockApi extends /** * Anthropic models version. */ - public enum AnthropicChatModel { + public enum AnthropicChatModel implements ModelDescription { /** * anthropic.claude-instant-v1 */ @@ -251,6 +252,11 @@ public class AnthropicChatBedrockApi extends AnthropicChatModel(String value) { this.id = value; } + + @Override + public String getModelName() { + return this.id; + } } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/api/Anthropic3ChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/api/Anthropic3ChatBedrockApi.java index 0148b4983..8b5b29ed1 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/api/Anthropic3ChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/anthropic3/api/Anthropic3ChatBedrockApi.java @@ -23,6 +23,7 @@ import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.An import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatResponse; import org.springframework.ai.bedrock.anthropic3.api.Anthropic3ChatBedrockApi.AnthropicChatStreamingResponse; import org.springframework.ai.bedrock.api.AbstractBedrockApi; +import org.springframework.ai.model.ModelDescription; import org.springframework.util.Assert; import reactor.core.publisher.Flux; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -436,7 +437,7 @@ public class Anthropic3ChatBedrockApi extends /** * Anthropic models version. */ - public enum AnthropicChatModel { + public enum AnthropicChatModel implements ModelDescription { /** * anthropic.claude-instant-v1 @@ -476,6 +477,11 @@ public class Anthropic3ChatBedrockApi extends this.id = value; } + @Override + public String getModelName() { + return this.id; + } + } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereChatBedrockApi.java index 5b133a997..766271b87 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/cohere/api/CohereChatBedrockApi.java @@ -30,6 +30,7 @@ import software.amazon.awssdk.regions.Region; import org.springframework.ai.bedrock.api.AbstractBedrockApi; import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatRequest; import org.springframework.ai.bedrock.cohere.api.CohereChatBedrockApi.CohereChatResponse; +import org.springframework.ai.model.ModelDescription; import org.springframework.util.Assert; /** @@ -366,7 +367,7 @@ public class CohereChatBedrockApi extends /** * Cohere models version. */ - public enum CohereChatModel { + public enum CohereChatModel implements ModelDescription { /** * cohere.command-light-text-v14 @@ -390,6 +391,11 @@ public class CohereChatBedrockApi extends CohereChatModel(String value) { this.id = value; } + + @Override + public String getModelName() { + return this.id; + } } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/api/Ai21Jurassic2ChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/api/Ai21Jurassic2ChatBedrockApi.java index 0ec58c8bd..fecf70fa4 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/api/Ai21Jurassic2ChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/jurassic2/api/Ai21Jurassic2ChatBedrockApi.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.ai.bedrock.api.AbstractBedrockApi; import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi.Ai21Jurassic2ChatRequest; import org.springframework.ai.bedrock.jurassic2.api.Ai21Jurassic2ChatBedrockApi.Ai21Jurassic2ChatResponse; +import org.springframework.ai.model.ModelDescription; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -371,7 +372,7 @@ public class Ai21Jurassic2ChatBedrockApi extends /** * Ai21 Jurassic2 models version. */ - public enum Ai21Jurassic2ChatModel { + public enum Ai21Jurassic2ChatModel implements ModelDescription { /** * ai21.j2-mid-v1 @@ -395,6 +396,11 @@ public class Ai21Jurassic2ChatBedrockApi extends Ai21Jurassic2ChatModel(String value) { this.id = value; } + + @Override + public String getModelName() { + return this.id; + } } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/api/LlamaChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/api/LlamaChatBedrockApi.java index 25d71aede..16af9735e 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/api/LlamaChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/llama/api/LlamaChatBedrockApi.java @@ -26,6 +26,7 @@ import software.amazon.awssdk.regions.Region; import org.springframework.ai.bedrock.api.AbstractBedrockApi; import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatRequest; import org.springframework.ai.bedrock.llama.api.LlamaChatBedrockApi.LlamaChatResponse; +import org.springframework.ai.model.ModelDescription; import java.time.Duration; @@ -204,7 +205,7 @@ public class LlamaChatBedrockApi extends /** * Llama models version. */ - public enum LlamaChatModel { + public enum LlamaChatModel implements ModelDescription { /** * meta.llama2-13b-chat-v1 @@ -238,6 +239,11 @@ public class LlamaChatBedrockApi extends LlamaChatModel(String value) { this.id = value; } + + @Override + public String getModelName() { + return this.id; + } } @Override diff --git a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanChatBedrockApi.java b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanChatBedrockApi.java index 78c7cd931..ce1842adf 100644 --- a/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanChatBedrockApi.java +++ b/models/spring-ai-bedrock/src/main/java/org/springframework/ai/bedrock/titan/api/TitanChatBedrockApi.java @@ -31,6 +31,7 @@ import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatReq import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponse; import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponse.CompletionReason; import org.springframework.ai.bedrock.titan.api.TitanChatBedrockApi.TitanChatResponseChunk; +import org.springframework.ai.model.ModelDescription; /** * Java client for the Bedrock Titan chat model. @@ -265,7 +266,7 @@ public class TitanChatBedrockApi extends /** * Titan models version. */ - public enum TitanChatModel { + public enum TitanChatModel implements ModelDescription { /** * amazon.titan-text-lite-v1 @@ -294,6 +295,11 @@ public class TitanChatBedrockApi extends TitanChatModel(String value) { this.id = value; } + + @Override + public String getModelName() { + return this.id; + } } @Override diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java index 915e13142..e92b1ac2e 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/api/MiniMaxApi.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; + +import org.springframework.ai.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; import org.springframework.boot.context.properties.bind.ConstructorBinding; @@ -111,7 +113,7 @@ public class MiniMaxApi { * MiniMax Chat Completion Models: * MiniMax Model. */ - public enum ChatModel { + public enum ChatModel implements ModelDescription { ABAB_6_Chat("abab6-chat"), ABAB_5_5_Chat("abab5.5-chat"), ABAB_5_5_S_Chat("abab5.5s-chat"); @@ -125,6 +127,11 @@ public class MiniMaxApi { public String getValue() { return value; } + + @Override + public String getModelName() { + return this.value; + } } /** diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/api/MistralAiApi.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/api/MistralAiApi.java index 16f2465c5..b2d5230eb 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/api/MistralAiApi.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/api/MistralAiApi.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.ai.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; import org.springframework.boot.context.properties.bind.ConstructorBinding; @@ -706,7 +707,7 @@ public class MistralAiApi { *
  • LARGE - mistral-large-latest (aka mistral-large-2402)
  • * */ - public enum ChatModel { + public enum ChatModel implements ModelDescription { // @formatter:off TINY("open-mistral-7b"), @@ -726,6 +727,11 @@ public class MistralAiApi { return this.value; } + @Override + public String getModelName() { + return this.value; + } + } /** diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaModel.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaModel.java index 73d41053c..449bab647 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaModel.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/api/OllamaModel.java @@ -15,13 +15,15 @@ */ package org.springframework.ai.ollama.api; +import org.springframework.ai.model.ModelDescription; + /** * Helper class for common Ollama models. * * @author Siarhei Blashuk * @since 0.8.1 */ -public enum OllamaModel { +public enum OllamaModel implements ModelDescription { /** * Llama 2 is a collection of language models ranging from 7B to 70B parameters. @@ -99,4 +101,9 @@ public enum OllamaModel { return this.id; } + @Override + public String getModelName() { + return this.id; + } + } diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java index cc4256267..5d1a8b12b 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/api/OpenAiApi.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.springframework.ai.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; import org.springframework.boot.context.properties.bind.ConstructorBinding; @@ -113,7 +114,7 @@ public class OpenAiApi { * - GPT-4 and GPT-4 Turbo * - GPT-3.5 Turbo. */ - public enum ChatModel { + public enum ChatModel implements ModelDescription { /** * Multimodal flagship model that’s cheaper and faster than GPT-4 Turbo. * Currently points to gpt-4o-2024-05-13. @@ -199,6 +200,11 @@ public class OpenAiApi { public String getValue() { return value; } + + @Override + public String getModelName() { + return this.value; + } } /** diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java index 758d27f75..a0d8b8a70 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatClientIT.java @@ -33,9 +33,8 @@ import reactor.core.publisher.Flux; 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; import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiTestConfiguration; import org.springframework.ai.openai.api.OpenAiApi; @@ -62,14 +61,17 @@ class OpenAiChatClientIT extends AbstractIT { @Test void roleTest() { + // @formatter:off ChatResponse response = ChatClient.builder(modelCaller).build().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(); + .call() + .chatResponse(); + // @formatter:on - System.out.println(response); + logger.info("" + response); // UserMessage userMessage = new UserMessage( // "Tell me about 3 famous pirates from the Golden Age of Piracy and what they // did."); @@ -88,10 +90,13 @@ class OpenAiChatClientIT extends AbstractIT { void listOutputConverter() { // TODO: there is a problem here. + // @formatter:off Collection list = ChatClient.builder(modelCaller).build().prompt() .user(u -> u.text("List five {subject}") .param("subject", "ice cream flavors")) - .call().list(String.class); + .call() + .list(String.class); + // @formatter:on // DefaultConversionService conversionService = new DefaultConversionService(); // ListOutputConverter outputConverter = new @@ -115,12 +120,13 @@ class OpenAiChatClientIT extends AbstractIT { @Test void mapOutputConverter() { - + // @formatter:off Map result = ChatClient.builder(modelCaller).build().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().single(new ParameterizedTypeReference>() { }); + // @formatter:on // MapOutputConverter outputConverter = new MapOutputConverter(); @@ -143,10 +149,12 @@ class OpenAiChatClientIT extends AbstractIT { @Test void beanOutputConverter() { + // @formatter:off ActorsFilms actorsFilms = ChatClient.builder(modelCaller).build().prompt() .user("Generate the filmography for a random actor.") .call() .single(ActorsFilms.class); + // @formatter:on // BeanOutputConverter outputConverter = new // BeanOutputConverter<>(ActorsFilms.class); @@ -173,10 +181,12 @@ class OpenAiChatClientIT extends AbstractIT { @Test void beanOutputConverterRecords() { + // @formatter:off ActorsFilmsRecord actorsFilms = ChatClient.builder(modelCaller).build().prompt() .user("Generate the filmography of 5 movies for Tom Hanks.") .call() .single(ActorsFilmsRecord.class); + // @formatter:on // BeanOutputConverter outputConverter = new // BeanOutputConverter<>(ActorsFilmsRecord.class); @@ -203,6 +213,7 @@ class OpenAiChatClientIT extends AbstractIT { BeanOutputConverter outputConverter = new BeanOutputConverter<>(ActorsFilmsRecord.class); + // @formatter:off Flux chatResponse = ChatClient.builder(modelCaller) .build() .prompt() @@ -217,6 +228,7 @@ class OpenAiChatClientIT extends AbstractIT { .block() .stream() .collect(Collectors.joining()); + // @formatter:on // String generationTextFromStream = chatResponse.collectList() // .block() @@ -254,11 +266,13 @@ class OpenAiChatClientIT extends AbstractIT { @Test void functionCallTest() { + // @formatter:off String response = ChatClient.builder(modelCaller).build().prompt() .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) .call() .content(); + // @formatter:on // UserMessage userMessage = new UserMessage("What's the weather like in San // Francisco, Tokyo, and Paris?"); @@ -287,11 +301,13 @@ class OpenAiChatClientIT extends AbstractIT { @Test void streamFunctionCallTest() { + // @formatter:off Flux response = ChatClient.builder(modelCaller).build().prompt() .user("What's the weather like in San Francisco, Tokyo, and Paris?") .function("getCurrentWeather", "Get the weather in location", new MockWeatherService()) .stream() .content(); + // @formatter:on // UserMessage userMessage = new UserMessage("What's the weather like in San // Francisco, Tokyo, and Paris?"); @@ -311,10 +327,7 @@ class OpenAiChatClientIT extends AbstractIT { // Flux response = streamingChatClient.stream(new Prompt(messages, // promptOptions)); - String content = response.collectList() - .block() - .stream() - .collect(Collectors.joining()); + String content = response.collectList().block().stream().collect(Collectors.joining()); logger.info("Response: {}", content); assertThat(content).containsAnyOf("30.0", "30"); @@ -326,6 +339,7 @@ class OpenAiChatClientIT extends AbstractIT { @ValueSource(strings = { "gpt-4-vision-preview", "gpt-4o" }) void multiModalityEmbeddedImage(String modelName) throws IOException { + // @formatter:off String response = ChatClient.builder(modelCaller).build().prompt() // TODO consider adding model(...) method to ChatClient as a shortcut to // OpenAiChatOptions.builder().withModel(modelName).build() @@ -334,6 +348,7 @@ class OpenAiChatClientIT extends AbstractIT { .media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/test.png"))) .call() .content(); + // @formatter:on // var imageData = new ClassPathResource("/test.png"); @@ -356,6 +371,7 @@ 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"); + // @formatter:off String response = ChatClient.builder(modelCaller) .build() .prompt() @@ -365,6 +381,7 @@ class OpenAiChatClientIT extends AbstractIT { .user(u -> u.text("Explain what do you see on this picture?").media(MimeTypeUtils.IMAGE_PNG, url)) .call() .content(); + // @formatter:on // var userMessage = new UserMessage("Explain what do you see on this picture?", // List @@ -387,6 +404,7 @@ 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"); + // @formatter:off Flux response = ChatClient.builder(modelCaller).build().prompt() .options(OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()) .build()) @@ -394,6 +412,7 @@ class OpenAiChatClientIT extends AbstractIT { .media(MimeTypeUtils.IMAGE_PNG, url)) .stream() .content(); + // @formatter:on // var userMessage = new UserMessage("Explain what do you see on this picture?", // List.of(new Media(MimeTypeUtils.IMAGE_PNG, diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiModelCaller.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiModelCaller.java index 7000032f8..c2bde3291 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiModelCaller.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiModelCaller.java @@ -42,6 +42,7 @@ import org.springframework.ai.chat.messages.MessageType; 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.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.model.function.AbstractFunctionCallSupport; import org.springframework.ai.model.function.FunctionCallbackContext; @@ -95,7 +96,7 @@ public class VertexAiGeminiModelCaller } - public enum ChatModel { + public enum ChatModel implements ModelDescription { GEMINI_PRO_VISION("gemini-pro-vision"), @@ -115,6 +116,11 @@ public class VertexAiGeminiModelCaller return this.value; } + @Override + public String getModelName() { + return this.value; + } + } public VertexAiGeminiModelCaller(VertexAI vertexAI) { diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java index 5984e189c..45e21a500 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/api/ZhiPuAiApi.java @@ -18,6 +18,8 @@ package org.springframework.ai.zhipuai.api; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.ai.model.ModelDescription; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; import org.springframework.boot.context.properties.bind.ConstructorBinding; @@ -110,7 +112,7 @@ public class ZhiPuAiApi { * ZhiPuAI Chat Completion Models: * ZhiPuAI Model. */ - public enum ChatModel { + public enum ChatModel implements ModelDescription { GLM_4("GLM-4"), GLM_3_Turbo("GLM-3-Turbo"); @@ -123,6 +125,11 @@ public class ZhiPuAiApi { public String getValue() { return value; } + + @Override + public String getModelName() { + return this.value; + } } /** diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java index cd71d8462..655ff2336 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/ChatClient.java @@ -246,6 +246,11 @@ public interface ChatClient { return this; } + public ChatClientRequest system(String text) { + this.systemText = text; + return this; + } + public ChatClientRequest system(Consumer consumer) { var ss = new SystemSpec(); consumer.accept(ss); @@ -364,7 +369,7 @@ public interface ChatClient { } @SuppressWarnings("unused") - public Collection list(Class clzz) { + public Collection list(Class type) { return single(new ParameterizedTypeReference>() { }); } @@ -461,15 +466,13 @@ public interface ChatClient { } public Flux content() { - return doGetFluxChatResponse(this.request.userText) - .map(r -> { - if (r.getResult() == null || r.getResult().getOutput() == null - || r.getResult().getOutput().getContent() == null) { - return ""; - } - return r.getResult().getOutput().getContent(); - }) - .filter(v -> StringUtils.hasText(v)); + return doGetFluxChatResponse(this.request.userText).map(r -> { + if (r.getResult() == null || r.getResult().getOutput() == null + || r.getResult().getOutput().getContent() == null) { + return ""; + } + return r.getResult().getOutput().getContent(); + }).filter(v -> StringUtils.hasText(v)); } // @SuppressWarnings("unused") @@ -515,11 +518,21 @@ public interface ChatClient { return this; } + public ChatClientBuilder defaultUser(String text) { + this.defaultRequest.user(text); + return this; + } + public ChatClientBuilder defaultUser(Consumer userSpecConsumer) { this.defaultRequest.user(userSpecConsumer); return this; } + public ChatClientBuilder defaultSystem(String text) { + this.defaultRequest.system(text); + return this; + } + public ChatClientBuilder defaultSystem(Consumer systemSpecConsumer) { this.defaultRequest.system(systemSpecConsumer); return this; diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/ModelDescription.java b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelDescription.java new file mode 100644 index 000000000..57b083fab --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/ModelDescription.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model; + +/** + * @author Christian Tzolov + */ +public interface ModelDescription { + + String getModelName(); + + default String getDescription() { + return ""; + } + + default String getVersion() { + return ""; + } + + default int getContextLength() { + return -1; + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatClientTest.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatClientTest.java new file mode 100644 index 000000000..2f2b2833a --- /dev/null +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/ChatClientTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.chat; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.function.FunctionCallingOptions; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author Christian Tzolov + */ +@ExtendWith(MockitoExtension.class) +public class ChatClientTest { + + @Mock + ChatCaller modelCaller; + + @Captor + ArgumentCaptor promptCaptor; + + @Test + public void call() throws MalformedURLException { + + var options = FunctionCallingOptions.builder().build(); + when(modelCaller.getDefaultOptions()).thenReturn(options); + when(modelCaller.call(promptCaptor.capture())) + .thenReturn(new ChatResponse(List.of(new Generation("response")))); + + var url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"); + + // @formatter:off + ChatClient client = ChatClient.builder(modelCaller) + .defaultSystem("System text") + .defaultFunctions("function1") + .build(); + + String response = client.prompt() + .user(u -> u.text("User text {music}").param("music", "Rock").media(MimeTypeUtils.IMAGE_PNG, url)) + .call() + .content(); + // @formatter:on + + assertThat(response).isEqualTo("response"); + assertThat(promptCaptor.getValue().getInstructions()).hasSize(2); + + Message systemMessage = promptCaptor.getValue().getInstructions().get(0); + assertThat(systemMessage.getContent()).isEqualTo("System text"); + assertThat(systemMessage.getMessageType()).isEqualTo(MessageType.SYSTEM); + + Message userMessage = promptCaptor.getValue().getInstructions().get(1); + assertThat(userMessage.getContent()).isEqualTo("User text Rock"); + assertThat(userMessage.getMessageType()).isEqualTo(MessageType.USER); + assertThat(userMessage.getMedia()).hasSize(1); + assertThat(userMessage.getMedia().iterator().next().getMimeType()).isEqualTo(MimeTypeUtils.IMAGE_PNG); + assertThat(userMessage.getMedia().iterator().next().getData()) + .isEqualTo("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"); + + assertThat(options.getFunctions()).containsExactly("function1"); + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/Main.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/Main.java deleted file mode 100644 index 08cd269c6..000000000 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/Main.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.chat; - -import java.net.MalformedURLException; -import java.net.URL; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import org.springframework.util.MimeTypeUtils; - -/** - * @author Christian Tzolov - */ -@ExtendWith(MockitoExtension.class) -public class Main { - - @Mock - ChatCaller modelCaller; - - @Test - public void test() throws MalformedURLException { - - var url = new URL("https://docs.spring.io/spring-ai/reference/1.0-SNAPSHOT/_images/multimodal.test.png"); - - ChatClient client = ChatClient.builder(modelCaller) - .defaultSystem(s -> s.text("System text {music}")) - .defaultUser(u -> u.param("music", "Jazz")) - .defaultFunctions("function1") - .build(); - - String response = client.prompt() - .user(u -> u.text("User text {music}").param("music", "Rock").media(MimeTypeUtils.IMAGE_PNG, url)) - .call() - .single(String.class); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java index 8d7606f81..b96dd1cf5 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java @@ -39,9 +39,9 @@ public class FunctionCallbackInPrompt2IT { private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class)); + .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class)); @Test void functionCallTest() { @@ -51,6 +51,7 @@ public class FunctionCallbackInPrompt2IT { ChatClient chatClient = ChatClient.builder(caller).build(); + // @formatter:off chatClient.prompt() .user("Tell me a joke?") .call().content(); @@ -59,6 +60,7 @@ public class FunctionCallbackInPrompt2IT { .user("What's the weather like in San Francisco, Tokyo, and Paris?") .function("CurrentWeatherService", "Get the weather in location", new MockWeatherService()) .call().content(); + // @formatter:on logger.info("Response: {}", content); @@ -74,6 +76,7 @@ public class FunctionCallbackInPrompt2IT { OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class); + // @formatter:off String content = ChatClient.builder(caller).build().prompt() .user("What's the weather like in Amsterdam?") .function("CurrentWeatherService", "Get the weather in location", @@ -84,7 +87,7 @@ public class FunctionCallbackInPrompt2IT { } }) .call().content(); - + // @formatter:on logger.info("Response: {}", content); assertThat(content).contains("18"); @@ -98,11 +101,13 @@ public class FunctionCallbackInPrompt2IT { OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class); + // @formatter:off String content = ChatClient.builder(caller).build().prompt() .user("What's the weather like in San Francisco, Tokyo, and Paris?") .function("CurrentWeatherService", "Get the weather in location", new MockWeatherService()) .stream().content() .collectList().block().stream().collect(Collectors.joining()); + // @formatter:on logger.info("Response: {}", content); diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java index 47fc147c1..1608ca599 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWrapper2IT.java @@ -53,6 +53,7 @@ public class FunctionCallbackWrapper2IT { OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class); + // @formatter:off ChatClient chatClient = ChatClient.builder(caller) .defaultFunctions("WeatherInfo") .defaultUser(u -> u.text("What's the weather like in {cities}?")) @@ -61,6 +62,7 @@ public class FunctionCallbackWrapper2IT { String content = chatClient.prompt() .user(u -> u.param("cities", "San Francisco, Tokyo, Paris")) .call().content(); + // @formatter:on logger.info("Response: {}", content); @@ -76,11 +78,13 @@ public class FunctionCallbackWrapper2IT { OpenAiModelCaller caller = context.getBean(OpenAiModelCaller.class); + // @formatter:off String content = ChatClient.builder(caller).build().prompt() .functions("WeatherInfo") .user(u -> u.text("What's the weather like in San Francisco, Tokyo, and Paris?")) .stream().content() .collectList().block().stream().collect(Collectors.joining()); + // @formatter:on logger.info("Response: {}", content);