From d9c40652e9054b3558ca6f719bef677aaab5edf0 Mon Sep 17 00:00:00 2001 From: Ilayaperumal Gopinathan Date: Sun, 16 Mar 2025 10:28:48 +0000 Subject: [PATCH] Refactor auto-configurations - Split model autoconfigurations based on the model - Change the autoconfiguration class into model specific autoconfigurations - chat, embedding, image etc., - Update/add tests based on this change - Make sure the conditional logic to enable the model auto configuration is at the class level so that the configuration properties as well as the models are not enabled when the model is explicitly disabled. By default, the condition will allow enabling the beans if not explicitly overridden. - Remove spring-ai-spring-boot-autoconfigure as a dedicated auto-configuration module Signed-off-by: Ilayaperumal Gopinathan --- .../pom.xml | 3 - ...va => AnthropicChatAutoConfiguration.java} | 6 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- ... => AnthropicChatAutoConfigurationIT.java} | 6 +- .../AnthropicModelConfigurationTests.java | 6 +- .../AnthropicPropertiesTests.java | 10 +- .../tool/FunctionCallWithFunctionBeanIT.java | 5 +- .../FunctionCallWithPromptFunctionIT.java | 4 +- .../pom.xml | 5 - ...AiAudioTranscriptionAutoConfiguration.java | 56 ++ .../AzureOpenAiAutoConfiguration.java | 195 ----- .../AzureOpenAiChatAutoConfiguration.java | 64 +- ...eOpenAiClientBuilderAutoConfiguration.java | 90 +-- ...AzureOpenAiEmbeddingAutoConfiguration.java | 48 +- .../AzureOpenAiImageAutoConfiguration.java | 56 ++ ...ot.autoconfigure.AutoConfiguration.imports | 5 +- .../AzureOpenAiAutoConfigurationIT.java | 149 ++-- ...eOpenAiAutoConfigurationPropertyTests.java | 9 +- ...OpenAiDirectOpenAiAutoConfigurationIT.java | 5 +- .../AzureOpenAiModelConfigurationTests.java | 155 ++-- .../tool/FunctionCallWithFunctionBeanIT.java | 4 +- .../FunctionCallWithFunctionWrapperIT.java | 4 +- .../FunctionCallWithPromptFunctionIT.java | 4 +- .../pom.xml | 5 - .../BedrockConverseProxyChatProperties.java | 64 -- .../BedrockTitanEmbeddingProperties.java | 78 -- ...drockCohereEmbeddingAutoConfiguration.java | 2 +- .../BedrockCohereEmbeddingProperties.java | 2 +- ...ockConverseProxyChatAutoConfiguration.java | 2 +- .../BedrockConverseProxyChatProperties.java | 2 +- ...edrockTitanEmbeddingAutoConfiguration.java | 2 +- .../BedrockTitanEmbeddingProperties.java | 2 +- ...ot.autoconfigure.AutoConfiguration.imports | 6 +- .../tool/FunctionCallWithFunctionBeanIT.java | 130 ---- .../converse/tool/MockWeatherService.java | 95 --- ...ockCohereEmbeddingAutoConfigurationIT.java | 2 +- .../BedrockCohereModelConfigurationTests.java | 5 +- ...edrockConverseModelConfigurationTests.java | 7 +- ...kConverseProxyChatAutoConfigurationIT.java | 2 +- ...drockConverseProxyChatPropertiesTests.java | 2 +- .../tool/FunctionCallWithFunctionBeanIT.java | 4 +- .../FunctionCallWithPromptFunctionIT.java | 4 +- .../tool/MockWeatherService.java | 2 +- ...rockTitanEmbeddingAutoConfigurationIT.java | 2 +- .../BedrockTitanModelConfigurationTests.java | 5 +- .../pom.xml | 1 - .../HuggingfaceChatAutoConfiguration.java | 4 +- .../HuggingfaceModelConfigurationTests.java | 2 +- .../pom.xml | 4 - ...java => MiniMaxChatAutoConfiguration.java} | 45 +- .../MiniMaxEmbeddingAutoConfiguration.java | 54 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../FunctionCallbackInPromptIT.java | 4 +- ...nctionCallbackWithPlainFunctionBeanIT.java | 2 +- .../MiniMaxAutoConfigurationIT.java | 69 +- .../MiniMaxFunctionCallbackIT.java | 4 +- .../autoconfigure/MiniMaxPropertiesTests.java | 31 +- .../MinimaxModelConfigurationTests.java | 48 +- .../pom.xml | 5 - ...va => MistralAiChatAutoConfiguration.java} | 47 +- .../MistralAiEmbeddingAutoConfiguration.java | 64 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../MistralAiAutoConfigurationIT.java | 63 +- .../MistralAiPropertiesTests.java | 8 +- .../MistralModelConfigurationTests.java | 67 +- .../tool/PaymentStatusBeanIT.java | 4 +- .../tool/PaymentStatusPromptIT.java | 4 +- .../tool/WeatherServicePromptIT.java | 8 +- .../pom.xml | 3 - ...ava => MoonshotChatAutoConfiguration.java} | 10 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../MoonshotAutoConfigurationIT.java | 77 -- .../MoonshotChatAutoConfigurationIT.java | 6 +- .../MoonshotPropertiesTests.java | 14 +- .../tool/FunctionCallbackInPromptIT.java | 4 +- ...nctionCallbackWithPlainFunctionBeanIT.java | 4 +- .../tool/MoonshotFunctionCallbackIT.java | 4 +- .../pom.xml | 2 - .../OCIGenAiAutoConfiguration.java | 115 --- .../OCIGenAiChatAutoConfiguration.java | 62 ++ .../OCIGenAiEmbeddingAutoConfiguration.java | 54 ++ ...GenAiInferenceClientAutoConfiguration.java | 75 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../OCIGenAIAutoConfigurationTest.java | 2 +- .../OCIGenAiAutoConfigurationIT.java | 4 +- .../pom.xml | 10 - .../OllamaApiAutoConfiguration.java | 74 ++ .../OllamaAutoConfiguration.java | 153 ---- .../OllamaChatAutoConfiguration.java | 64 +- .../OllamaEmbeddingAutoConfiguration.java | 79 ++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../OllamaChatAutoConfigurationIT.java | 2 +- .../OllamaChatAutoConfigurationTests.java | 3 +- .../OllamaEmbeddingAutoConfigurationIT.java | 3 +- ...OllamaEmbeddingAutoConfigurationTests.java | 3 +- .../OllamaModelConfigurationTests.java | 49 +- .../tool/FunctionCallbackInPromptIT.java | 6 +- .../tool/OllamaFunctionCallbackIT.java | 6 +- .../tool/OllamaFunctionToolBeanIT.java | 6 +- .../tool/FunctionCallbackContextKotlinIT.kt | 4 +- .../tool/FunctionCallbackKotlinIT.kt | 4 +- .../tool/MockKotlinWeatherService.kt | 1 - .../pom.xml | 20 +- ...AiAudioTranscriptionAutoConfiguration.java | 1 - .../OpenAiChatAutoConfiguration.java | 2 +- .../OpenAiEmbeddingAutoConfiguration.java | 2 +- .../OpenAiImageAutoConfiguration.java | 2 +- .../OpenAiModerationAutoConfiguration.java | 20 +- .../pom.xml | 9 - ...PostgresMlEmbeddingAutoConfiguration.java} | 8 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../PostgresMlAutoConfigurationIT.java | 110 --- ...ostgresMlEmbeddingAutoConfigurationIT.java | 10 +- .../pom.xml | 3 - ...java => QianFanChatAutoConfiguration.java} | 80 +- .../QianFanEmbeddingAutoConfiguration.java | 93 +++ .../QianFanImageAutoConfiguration.java | 87 +++ ...ot.autoconfigure.AutoConfiguration.imports | 4 +- .../QianFanAutoConfigurationIT.java | 51 +- .../autoconfigure/QianFanPropertiesTests.java | 50 +- .../pom.xml | 2 - .../StabilityAiImageAutoConfiguration.java | 7 +- .../StabilityAiImagePropertiesTests.java | 4 +- .../pom.xml | 1 - ...ormersEmbeddingModelAutoConfiguration.java | 4 +- .../pom.xml | 11 - ...iEmbeddingConnectionAutoConfiguration.java | 63 ++ ...iMultiModalEmbeddingAutoConfiguration.java | 61 ++ ...rtexAiTextEmbeddingAutoConfiguration.java} | 53 +- ... VertexAiGeminiChatAutoConfiguration.java} | 11 +- ...ot.autoconfigure.AutoConfiguration.imports | 5 +- ...TextEmbeddingModelAutoConfigurationIT.java | 132 ++-- .../VertexAiGeminiAutoConfigurationIT.java | 73 -- ...VertexAiGeminiChatAutoConfigurationIT.java | 6 +- .../VertexAiModelConfigurationTests.java | 59 ++ .../tool/FunctionCallWithFunctionBeanIT.java | 4 +- .../FunctionCallWithFunctionWrapperIT.java | 4 +- .../FunctionCallWithPromptFunctionIT.java | 4 +- .../pom.xml | 2 - .../WatsonxAiChatAutoConfiguration.java | 26 +- ... WatsonxAiEmbeddingAutoConfiguration.java} | 21 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../WatsonxAiAutoConfigurationTests.java | 4 +- .../pom.xml | 4 - .../ZhiPuAiChatAutoConfiguration.java | 60 +- ...=> ZhiPuAiEmbeddingAutoConfiguration.java} | 79 +- .../ZhiPuAiImageAutoConfiguration.java | 71 ++ ...ot.autoconfigure.AutoConfiguration.imports | 4 +- .../ZhiPuAiAutoConfigurationIT.java | 49 +- .../autoconfigure/ZhiPuAiPropertiesTests.java | 47 +- .../tool/FunctionCallbackInPromptIT.java | 6 +- ...nctionCallbackWithPlainFunctionBeanIT.java | 6 +- .../tool/ZhipuAiFunctionCallbackIT.java | 6 +- .../pom.xml | 1 + .../pom.xml | 72 -- .../ChatObservationAutoConfiguration.java | 156 ---- .../ChatObservationProperties.java | 71 -- .../chat/observation/package-info.java | 25 - ...ot.autoconfigure.AutoConfiguration.imports | 16 - ...ChatObservationAutoConfigurationTests.java | 108 --- .../pom.xml | 66 -- ...EmbeddingObservationAutoConfiguration.java | 49 -- .../embedding/observation/package-info.java | 25 - ...ot.autoconfigure.AutoConfiguration.imports | 16 - ...dingObservationAutoConfigurationTests.java | 50 -- .../pom.xml | 66 -- .../ImageObservationAutoConfiguration.java | 55 -- .../ImageObservationProperties.java | 45 -- .../image/observation/package-info.java | 25 - ...ot.autoconfigure.AutoConfiguration.imports | 16 - ...mageObservationAutoConfigurationTests.java | 49 -- pom.xml | 2 - .../ai/model/SpringAIModels.java | 4 +- spring-ai-integration-tests/pom.xml | 4 +- .../tests/TestcontainersConfiguration.java | 4 +- spring-ai-spring-boot-autoconfigure/pom.xml | 365 ---------- .../ChatObservationAutoConfiguration.java | 156 ---- .../ChatObservationProperties.java | 71 -- .../chat/observation/package-info.java | 25 - ...EmbeddingObservationAutoConfiguration.java | 49 -- .../embedding/observation/package-info.java | 25 - .../ImageObservationAutoConfiguration.java | 55 -- .../ImageObservationProperties.java | 45 -- .../image/observation/package-info.java | 25 - .../AnthropicChatProperties.java | 65 -- .../AnthropicConnectionProperties.java | 86 --- .../AzureOpenAIClientBuilderCustomizer.java | 21 - ...ureOpenAiAudioTranscriptionProperties.java | 57 -- .../AzureOpenAiChatProperties.java | 59 -- .../AzureOpenAiConnectionProperties.java | 81 --- .../AzureOpenAiEmbeddingProperties.java | 68 -- .../AzureOpenAiImageOptionsProperties.java | 58 -- .../BedrockAwsConnectionConfiguration.java | 93 --- .../BedrockAwsConnectionProperties.java | 100 --- ...drockCohereEmbeddingAutoConfiguration.java | 69 -- .../BedrockCohereEmbeddingProperties.java | 78 -- ...edrockTitanEmbeddingAutoConfiguration.java | 69 -- .../ChatClientAutoConfiguration.java | 92 --- .../ChatClientBuilderConfigurer.java | 60 -- .../ChatClientBuilderProperties.java | 71 -- .../CommonChatMemoryProperties.java | 37 - .../CassandraChatMemoryAutoConfiguration.java | 63 -- .../CassandraChatMemoryProperties.java | 94 --- .../ChatObservationAutoConfiguration.java | 156 ---- .../ChatObservationProperties.java | 71 -- .../autoconfigure/package-info.java | 25 - ...EmbeddingObservationAutoConfiguration.java | 49 -- .../autoconfigure/package-info.java | 25 - .../HuggingfaceChatAutoConfiguration.java | 40 - .../HuggingfaceChatProperties.java | 73 -- .../ImageObservationAutoConfiguration.java | 55 -- .../ImageObservationProperties.java | 45 -- .../autoconfigure/package-info.java | 25 - .../autoconfigure/MiniMaxChatProperties.java | 65 -- .../MiniMaxConnectionProperties.java | 32 - .../MiniMaxEmbeddingProperties.java | 71 -- .../MiniMaxParentProperties.java | 44 -- .../MistralAiChatProperties.java | 79 -- .../MistralAiCommonProperties.java | 39 - .../MistralAiEmbeddingProperties.java | 81 --- .../MistralAiParentProperties.java | 47 -- .../autoconfigure/MoonshotChatProperties.java | 66 -- .../MoonshotCommonProperties.java | 37 - .../MoonshotParentProperties.java | 46 -- .../OCICohereChatModelProperties.java | 61 -- .../OCIConnectionProperties.java | 153 ---- .../OCIEmbeddingModelProperties.java | 93 --- .../oci/genai/autoconfigure/ServingMode.java | 38 - .../OllamaAutoConfiguration.java | 151 ---- .../autoconfigure/OllamaChatProperties.java | 68 -- .../OllamaConnectionDetails.java | 30 - .../OllamaConnectionProperties.java | 45 -- .../OllamaEmbeddingProperties.java | 68 -- .../OllamaInitializationProperties.java | 124 ---- .../OpenAiAudioSpeechProperties.java | 75 -- .../OpenAiAudioTranscriptionProperties.java | 63 -- .../OpenAiAutoConfiguration.java | 310 -------- .../autoconfigure/OpenAiChatProperties.java | 71 -- .../OpenAiConnectionProperties.java | 32 - .../OpenAiEmbeddingProperties.java | 77 -- .../autoconfigure/OpenAiImageProperties.java | 64 -- .../OpenAiModerationProperties.java | 48 -- .../autoconfigure/OpenAiParentProperties.java | 67 -- .../PostgresMlAutoConfiguration.java | 51 -- .../PostgresMlEmbeddingProperties.java | 87 --- .../QianFanAutoConfiguration.java | 165 ----- .../autoconfigure/QianFanChatProperties.java | 65 -- .../QianFanConnectionProperties.java | 33 - .../QianFanEmbeddingProperties.java | 71 -- .../autoconfigure/QianFanImageProperties.java | 63 -- .../QianFanParentProperties.java | 54 -- .../StabilityAiConnectionProperties.java | 33 - .../StabilityAiImageAutoConfiguration.java | 72 -- .../StabilityAiImageProperties.java | 63 -- .../StabilityAiParentProperties.java | 47 -- .../ToolCallingAutoConfiguration.java | 87 --- .../TransformersEmbeddingModelProperties.java | 219 ------ .../VertexAiEmbeddingAutoConfiguration.java | 102 --- ...VertexAiEmbeddingConnectionProperties.java | 85 --- ...VertexAiMultimodalEmbeddingProperties.java | 58 -- .../VertexAiTextEmbeddingProperties.java | 59 -- .../VertexAiGeminiAutoConfiguration.java | 120 --- .../gemini/VertexAiGeminiChatProperties.java | 53 -- .../VertexAiGeminiConnectionProperties.java | 122 ---- .../WatsonxAiChatProperties.java | 74 -- .../WatsonxAiConnectionProperties.java | 93 --- .../WatsonxAiEmbeddingProperties.java | 67 -- .../ZhiPuAiAutoConfiguration.java | 147 ---- .../autoconfigure/ZhiPuAiChatProperties.java | 65 -- .../ZhiPuAiConnectionProperties.java | 32 - .../ZhiPuAiEmbeddingProperties.java | 71 -- .../autoconfigure/ZhiPuAiImageProperties.java | 60 -- .../ZhiPuAiParentProperties.java | 44 -- .../SpringAiRetryAutoConfiguration.java | 114 --- .../SpringAiRetryProperties.java | 147 ---- ...itional-spring-configuration-metadata.json | 43 -- ...ot.autoconfigure.AutoConfiguration.imports | 44 -- ...ChatObservationAutoConfigurationTests.java | 108 --- ...dingObservationAutoConfigurationTests.java | 50 -- ...mageObservationAutoConfigurationTests.java | 49 -- .../AnthropicAutoConfigurationIT.java | 94 --- .../AnthropicPropertiesTests.java | 132 ---- .../tool/FunctionCallWithFunctionBeanIT.java | 126 ---- .../FunctionCallWithPromptFunctionIT.java | 76 -- .../AzureOpenAiAutoConfigurationIT.java | 252 ------- ...eOpenAiAutoConfigurationPropertyTests.java | 100 --- ...OpenAiDirectOpenAiAutoConfigurationIT.java | 123 ---- .../tool/DeploymentNameUtil.java | 37 - .../tool/FunctionCallWithFunctionBeanIT.java | 127 ---- .../FunctionCallWithFunctionWrapperIT.java | 91 --- .../FunctionCallWithPromptFunctionIT.java | 79 -- .../tool/MockWeatherService.java | 96 --- .../BedrockAwsConnectionConfigurationIT.java | 132 ---- .../autoconfigure/BedrockTestUtils.java | 54 -- .../autoconfigure/RequiresAwsCredentials.java | 35 - ...ockCohereEmbeddingAutoConfigurationIT.java | 141 ---- ...kConverseProxyChatAutoConfigurationIT.java | 81 --- ...drockConverseProxyChatPropertiesTests.java | 84 --- .../FunctionCallWithPromptFunctionIT.java | 75 -- .../converse/tool/MockWeatherService.java | 95 --- ...rockTitanEmbeddingAutoConfigurationIT.java | 140 ---- .../ChatClientAutoConfigurationIT.java | 121 --- ...ientObservationAutoConfigurationTests.java | 49 -- ...assandraChatMemoryAutoConfigurationIT.java | 122 ---- .../CassandraChatMemoryPropertiesTest.java | 63 -- ...ChatObservationAutoConfigurationTests.java | 108 --- ...dingObservationAutoConfigurationTests.java | 50 -- .../HuggingfaceChatAutoConfigurationIT.java | 84 --- ...mageObservationAutoConfigurationTests.java | 49 -- .../FunctionCallbackInPromptIT.java | 117 --- ...nctionCallbackWithPlainFunctionBeanIT.java | 175 ----- .../MiniMaxAutoConfigurationIT.java | 97 --- .../MiniMaxFunctionCallbackIT.java | 122 ---- .../autoconfigure/MiniMaxPropertiesTests.java | 329 --------- .../autoconfigure/MockWeatherService.java | 97 --- .../MistralAiAutoConfigurationIT.java | 95 --- .../MistralAiPropertiesTests.java | 148 ---- .../tool/PaymentStatusBeanIT.java | 115 --- .../tool/PaymentStatusBeanOpenAiIT.java | 122 ---- .../tool/PaymentStatusPromptIT.java | 96 --- .../tool/WeatherServicePromptIT.java | 148 ---- .../MoonshotPropertiesTests.java | 166 ----- .../tool/FunctionCallbackInPromptIT.java | 119 --- ...nctionCallbackWithPlainFunctionBeanIT.java | 175 ----- .../tool/MockWeatherService.java | 95 --- .../tool/MoonshotFunctionCallbackIT.java | 126 ---- .../OCIGenAIAutoConfigurationTest.java | 93 --- .../OCIGenAiAutoConfigurationIT.java | 90 --- .../ollama/autoconfigure/BaseOllamaIT.java | 115 --- .../OllamaChatAutoConfigurationIT.java | 133 ---- .../OllamaChatAutoConfigurationTests.java | 60 -- .../OllamaEmbeddingAutoConfigurationIT.java | 103 --- ...OllamaEmbeddingAutoConfigurationTests.java | 57 -- .../ollama/autoconfigure/OllamaImage.java | 27 - .../tool/FunctionCallbackInPromptIT.java | 125 ---- .../tool/MockWeatherService.java | 97 --- .../tool/OllamaFunctionCallbackIT.java | 147 ---- .../tool/OllamaFunctionToolBeanIT.java | 195 ----- .../OpenAiAutoConfigurationIT.java | 205 ------ .../autoconfigure/OpenAiPropertiesTests.java | 687 ------------------ .../OpenAiResponseFormatPropertiesTests.java | 357 --------- .../tool/FunctionCallbackInPrompt2IT.java | 156 ---- .../tool/FunctionCallbackInPromptIT.java | 117 --- ...nctionCallbackWithPlainFunctionBeanIT.java | 431 ----------- .../tool/MockWeatherService.java | 97 --- .../tool/OpenAiFunctionCallback2IT.java | 107 --- .../tool/OpenAiFunctionCallbackIT.java | 118 --- .../PostgresMlEmbeddingPropertiesTests.java | 64 -- .../QianFanAutoConfigurationIT.java | 114 --- .../autoconfigure/QianFanPropertiesTests.java | 438 ----------- .../StabilityAiAutoConfigurationIT.java | 63 -- .../StabilityAiImagePropertiesTests.java | 110 --- .../ToolCallingAutoConfigurationTests.java | 193 ----- ...mersEmbeddingModelAutoConfigurationIT.java | 112 --- ...TextEmbeddingModelAutoConfigurationIT.java | 144 ---- .../tool/FunctionCallWithFunctionBeanIT.java | 148 ---- .../FunctionCallWithFunctionWrapperIT.java | 90 --- .../FunctionCallWithPromptFunctionIT.java | 94 --- .../gemini/tool/MockWeatherService.java | 96 --- .../WatsonxAiAutoConfigurationTests.java | 56 -- .../ZhiPuAiAutoConfigurationIT.java | 111 --- .../autoconfigure/ZhiPuAiPropertiesTests.java | 432 ----------- .../tool/FunctionCallbackInPromptIT.java | 120 --- ...nctionCallbackWithPlainFunctionBeanIT.java | 176 ----- .../tool/MockWeatherService.java | 97 --- .../tool/ZhipuAiFunctionCallbackIT.java | 124 ---- .../SpringAiRetryAutoConfigurationIT.java | 45 -- .../SpringAiRetryPropertiesTests.java | 78 -- .../resources/application-test.properties | 10 - .../src/test/resources/speech/jfk.flac | Bin 1152693 -> 0 bytes .../src/test/resources/spring_framework.png | Bin 43647 -> 0 bytes spring-ai-spring-boot-docker-compose/pom.xml | 13 +- .../spring-ai-starter-coherence-store/pom.xml | 6 - spring-ai-spring-boot-testcontainers/pom.xml | 2 +- ...maContainerConnectionDetailsFactoryIT.java | 4 +- 375 files changed, 1769 insertions(+), 22851 deletions(-) rename auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/{AnthropicAutoConfiguration.java => AnthropicChatAutoConfiguration.java} (96%) rename auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/{AnthropicAutoConfigurationIT.java => AnthropicChatAutoConfigurationIT.java} (93%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionAutoConfiguration.java delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java (50%) rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderAutoConfiguration.java (51%) rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingAutoConfiguration.java (50%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageAutoConfiguration.java delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/{autoconfigure/cohere => cohere/autoconfigure}/BedrockCohereEmbeddingAutoConfiguration.java (98%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/{autoconfigure/cohere => cohere/autoconfigure}/BedrockCohereEmbeddingProperties.java (97%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/{autoconfigure/converse => converse/autoconfigure}/BedrockConverseProxyChatAutoConfiguration.java (98%) rename {spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse => auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure}/BedrockConverseProxyChatProperties.java (96%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/{autoconfigure/titan => titan/autoconfigure}/BedrockTitanEmbeddingAutoConfiguration.java (98%) rename {spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan => auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure}/BedrockTitanEmbeddingProperties.java (97%) delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/cohere => cohere/autoconfigure}/BedrockCohereEmbeddingAutoConfigurationIT.java (98%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/cohere => cohere/autoconfigure}/BedrockCohereModelConfigurationTests.java (86%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/converse => converse/autoconfigure}/BedrockConverseModelConfigurationTests.java (89%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/converse => converse/autoconfigure}/BedrockConverseProxyChatAutoConfigurationIT.java (97%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/converse => converse/autoconfigure}/BedrockConverseProxyChatPropertiesTests.java (98%) rename {spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse => auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure}/tool/FunctionCallWithFunctionBeanIT.java (96%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/converse => converse/autoconfigure}/tool/FunctionCallWithPromptFunctionIT.java (93%) rename {spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic => auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse}/autoconfigure/tool/MockWeatherService.java (96%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/titan => titan/autoconfigure}/BedrockTitanEmbeddingAutoConfigurationIT.java (98%) rename auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/{autoconfigure/titan => titan/autoconfigure}/BedrockTitanModelConfigurationTests.java (86%) rename auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/{MiniMaxAutoConfiguration.java => MiniMaxChatAutoConfiguration.java} (73%) rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java (62%) rename auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/{MistralAiAutoConfiguration.java => MistralAiChatAutoConfiguration.java} (73%) rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java (59%) rename auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/{MoonshotAutoConfiguration.java => MoonshotChatAutoConfiguration.java} (93%) delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java => auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfigurationIT.java (92%) delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiChatAutoConfiguration.java create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiEmbeddingAutoConfiguration.java rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java (66%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java (57%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfiguration.java rename {spring-ai-spring-boot-autoconfigure => auto-configurations/models/spring-ai-autoconfigure-model-ollama}/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt (95%) rename {spring-ai-spring-boot-autoconfigure => auto-configurations/models/spring-ai-autoconfigure-model-ollama}/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt (95%) rename {spring-ai-spring-boot-autoconfigure => auto-configurations/models/spring-ai-autoconfigure-model-ollama}/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt (97%) rename auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/{PostgresMlAutoConfiguration.java => PostgresMlEmbeddingAutoConfiguration.java} (89%) delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java => auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfigurationIT.java (91%) rename auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/{QianFanAutoConfiguration.java => QianFanChatAutoConfiguration.java} (54%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingAutoConfiguration.java create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageAutoConfiguration.java create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionAutoConfiguration.java create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultiModalEmbeddingAutoConfiguration.java rename auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/{VertexAiEmbeddingAutoConfiguration.java => VertexAiTextEmbeddingAutoConfiguration.java} (57%) rename auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/{VertexAiGeminiAutoConfiguration.java => VertexAiGeminiChatAutoConfiguration.java} (96%) delete mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java rename spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java => auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfigurationIT.java (93%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiModelConfigurationTests.java rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatAutoConfiguration.java (72%) rename auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/{WatsonxAiAutoConfiguration.java => WatsonxAiEmbeddingAutoConfiguration.java} (78%) rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java => auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java (65%) rename auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/{ZhiPuAiAutoConfiguration.java => ZhiPuAiEmbeddingAutoConfiguration.java} (52%) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/pom.xml delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java delete mode 100644 auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/pom.xml delete mode 100644 auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java delete mode 100644 auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java delete mode 100644 auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/pom.xml delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/pom.xml delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAIClientBuilderCustomizer.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageOptionsProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/resources/application-test.properties delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/resources/speech/jfk.flac delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/resources/spring_framework.png diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml index 890375ca2..4030432cd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml @@ -39,21 +39,18 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java similarity index 96% rename from auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java index eb064bec9..dd4ae062b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java @@ -56,9 +56,11 @@ import org.springframework.web.reactive.function.client.WebClient; ToolCallingAutoConfiguration.class }) @EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class }) @ConditionalOnClass(AnthropicApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC, + matchIfMissing = true) @ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class }) -public class AnthropicAutoConfiguration { +public class AnthropicChatAutoConfiguration { @Bean @ConditionalOnMissingBean @@ -74,8 +76,6 @@ public class AnthropicAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC, - matchIfMissing = true) public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties, RetryTemplate retryTemplate, ToolCallingManager toolCallingManager, ObjectProvider observationRegistry, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 45f613d48..88fef7ef0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration +org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java similarity index 93% rename from auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java index a46ff4022..bed6487cd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java @@ -39,13 +39,13 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*") -public class AnthropicAutoConfigurationIT { +public class AnthropicChatAutoConfigurationIT { - private static final Log logger = LogFactory.getLog(AnthropicAutoConfigurationIT.class); + private static final Log logger = LogFactory.getLog(AnthropicChatAutoConfigurationIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class)); @Test void call() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java index 387556f6f..d9e6d3b5c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java @@ -25,7 +25,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for {@link AnthropicAutoConfiguration}'s conditional enabling of models. + * Unit Tests for {@link AnthropicChatAutoConfiguration}'s conditional enabling of models. * * @author Ilayaperumal Gopinathan */ @@ -33,7 +33,7 @@ public class AnthropicModelConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class)); @Test void chatModelActivation() { @@ -42,7 +42,7 @@ public class AnthropicModelConfigurationTests { }); this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { - assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty(); }); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java index 186437c48..9edaf5b2c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java @@ -44,7 +44,7 @@ public class AnthropicPropertiesTests { "spring.ai.anthropic.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) + RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(AnthropicChatProperties.class); var connectionProperties = context.getBean(AnthropicConnectionProperties.class); @@ -80,7 +80,7 @@ public class AnthropicPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) + RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(AnthropicChatProperties.class); var connectionProperties = context.getBean(AnthropicConnectionProperties.class); @@ -104,7 +104,7 @@ public class AnthropicPropertiesTests { // It is enabled by default new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) + RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); @@ -113,7 +113,7 @@ public class AnthropicPropertiesTests { // Explicitly enable the chat auto-configuration. new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=true") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) + RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); @@ -122,7 +122,7 @@ public class AnthropicPropertiesTests { // Explicitly disable the chat auto-configuration. new ApplicationContextRunner().withPropertyValues("spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) + RestClientAutoConfiguration.class, AnthropicChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty(); }); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java index 740cf066a..5a3bdc115 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java @@ -27,13 +27,12 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.anthropic.AnthropicChatModel; import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration; +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Request; import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Response; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.function.FunctionCallingOptions; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,7 +49,7 @@ class FunctionCallWithFunctionBeanIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java index a8fd3cc8c..b7e55c4d0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java @@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.anthropic.AnthropicChatModel; import org.springframework.ai.anthropic.AnthropicChatOptions; import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration; +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -43,7 +43,7 @@ public class FunctionCallWithPromptFunctionIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AnthropicChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml index 595864404..e324901bd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml @@ -39,35 +39,30 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-image-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionAutoConfiguration.java new file mode 100644 index 000000000..0e853fdae --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.model.azure.openai.autoconfigure; + +import com.azure.ai.openai.OpenAIClientBuilder; + +import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; +import org.springframework.ai.azure.openai.AzureOpenAiChatModel; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * {@link AutoConfiguration Auto-configuration} for Azure OpenAI. + * + * @author Piotr Olaszewski + * @author Soby Chacko + * @author Manuel Andreo Garcia + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration +@ConditionalOnClass({ AzureOpenAiAudioTranscriptionModel.class }) +@EnableConfigurationProperties(AzureOpenAiAudioTranscriptionProperties.class) +@ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL, + havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true) +@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class) +public class AzureOpenAiAudioTranscriptionAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient, + AzureOpenAiAudioTranscriptionProperties audioProperties) { + return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions()); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java deleted file mode 100644 index c442995b1..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import com.azure.ai.openai.OpenAIClientBuilder; -import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.credential.KeyCredential; -import com.azure.core.credential.TokenCredential; -import com.azure.core.util.ClientOptions; -import com.azure.core.util.Header; -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.model.SpringAIModelProperties; -import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; -import org.springframework.ai.azure.openai.AzureOpenAiImageModel; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * {@link AutoConfiguration Auto-configuration} for Azure OpenAI. - * - * @author Piotr Olaszewski - * @author Soby Chacko - * @author Manuel Andreo Garcia - * @author Ilayaperumal Gopinathan - */ -@AutoConfiguration(after = { ToolCallingAutoConfiguration.class }) -@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class }) -@EnableConfigurationProperties({ AzureOpenAiChatProperties.class, AzureOpenAiEmbeddingProperties.class, - AzureOpenAiConnectionProperties.class, AzureOpenAiImageOptionsProperties.class, - AzureOpenAiAudioTranscriptionProperties.class }) -@ImportAutoConfiguration(classes = { ToolCallingAutoConfiguration.class }) -public class AzureOpenAiAutoConfiguration { - - private static final String APPLICATION_ID = "spring-ai"; - - @Bean - @ConditionalOnMissingBean // ({ OpenAIClient.class, TokenCredential.class }) - public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties connectionProperties, - ObjectProvider customizers) { - - if (StringUtils.hasText(connectionProperties.getApiKey())) { - - Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty"); - - Map customHeaders = connectionProperties.getCustomHeaders(); - List
headers = customHeaders.entrySet() - .stream() - .map(entry -> new Header(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - ClientOptions clientOptions = new ClientOptions().setApplicationId(APPLICATION_ID).setHeaders(headers); - OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint()) - .credential(new AzureKeyCredential(connectionProperties.getApiKey())) - .clientOptions(clientOptions); - applyOpenAIClientBuilderCustomizers(clientBuilder, customizers); - return clientBuilder; - } - - // Connect to OpenAI (e.g. not the Azure OpenAI). The deploymentName property is - // used as OpenAI model name. - if (StringUtils.hasText(connectionProperties.getOpenAiApiKey())) { - OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint("https://api.openai.com/v1") - .credential(new KeyCredential(connectionProperties.getOpenAiApiKey())) - .clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID)); - applyOpenAIClientBuilderCustomizers(clientBuilder, customizers); - return clientBuilder; - } - - throw new IllegalArgumentException("Either API key or OpenAI API key must not be empty"); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(TokenCredential.class) - public OpenAIClientBuilder openAIClientWithTokenCredential(AzureOpenAiConnectionProperties connectionProperties, - TokenCredential tokenCredential, ObjectProvider customizers) { - - Assert.notNull(tokenCredential, "TokenCredential must not be null"); - Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty"); - - OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint()) - .credential(tokenCredential) - .clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID)); - applyOpenAIClientBuilderCustomizers(clientBuilder, customizers); - return clientBuilder; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, - matchIfMissing = true) - public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, - AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var chatModel = AzureOpenAiChatModel.builder() - .openAIClientBuilder(openAIClientBuilder) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, - matchIfMissing = true) - public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient, - AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(), - embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, - matchIfMissing = true) - public AzureOpenAiImageModel azureOpenAiImageModel(OpenAIClientBuilder openAIClientBuilder, - AzureOpenAiImageOptionsProperties imageProperties) { - - return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL, - havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true) - public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient, - AzureOpenAiAudioTranscriptionProperties audioProperties) { - return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions()); - } - - private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder, - ObjectProvider customizers) { - customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder)); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java similarity index 50% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java index 0a9591ed2..ce067c58e 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * 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. @@ -14,73 +14,59 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.azure.openai.autoconfigure; +import com.azure.ai.openai.OpenAIClientBuilder; import io.micrometer.observation.ObservationRegistry; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.regions.providers.AwsRegionProvider; -import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient; -import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; +import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; /** - * {@link AutoConfiguration Auto-configuration} for Bedrock Converse Proxy Chat Client. + * {@link AutoConfiguration Auto-configuration} for Azure OpenAI. * - * Leverages the Spring Cloud AWS to resolve the {@link AwsCredentialsProvider}. - * - * @author Christian Tzolov - * @author Wei Jiang + * @author Piotr Olaszewski + * @author Soby Chacko + * @author Manuel Andreo Garcia + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { ToolCallingAutoConfiguration.class }) -@EnableConfigurationProperties({ BedrockConverseProxyChatProperties.class, BedrockAwsConnectionConfiguration.class }) -@ConditionalOnClass({ BedrockProxyChatModel.class, BedrockRuntimeClient.class, BedrockRuntimeAsyncClient.class }) -@ConditionalOnProperty(prefix = BedrockConverseProxyChatProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) -@Import(BedrockAwsConnectionConfiguration.class) -@ImportAutoConfiguration({ ToolCallingAutoConfiguration.class }) -public class BedrockConverseProxyChatAutoConfiguration { +@ConditionalOnClass({ AzureOpenAiChatModel.class }) +@EnableConfigurationProperties({ AzureOpenAiChatProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, + matchIfMissing = true) +@ImportAutoConfiguration( + classes = { AzureOpenAiClientBuilderAutoConfiguration.class, ToolCallingAutoConfiguration.class }) +public class AzureOpenAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) - public BedrockProxyChatModel bedrockProxyChatModel(AwsCredentialsProvider credentialsProvider, - AwsRegionProvider regionProvider, BedrockAwsConnectionProperties connectionProperties, - BedrockConverseProxyChatProperties chatProperties, ToolCallingManager toolCallingManager, + public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, + AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager, ObjectProvider observationRegistry, - ObjectProvider observationConvention, - ObjectProvider bedrockRuntimeClient, - ObjectProvider bedrockRuntimeAsyncClient) { + ObjectProvider observationConvention) { - var chatModel = BedrockProxyChatModel.builder() - .credentialsProvider(credentialsProvider) - .region(regionProvider.getRegion()) - .timeout(connectionProperties.getTimeout()) + var chatModel = AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAIClientBuilder) .defaultOptions(chatProperties.getOptions()) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) .toolCallingManager(toolCallingManager) - .bedrockRuntimeClient(bedrockRuntimeClient.getIfAvailable()) - .bedrockRuntimeAsyncClient(bedrockRuntimeAsyncClient.getIfAvailable()) + .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) .build(); - observationConvention.ifAvailable(chatModel::setObservationConvention); return chatModel; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderAutoConfiguration.java similarity index 51% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderAutoConfiguration.java index f01962f43..4dafd1ced 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderAutoConfiguration.java @@ -26,27 +26,13 @@ import com.azure.core.credential.KeyCredential; import com.azure.core.credential.TokenCredential; import com.azure.core.util.ClientOptions; import com.azure.core.util.Header; -import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; -import org.springframework.ai.azure.openai.AzureOpenAiImageModel; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -59,13 +45,10 @@ import org.springframework.util.StringUtils; * @author Manuel Andreo Garcia * @author Ilayaperumal Gopinathan */ -@AutoConfiguration(after = { ToolCallingAutoConfiguration.class }) -@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class }) -@EnableConfigurationProperties({ AzureOpenAiChatProperties.class, AzureOpenAiEmbeddingProperties.class, - AzureOpenAiConnectionProperties.class, AzureOpenAiImageOptionsProperties.class, - AzureOpenAiAudioTranscriptionProperties.class }) -@ImportAutoConfiguration(classes = { ToolCallingAutoConfiguration.class }) -public class AzureOpenAiAutoConfiguration { +@AutoConfiguration +@ConditionalOnClass({ OpenAIClientBuilder.class }) +@EnableConfigurationProperties(AzureOpenAiConnectionProperties.class) +public class AzureOpenAiClientBuilderAutoConfiguration { private static final String APPLICATION_ID = "spring-ai"; @@ -120,71 +103,6 @@ public class AzureOpenAiAutoConfiguration { return clientBuilder; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, - AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var chatModel = AzureOpenAiChatModel.builder() - .openAIClientBuilder(openAIClientBuilder) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient, - AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(), - embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiImageOptionsProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public AzureOpenAiImageModel azureOpenAiImageClient(OpenAIClientBuilder openAIClientBuilder, - AzureOpenAiImageOptionsProperties imageProperties) { - - return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient, - AzureOpenAiAudioTranscriptionProperties audioProperties) { - return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions()); - } - private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder, ObjectProvider customizers) { customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder)); diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingAutoConfiguration.java similarity index 50% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingAutoConfiguration.java index 495428766..13dc388bd 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingAutoConfiguration.java @@ -14,16 +14,18 @@ * limitations under the License. */ -package org.springframework.ai.model.transformers.autoconfigure; +package org.springframework.ai.model.azure.openai.autoconfigure; -import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer; -import ai.onnxruntime.OrtSession; +import com.azure.ai.openai.OpenAIClientBuilder; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.transformers.TransformersEmbeddingModel; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -31,41 +33,35 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; /** - * {@link AutoConfiguration Auto-configuration} for Transformers Embedding Model. + * {@link AutoConfiguration Auto-configuration} for Azure OpenAI. * - * @author Christian Tzolov + * @author Piotr Olaszewski + * @author Soby Chacko + * @author Manuel Andreo Garcia + * @author Ilayaperumal Gopinathan */ @AutoConfiguration -@EnableConfigurationProperties({ TransformersEmbeddingModelProperties.class }) -@ConditionalOnClass({ OrtSession.class, HuggingFaceTokenizer.class, TransformersEmbeddingModel.class }) -public class TransformersEmbeddingModelAutoConfiguration { +@ConditionalOnClass({ AzureOpenAiEmbeddingModel.class }) +@EnableConfigurationProperties({ AzureOpenAiEmbeddingProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, + matchIfMissing = true) +@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class) +public class AzureOpenAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = TransformersEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public TransformersEmbeddingModel embeddingModel(TransformersEmbeddingModelProperties properties, - ObjectProvider observationRegistry, + public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient, + AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider observationRegistry, ObjectProvider observationConvention) { - TransformersEmbeddingModel embeddingModel = new TransformersEmbeddingModel(properties.getMetadataMode(), + var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(), + embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - embeddingModel.setDisableCaching(!properties.getCache().isEnabled()); - embeddingModel.setResourceCacheDirectory(properties.getCache().getDirectory()); - - embeddingModel.setTokenizerResource(properties.getTokenizer().getUri()); - embeddingModel.setTokenizerOptions(properties.getTokenizer().getOptions()); - - embeddingModel.setModelResource(properties.getOnnx().getModelUri()); - - embeddingModel.setGpuDeviceId(properties.getOnnx().getGpuDeviceId()); - - embeddingModel.setModelOutputName(properties.getOnnx().getModelOutputName()); - observationConvention.ifAvailable(embeddingModel::setObservationConvention); return embeddingModel; + } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageAutoConfiguration.java new file mode 100644 index 000000000..df74ab365 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.model.azure.openai.autoconfigure; + +import com.azure.ai.openai.OpenAIClientBuilder; + +import org.springframework.ai.azure.openai.AzureOpenAiImageModel; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * {@link AutoConfiguration Auto-configuration} for Azure OpenAI. + * + * @author Piotr Olaszewski + * @author Soby Chacko + * @author Manuel Andreo Garcia + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration +@ConditionalOnClass(AzureOpenAiImageModel.class) +@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties(AzureOpenAiImageOptionsProperties.class) +@ImportAutoConfiguration(classes = AzureOpenAiClientBuilderAutoConfiguration.class) +public class AzureOpenAiImageAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public AzureOpenAiImageModel azureOpenAiImageModel(OpenAIClientBuilder openAIClientBuilder, + AzureOpenAiImageOptionsProperties imageProperties) { + + return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions()); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 130083802..c16af6362 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration +org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration +org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration +org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiImageAutoConfiguration +org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAudioTranscriptionAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java index 4cd87697c..6dd72c57a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java @@ -35,11 +35,8 @@ import com.azure.core.http.HttpResponse; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAIClientBuilderCustomizer; - import reactor.core.publisher.Flux; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; @@ -86,7 +83,7 @@ class AzureOpenAiAutoConfigurationIT { "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME, "spring.ai.azure.openai.audio.transcription.options.deployment-name=" + System.getenv("AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME") // @formatter:on - ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); + ); private final Message systemMessage = new SystemPromptTemplate(""" You are a helpful AI assistant. Your name is {name}. @@ -100,16 +97,17 @@ class AzureOpenAiAutoConfigurationIT { @Test void chatCompletion() { - this.contextRunner.run(context -> { - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage))); - assertThat(response.getResult().getOutput().getText()).contains("Blackbeard"); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .run(context -> { + AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); + ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage))); + assertThat(response.getResult().getOutput().getText()).contains("Blackbeard"); + }); } @Test void httpRequestContainsUserAgentAndCustomHeaders() { - this.contextRunner + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) .withPropertyValues("spring.ai.azure.openai.custom-headers.foo=bar", "spring.ai.azure.openai.custom-headers.fizz=buzz") .run(context -> { @@ -136,100 +134,137 @@ class AzureOpenAiAutoConfigurationIT { @Test void chatCompletionStreaming() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .run(context -> { - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); + AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - Flux response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage))); + Flux response = chatModel + .stream(new Prompt(List.of(this.userMessage, this.systemMessage))); - List responses = response.collectList().block(); - assertThat(responses.size()).isGreaterThan(10); + List responses = response.collectList().block(); + assertThat(responses.size()).isGreaterThan(10); - String stitchedResponseContent = responses.stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); + String stitchedResponseContent = responses.stream() + .map(ChatResponse::getResults) + .flatMap(List::stream) + .map(Generation::getOutput) + .map(AssistantMessage::getText) + .collect(Collectors.joining()); - assertThat(stitchedResponseContent).contains("Blackbeard"); - }); + assertThat(stitchedResponseContent).contains("Blackbeard"); + }); } @Test void embedding() { - this.contextRunner.run(context -> { - AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + EmbeddingResponse embeddingResponse = embeddingModel + .embedForResponse(List.of("Hello World", "World is big and salvation is near")); + assertThat(embeddingResponse.getResults()).hasSize(2); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); + assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + + assertThat(embeddingModel.dimensions()).isEqualTo(1536); + }); - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); } @Test @EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME", matches = ".+") void transcribe() { - this.contextRunner.run(context -> { - AzureOpenAiAudioTranscriptionModel transcriptionModel = context - .getBean(AzureOpenAiAudioTranscriptionModel.class); - Resource audioFile = new ClassPathResource("/speech/jfk.flac"); - String response = transcriptionModel.call(audioFile); - assertThat(response).isEqualTo( - "And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country."); - }); + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .run(context -> { + AzureOpenAiAudioTranscriptionModel transcriptionModel = context + .getBean(AzureOpenAiAudioTranscriptionModel.class); + Resource audioFile = new ClassPathResource("/speech/jfk.flac"); + String response = transcriptionModel.call(audioFile); + assertThat(response).isEqualTo( + "And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country."); + }); } @Test void chatActivation() { // Disable the chat auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + }); // The chat auto-configuration is enabled by default. - this.contextRunner.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); + }); // Explicitly enable the chat auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=true") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); + }); } @Test void embeddingActivation() { // Disable the embedding auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isEmpty(); + }); // The embedding auto-configuration is enabled by default. - this.contextRunner - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); + }); // Explicitly enable the embedding auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=true") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty()); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); + }); } @Test void audioTranscriptionActivation() { // Disable the transcription auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty()); + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isEmpty(); + }); // The transcription auto-configuration is enabled by default. this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty()); // Explicitly enable the transcription auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=true") + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=azure-openai") .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty()); } @@ -237,7 +272,7 @@ class AzureOpenAiAutoConfigurationIT { void openAIClientBuilderCustomizer() { AtomicBoolean firstCustomizationApplied = new AtomicBoolean(false); AtomicBoolean secondCustomizationApplied = new AtomicBoolean(false); - this.contextRunner + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) .withBean("first", AzureOpenAIClientBuilderCustomizer.class, () -> clientBuilder -> firstCustomizationApplied.set(true)) .withBean("second", AzureOpenAIClientBuilderCustomizer.class, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java index df965f34e..f166bf284 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java @@ -18,10 +18,6 @@ package org.springframework.ai.model.azure.openai.autoconfigure; import org.junit.jupiter.api.Test; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiConnectionProperties; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -40,7 +36,7 @@ public class AzureOpenAiAutoConfigurationPropertyTests { .withPropertyValues("spring.ai.azure.openai.api-key=TEST_API_KEY", "spring.ai.azure.openai.endpoint=TEST_ENDPOINT", "spring.ai.azure.openai.embedding.options.deployment-name=MODEL_XYZ") - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(AzureOpenAiEmbeddingProperties.class); var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class); @@ -72,7 +68,8 @@ public class AzureOpenAiAutoConfigurationPropertyTests { "spring.ai.azure.openai.chat.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class, + AzureOpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(AzureOpenAiChatProperties.class); var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java index 99c4ed05c..a3dff4617 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import reactor.core.publisher.Flux; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.messages.AssistantMessage; @@ -60,7 +59,9 @@ public class AzureOpenAiDirectOpenAiAutoConfigurationIT { "spring.ai.azure.openai.chat.options.maxTokens=123", "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME // @formatter:on - ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); + ) + .withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class, + AzureOpenAiEmbeddingAutoConfiguration.class)); private final Message systemMessage = new SystemPromptTemplate(""" You are a helpful AI assistant. Your name is {name}. diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java index 19a2ce674..7c3a933f2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java @@ -29,38 +29,42 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for {@link AzureOpenAiAutoConfiguration}'s conditional enabling of models. + * Unit Tests for Azure OpenAI auto-configurations conditional enabling of models. * * @author Ilayaperumal Gopinathan */ @EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") public class AzureOpenAiModelConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"), - "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( + "spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"), + "spring.ai.openai.base-url=TEST_BASE_URL"); @Test void chatModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=azure-openai").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + }); - this.contextRunner + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) .withPropertyValues("spring.ai.model.chat=azure-openai", "spring.ai.model.embedding=none", "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") @@ -74,24 +78,29 @@ public class AzureOpenAiModelConfigurationTests { @Test void embeddingModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=azure-openai").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + }); - this.contextRunner + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiEmbeddingAutoConfiguration.class)) .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=azure-openai", "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") @@ -105,24 +114,29 @@ public class AzureOpenAiModelConfigurationTests { @Test void imageModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.image=none").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.image=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.image=azure-openai").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.image=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + }); - this.contextRunner + this.contextRunner.withConfiguration(AutoConfigurations.of(AzureOpenAiImageAutoConfiguration.class)) .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", "spring.ai.model.image=azure-openai", "spring.ai.model.audio.speech=none", "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") @@ -136,24 +150,33 @@ public class AzureOpenAiModelConfigurationTests { @Test void audioTranscriptionModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=none").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=azure-openai").run(context -> { - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=azure-openai") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration(AutoConfigurations.of(AzureOpenAiAudioTranscriptionAutoConfiguration.class)) .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", "spring.ai.model.audio.transcription=azure-openai", "spring.ai.model.moderation=none") diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java index 5f560d174..e4a17e2a7 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java @@ -24,13 +24,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -51,7 +51,7 @@ class FunctionCallWithFunctionBeanIT { "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java index b809da5dd..c8c61ae15 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java @@ -23,12 +23,12 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -49,7 +49,7 @@ public class FunctionCallWithFunctionWrapperIT { "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java index 90e5d7a9b..8f2d63b8e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java @@ -23,12 +23,12 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -46,7 +46,7 @@ public class FunctionCallWithPromptFunctionIT { "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml index 7c0112362..11d42f694 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml @@ -46,35 +46,30 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-image-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java deleted file mode 100644 index 0428a9c90..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java +++ /dev/null @@ -1,64 +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.model.bedrock.autoconfigure.converse; - -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.util.Assert; - -/** - * Configuration properties for Bedrock Converse. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@ConfigurationProperties(BedrockConverseProxyChatProperties.CONFIG_PREFIX) -public class BedrockConverseProxyChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.bedrock.converse.chat"; - - /** - * Enable Bedrock Converse chat model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private ToolCallingChatOptions options = ToolCallingChatOptions.builder() - .temperature(0.7) - .maxTokens(300) - .topK(10) - .build(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public ToolCallingChatOptions getOptions() { - return this.options; - } - - public void setOptions(ToolCallingChatOptions options) { - Assert.notNull(options, "FunctionCallingOptions must not be null"); - this.options = options; - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java deleted file mode 100644 index 48c452ea4..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.titan; - -import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType; -import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Bedrock Titan Embedding autoconfiguration properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(BedrockTitanEmbeddingProperties.CONFIG_PREFIX) -public class BedrockTitanEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.bedrock.titan.embedding"; - - /** - * Enable Bedrock Titan Embedding Model. False by default. - */ - private boolean enabled = false; - - /** - * Bedrock Titan Embedding generative name. Defaults to 'amazon.titan-embed-image-v1'. - */ - private String model = TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id(); - - /** - * Titan Embedding API input types. Could be either text or image (encoded in base64). - * Defaults to {@link InputType#IMAGE}. - */ - private InputType inputType = InputType.IMAGE; - - public static String getConfigPrefix() { - return CONFIG_PREFIX; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getModel() { - return this.model; - } - - public void setModel(String model) { - this.model = model; - } - - public InputType getInputType() { - return this.inputType; - } - - public void setInputType(InputType inputType) { - this.inputType = inputType; - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java index 57765ad55..2b07482d4 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.cohere; +package org.springframework.ai.model.bedrock.cohere.autoconfigure; import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingProperties.java similarity index 97% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingProperties.java index 131a9832a..f12412770 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.cohere; +package org.springframework.ai.model.bedrock.cohere.autoconfigure; import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingOptions; import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfiguration.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfiguration.java index e063bc3af..9aed1da08 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.bedrock.converse.autoconfigure; import io.micrometer.observation.ObservationRegistry; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatProperties.java similarity index 96% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatProperties.java index 0428a9c90..b7231551b 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.bedrock.converse.autoconfigure; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java index 474ec9030..efb5c2637 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.titan; +package org.springframework.ai.model.bedrock.titan.autoconfigure; import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingProperties.java similarity index 97% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingProperties.java index 48c452ea4..05ec385c6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.titan; +package org.springframework.ai.model.bedrock.titan.autoconfigure; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType; import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 21b64d10d..be2a4839c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -cohere.org.springframework.ai.model.bedrock.autoconfigure.BedrockCohereEmbeddingAutoConfiguration -titan.org.springframework.ai.model.bedrock.autoconfigure.BedrockTitanEmbeddingAutoConfiguration -converse.org.springframework.ai.model.bedrock.autoconfigure.BedrockConverseProxyChatAutoConfiguration +org.springframework.ai.model.bedrock.cohere.autoconfigure.BedrockCohereEmbeddingAutoConfiguration +org.springframework.ai.model.bedrock.titan.autoconfigure.BedrockTitanEmbeddingAutoConfiguration +org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java deleted file mode 100644 index 4ad9486a4..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2023-2025 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.bedrock.autoconfigure.converse.tool; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; -import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; -import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@RequiresAwsCredentials -class FunctionCallWithFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - - this.contextRunner - .withPropertyValues( - "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0") - .run(context -> { - - BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class); - - var userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel.call(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction3").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void functionStreamTest() { - - this.contextRunner - .withPropertyValues( - "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0") - .run(context -> { - - BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class); - - var userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius."); - - Flux responses = chatModel.stream(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); - - String content = responses.collectList() - .block() - .stream() - .filter(cr -> cr.getResult() != null) - .map(cr -> cr.getResult().getOutput().getText()) - .collect(Collectors.joining()); - - logger.info("Response: {}", content); - assertThat(content).contains("30", "10", "15"); - - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location. Return temperature in 36°F or 36°C format.") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunction3() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java deleted file mode 100644 index 29081ec2f..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.converse.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfigurationIT.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfigurationIT.java index 4e6f0c4eb..127f0b3d2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereEmbeddingAutoConfigurationIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.cohere; +package org.springframework.ai.model.bedrock.cohere.autoconfigure; import java.util.List; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java similarity index 86% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java index 019261105..7690cb9f7 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/cohere/autoconfigure/BedrockCohereModelConfigurationTests.java @@ -14,15 +14,12 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.cohere; +package org.springframework.ai.model.bedrock.cohere.autoconfigure; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; -import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; -import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingAutoConfiguration; -import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseModelConfigurationTests.java similarity index 89% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseModelConfigurationTests.java index 0192e85d9..9abd3712d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseModelConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.bedrock.converse.autoconfigure; import org.junit.jupiter.api.Test; @@ -25,9 +25,8 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for - * {@link org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration}'s - * conditional enabling of models. + * Unit Tests for {@link BedrockConverseProxyChatAutoConfiguration}'s conditional enabling + * of models. * * @author Ilayaperumal Gopinathan */ diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfigurationIT.java similarity index 97% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfigurationIT.java index f89cda503..8f101a357 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfigurationIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.bedrock.converse.autoconfigure; import java.util.List; import java.util.stream.Collectors; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatPropertiesTests.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatPropertiesTests.java index 65913a7f0..73db74a3d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse; +package org.springframework.ai.model.bedrock.converse.autoconfigure; import org.junit.jupiter.api.Test; diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java similarity index 96% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java index 4ad9486a4..ee837f271 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse.tool; +package org.springframework.ai.model.bedrock.converse.autoconfigure.tool; import java.util.List; import java.util.function.Function; @@ -27,7 +27,7 @@ import reactor.core.publisher.Flux; import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; +import org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration; import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java similarity index 93% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java index 95c595370..efff6f331 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.converse.tool; +package org.springframework.ai.model.bedrock.converse.autoconfigure.tool; import java.util.List; @@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; +import org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration; import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/MockWeatherService.java similarity index 96% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/MockWeatherService.java index 645ab7060..3047b7ce4 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/converse/autoconfigure/tool/MockWeatherService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.anthropic.autoconfigure.tool; +package org.springframework.ai.model.bedrock.converse.autoconfigure.tool; import java.util.function.Function; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfigurationIT.java similarity index 98% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfigurationIT.java index 8764167dc..24584382b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanEmbeddingAutoConfigurationIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.titan; +package org.springframework.ai.model.bedrock.titan.autoconfigure; import java.util.Base64; import java.util.List; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java similarity index 86% rename from auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java rename to auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java index 83d194a9e..443ae6d1d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/titan/autoconfigure/BedrockTitanModelConfigurationTests.java @@ -14,15 +14,12 @@ * limitations under the License. */ -package org.springframework.ai.model.bedrock.autoconfigure.titan; +package org.springframework.ai.model.bedrock.titan.autoconfigure; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; -import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatProperties; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml index 09da01d8f..75235f4db 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml @@ -39,7 +39,6 @@ org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java index 9b937deab..8b8526d9c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java @@ -29,12 +29,12 @@ import org.springframework.context.annotation.Bean; @AutoConfiguration @ConditionalOnClass(HuggingfaceChatModel.class) @EnableConfigurationProperties(HuggingfaceChatProperties.class) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE, + matchIfMissing = true) public class HuggingfaceChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE, - matchIfMissing = true) public HuggingfaceChatModel huggingfaceChatModel(HuggingfaceChatProperties huggingfaceChatProperties) { return new HuggingfaceChatModel(huggingfaceChatProperties.getApiKey(), huggingfaceChatProperties.getUrl()); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java index 835036219..7d9a4a8c3 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java @@ -42,7 +42,7 @@ public class HuggingfaceModelConfigurationTests { }); this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { - assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(HuggingfaceChatModel.class)).isEmpty(); }); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml index 0ead4e2ab..6c8a26e97 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml @@ -39,28 +39,24 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java similarity index 73% rename from auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java index f74e3ba21..7406ce43b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java @@ -20,17 +20,15 @@ import java.util.List; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxEmbeddingModel; -import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -47,20 +45,20 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for MiniMax Chat and Embedding Models. + * {@link AutoConfiguration Auto-configuration} for MiniMax Chat Model. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @ConditionalOnClass(MiniMaxApi.class) -@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class, - MiniMaxEmbeddingProperties.class }) -public class MiniMaxAutoConfiguration { +@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX, + matchIfMissing = true) +public class MiniMaxChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX, - matchIfMissing = true) public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties, MiniMaxChatProperties chatProperties, ObjectProvider restClientBuilderProvider, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, @@ -79,29 +77,6 @@ public class MiniMaxAutoConfiguration { return chatModel; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MINIMAX, - matchIfMissing = true) - public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties, - MiniMaxEmbeddingProperties embeddingProperties, - ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var miniMaxApi = miniMaxApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var embeddingModel = new MiniMaxEmbeddingModel(miniMaxApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - private MiniMaxApi miniMaxApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java similarity index 62% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java index caea39372..e7098339c 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java @@ -16,19 +16,14 @@ package org.springframework.ai.model.minimax.autoconfigure; -import java.util.List; - import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -36,7 +31,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -45,42 +39,20 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for MiniMax Chat and Embedding Models. + * {@link AutoConfiguration Auto-configuration} for MiniMax Embedding Model. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @ConditionalOnClass(MiniMaxApi.class) -@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class, - MiniMaxEmbeddingProperties.class }) -public class MiniMaxAutoConfiguration { +@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxEmbeddingProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MINIMAX, + matchIfMissing = true) +public class MiniMaxEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MiniMaxChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties, - MiniMaxChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackResolver, - toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MiniMaxEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties, MiniMaxEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, @@ -112,12 +84,4 @@ public class MiniMaxAutoConfiguration { return new MiniMaxApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1f671df33..edf614f0b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.minimax.autoconfigure.MiniMaxAutoConfiguration +org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration +org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java index f86e76185..c4982ba90 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; @@ -34,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -51,7 +51,7 @@ public class FunctionCallbackInPromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)); + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java index 734a0b8a7..b4e3d6a9b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java @@ -55,7 +55,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .withUserConfiguration(Config.class); // FIXME: multiple function calls may stop prematurely due to model performance diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java index 2b82ba7d4..86f52f0a2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java @@ -47,51 +47,58 @@ public class MiniMaxAutoConfigurationIT { private static final Log logger = LogFactory.getLog(MiniMaxAutoConfigurationIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)); + .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")); @Test void generate() { - this.contextRunner.run(context -> { - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); + this.contextRunner + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) + .run(context -> { + MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); + String response = chatModel.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); } @Test void generateStreaming() { - this.contextRunner.run(context -> { - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); + this.contextRunner + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) + .run(context -> { + MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); + Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); + String response = responseFlux.collectList() + .block() + .stream() + .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) + .collect(Collectors.joining()); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); } @Test void embedding() { - this.contextRunner.run(context -> { - MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class); + this.contextRunner + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) + .run(context -> { + MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + EmbeddingResponse embeddingResponse = embeddingModel + .embedForResponse(List.of("Hello World", "World is big and salvation is near")); + assertThat(embeddingResponse.getResults()).hasSize(2); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); + assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); + assertThat(embeddingModel.dimensions()).isEqualTo(1536); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java index 5bf102959..50c2bee69 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java @@ -25,7 +25,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; @@ -34,6 +33,7 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -53,7 +53,7 @@ public class MiniMaxFunctionCallbackIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java index 962b2e114..f897867cb 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java @@ -50,7 +50,7 @@ public class MiniMaxPropertiesTests { "spring.ai.minimax.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MiniMaxChatProperties.class); var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); @@ -79,7 +79,7 @@ public class MiniMaxPropertiesTests { "spring.ai.minimax.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MiniMaxChatProperties.class); var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); @@ -105,7 +105,7 @@ public class MiniMaxPropertiesTests { "spring.ai.minimax.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); @@ -132,7 +132,7 @@ public class MiniMaxPropertiesTests { "spring.ai.minimax.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); @@ -199,17 +199,14 @@ public class MiniMaxPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MiniMaxChatProperties.class); var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("embo-01"); - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); @@ -248,7 +245,7 @@ public class MiniMaxPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); @@ -267,16 +264,16 @@ public class MiniMaxPropertiesTests { .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isEmpty(); assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); @@ -286,7 +283,7 @@ public class MiniMaxPropertiesTests { .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", "spring.ai.minimax.embedding.enabled=true") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); @@ -299,16 +296,16 @@ public class MiniMaxPropertiesTests { .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); @@ -318,7 +315,7 @@ public class MiniMaxPropertiesTests { .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", "spring.ai.model.chat=minimax") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) + RestClientAutoConfiguration.class, MiniMaxChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java index d1cdd2709..eff69c6c8 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java @@ -27,57 +27,59 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for {@link MiniMaxAutoConfiguration}'s conditional enabling of models. + * Unit Tests for MiniMax auto-configurations' conditional enabling of models. * * @author Ilayaperumal Gopinathan */ public class MinimaxModelConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(MiniMaxAutoConfiguration.class, SpringAiRetryAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class)) .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL"); @Test void chatModelActivation() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class)).run(context -> { assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") .run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=minimax", "spring.ai.model.embedding=none") + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=minimax", "spring.ai.model.embedding=none") .run(context -> { assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); }); } @Test void embeddingModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=minimax").run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MiniMaxEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=minimax") + .run(context -> { + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml index e75110f3f..d62018c33 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml @@ -39,35 +39,30 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-image-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java similarity index 73% rename from auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java index 77bab0242..c67a43856 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java @@ -18,18 +18,16 @@ package org.springframework.ai.model.mistralai.autoconfigure; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.mistralai.MistralAiChatModel; +import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.mistralai.MistralAiChatModel; -import org.springframework.ai.mistralai.MistralAiEmbeddingModel; -import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -47,49 +45,26 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for Mistral AI. + * Chat {@link AutoConfiguration Auto-configuration} for Mistral AI. * * @author Ricken Bazolo * @author Christian Tzolov * @author Thomas Vitale + * @author Ilayaperumal Gopinathan * @since 0.8.1 */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@EnableConfigurationProperties({ MistralAiEmbeddingProperties.class, MistralAiCommonProperties.class, - MistralAiChatProperties.class }) +@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiChatProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL, + matchIfMissing = true) @ConditionalOnClass(MistralAiApi.class) @ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -public class MistralAiAutoConfiguration { +public class MistralAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MISTRAL, - matchIfMissing = true) - public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties, - MistralAiEmbeddingProperties embeddingProperties, - ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var mistralAiApi = mistralAiApi(embeddingProperties.getApiKey(), commonProperties.getApiKey(), - embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var embeddingModel = new MistralAiEmbeddingModel(mistralAiApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL, - matchIfMissing = true) public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties, MistralAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, ToolCallingManager toolCallingManager, RetryTemplate retryTemplate, diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java similarity index 59% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java index b58a75200..db0605dc3 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java @@ -18,16 +18,12 @@ package org.springframework.ai.model.mistralai.autoconfigure; import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.mistralai.MistralAiChatModel; import org.springframework.ai.mistralai.MistralAiEmbeddingModel; import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -36,7 +32,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -45,26 +40,24 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for Mistral AI. + * Embedding {@link AutoConfiguration Auto-configuration} for Mistral AI. * * @author Ricken Bazolo * @author Christian Tzolov * @author Thomas Vitale + * @author Ilayaperumal Gopinathan * @since 0.8.1 */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, - ToolCallingAutoConfiguration.class }) -@EnableConfigurationProperties({ MistralAiEmbeddingProperties.class, MistralAiCommonProperties.class, - MistralAiChatProperties.class }) +@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) +@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiEmbeddingProperties.class }) @ConditionalOnClass(MistralAiApi.class) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - ToolCallingAutoConfiguration.class }) -public class MistralAiAutoConfiguration { +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MISTRAL, + matchIfMissing = true) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class }) +public class MistralAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MistralAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties, MistralAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, @@ -84,33 +77,6 @@ public class MistralAiAutoConfiguration { return embeddingModel; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties, - MistralAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - ToolCallingManager toolCallingManager, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(), - chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var chatModel = MistralAiChatModel.builder() - .mistralAiApi(mistralAiApi) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .retryTemplate(retryTemplate) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { @@ -123,12 +89,4 @@ public class MistralAiAutoConfiguration { return new MistralAiApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6436596f9..167a445dc 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration +org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration +org.springframework.ai.model.mistralai.autoconfigure.MistralAiEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java index b7bb85226..4759d322d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java @@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov + * @author Ilayaperumal Gopinathan * @since 0.8.1 */ @EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") @@ -46,50 +47,52 @@ public class MistralAiAutoConfigurationIT { private static final Log logger = LogFactory.getLog(MistralAiAutoConfigurationIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); + .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")); @Test void generate() { - this.contextRunner.run(context -> { - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)) + .run(context -> { + MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); + String response = chatModel.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); } @Test void generateStreaming() { - this.contextRunner.run(context -> { - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)) + .run(context -> { + MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); + Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); + String response = responseFlux.collectList() + .block() + .stream() + .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) + .collect(Collectors.joining()); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); } @Test void embedding() { - this.contextRunner.run(context -> { - MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class)) + .run(context -> { + MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + EmbeddingResponse embeddingResponse = embeddingModel + .embedForResponse(List.of("Hello World", "World is big and salvation is near")); + assertThat(embeddingResponse.getResults()).hasSize(2); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); + assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); + assertThat(embeddingModel.dimensions()).isEqualTo(1024); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java index dd4eae9ab..eb444a7e9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java @@ -38,7 +38,7 @@ public class MistralAiPropertiesTests { .withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); var connectionProperties = context.getBean(MistralAiCommonProperties.class); @@ -87,7 +87,7 @@ public class MistralAiPropertiesTests { "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2", "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, MistralAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MistralAiChatProperties.class); @@ -108,7 +108,7 @@ public class MistralAiPropertiesTests { "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2", "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); var connectionProperties = context.getBean(MistralAiCommonProperties.class); @@ -132,7 +132,7 @@ public class MistralAiPropertiesTests { "spring.ai.mistralai.embedding.options.model=MODEL_XYZ", "spring.ai.mistralai.embedding.options.encodingFormat=MyEncodingFormat") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, MistralAiEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(MistralAiCommonProperties.class); var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java index fb4d5013d..0f01a27d1 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java @@ -26,57 +26,64 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for {@link MistralAiAutoConfiguration}'s conditional enabling of models. + * Unit Tests for Mistral AI auto-configurations conditional enabling of models. * * @author Ilayaperumal Gopinathan */ public class MistralModelConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)) .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")); @Test void chatModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") - .run(context -> { - assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiChatModel.class)).isEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.model.chat=mistral", "spring.ai.model.embedding=none") + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(MistralAiChatModel.class)).isEmpty(); + }); + + this.contextRunner + .withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class, + MistralAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=mistral", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty(); assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); }); } @Test void embeddingModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { - assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=mistral").run(context -> { - assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(MistralAiEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=mistral") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java index bd7ed874a..bacc8182c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java @@ -26,13 +26,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.mistralai.MistralAiChatModel; import org.springframework.ai.mistralai.MistralAiChatOptions; import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -53,7 +53,7 @@ class PaymentStatusBeanIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java index 01a278a3d..c524dcb85 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java @@ -25,13 +25,13 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.mistralai.MistralAiChatModel; import org.springframework.ai.mistralai.MistralAiChatOptions; import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration; import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -51,7 +51,7 @@ public class PaymentStatusPromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java index 758ab779f..45d40d981 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java @@ -27,9 +27,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; -import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request; -import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -37,6 +34,9 @@ import org.springframework.ai.mistralai.MistralAiChatModel; import org.springframework.ai.mistralai.MistralAiChatOptions; import org.springframework.ai.mistralai.api.MistralAiApi; import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice; +import org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration; +import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request; +import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.tool.function.FunctionToolCallback; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -56,7 +56,7 @@ public class WeatherServicePromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.mistralai.api-key=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(MistralAiChatAutoConfiguration.class)); @Test void promptFunctionCall() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml index 10b41e644..08a9e3d0b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml @@ -39,21 +39,18 @@ org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfiguration.java similarity index 93% rename from auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfiguration.java index d1d836c61..ec7b66317 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfiguration.java @@ -48,19 +48,17 @@ import org.springframework.web.client.RestClient; * {@link AutoConfiguration Auto-configuration} for Moonshot Chat Model. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @EnableConfigurationProperties({ MoonshotCommonProperties.class, MoonshotChatProperties.class }) @ConditionalOnClass(MoonshotApi.class) -public class MoonshotAutoConfiguration { +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT, + matchIfMissing = true) +public class MoonshotChatAutoConfiguration { @Bean @ConditionalOnMissingBean - // @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name = - // "enabled", havingValue = "true", - // matchIfMissing = true) - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT, - matchIfMissing = true) public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties, MoonshotChatProperties chatProperties, ObjectProvider restClientBuilderProvider, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index aa04b8eda..9fa8089d9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration +org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java deleted file mode 100644 index 24f2fced3..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure; - -import java.util.Objects; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*") -public class MoonshotAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(MoonshotAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - MoonshotChatModel client = context.getBean(MoonshotChatModel.class); - String response = client.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - MoonshotChatModel client = context.getBean(MoonshotChatModel.class); - Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); - String response = Objects.requireNonNull(responseFlux.collectList().block()) - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfigurationIT.java similarity index 92% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfigurationIT.java index 24f2fced3..33f29afba 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatAutoConfigurationIT.java @@ -40,14 +40,14 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Geng Rong */ @EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*") -public class MoonshotAutoConfigurationIT { +public class MoonshotChatAutoConfigurationIT { - private static final Log logger = LogFactory.getLog(MoonshotAutoConfigurationIT.class); + private static final Log logger = LogFactory.getLog(MoonshotChatAutoConfigurationIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)); + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)); @Test void generate() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java index 119b0fbfe..bec989217 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java @@ -42,7 +42,7 @@ public class MoonshotPropertiesTests { "spring.ai.moonshot.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MoonshotChatProperties.class); var connectionProperties = context.getBean(MoonshotCommonProperties.class); @@ -71,7 +71,7 @@ public class MoonshotPropertiesTests { "spring.ai.moonshot.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MoonshotChatProperties.class); var connectionProperties = context.getBean(MoonshotCommonProperties.class); @@ -110,7 +110,7 @@ public class MoonshotPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(MoonshotChatProperties.class); var connectionProperties = context.getBean(MoonshotCommonProperties.class); @@ -137,16 +137,16 @@ public class MoonshotPropertiesTests { .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL", "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(MoonshotChatModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty(); @@ -156,7 +156,7 @@ public class MoonshotPropertiesTests { .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL", "spring.ai.model.chat=moonshot") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java index 0dc2271f3..44960f9c5 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java @@ -25,7 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration; +import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; @@ -53,7 +53,7 @@ public class FunctionCallbackInPromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)); + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java index 7ab0093de..a5f0e1e30 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -26,7 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration; +import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; @@ -57,7 +57,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java index 86a85a9e1..c769d5155 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java @@ -26,7 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration; +import org.springframework.ai.model.moonshot.autoconfigure.MoonshotChatAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; @@ -56,7 +56,7 @@ public class MoonshotFunctionCallbackIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) + RestClientAutoConfiguration.class, MoonshotChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml index 4bd179f43..559b83920 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml @@ -46,14 +46,12 @@ org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java deleted file mode 100644 index 777c89718..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import java.io.IOException; - -import com.oracle.bmc.ClientConfiguration; -import com.oracle.bmc.Region; -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider; -import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider; -import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider; -import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider; -import com.oracle.bmc.auth.SimplePrivateKeySupplier; -import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider; -import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient; -import com.oracle.bmc.retrier.RetryConfiguration; -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.model.SpringAIModelProperties; -import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.oci.OCIEmbeddingModel; -import org.springframework.ai.oci.cohere.OCICohereChatModel; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.util.StringUtils; - -/** - * {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure Generative - * AI. - * - * @author Anders Swanson - */ -@AutoConfiguration -@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class }) -@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class, - OCICohereChatModelProperties.class }) -public class OCIGenAiAutoConfiguration { - - private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties) - throws IOException { - return switch (properties.getAuthenticationType()) { - case FILE -> new ConfigFileAuthenticationDetailsProvider(properties.getFile(), properties.getProfile()); - case INSTANCE_PRINCIPAL -> InstancePrincipalsAuthenticationDetailsProvider.builder().build(); - case WORKLOAD_IDENTITY -> OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build(); - case SIMPLE -> SimpleAuthenticationDetailsProvider.builder() - .userId(properties.getUserId()) - .tenantId(properties.getTenantId()) - .fingerprint(properties.getFingerprint()) - .privateKeySupplier(new SimplePrivateKeySupplier(properties.getPrivateKey())) - .passPhrase(properties.getPassPhrase()) - .region(Region.valueOf(properties.getRegion())) - .build(); - }; - } - - @ConditionalOnMissingBean - @Bean - public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties) - throws IOException { - ClientConfiguration clientConfiguration = ClientConfiguration.builder() - .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION) - .build(); - GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder() - .configuration(clientConfiguration); - if (StringUtils.hasText(properties.getRegion())) { - builder.region(Region.valueOf(properties.getRegion())); - } - if (StringUtils.hasText(properties.getEndpoint())) { - builder.endpoint(properties.getEndpoint()); - } - return builder.build(authenticationProvider(properties)); - } - - @Bean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OCI_GENAI, - matchIfMissing = true) - public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient, - OCIEmbeddingModelProperties properties) { - return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions()); - } - - @Bean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI, - matchIfMissing = true) - public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient, - OCICohereChatModelProperties properties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(), - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiChatAutoConfiguration.java new file mode 100644 index 000000000..fe2024ce3 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiChatAutoConfiguration.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023-2025 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.oci.genai.autoconfigure; + +import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.oci.cohere.OCICohereChatModel; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Chat {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure + * Generative AI. + * + * @author Anders Swanson + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration +@ConditionalOnClass(OCICohereChatModel.class) +@EnableConfigurationProperties(OCICohereChatModelProperties.class) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI, + matchIfMissing = true) +@ImportAutoConfiguration(OCIGenAiInferenceClientAutoConfiguration.class) +public class OCIGenAiChatAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient, + OCICohereChatModelProperties properties, ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(), + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + observationConvention.ifAvailable(chatModel::setObservationConvention); + + return chatModel; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiEmbeddingAutoConfiguration.java new file mode 100644 index 000000000..7c9ded2c1 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiEmbeddingAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023-2025 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.oci.genai.autoconfigure; + +import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient; + +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.oci.OCIEmbeddingModel; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Embedding {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure + * Generative AI. + * + * @author Anders Swanson + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration +@ConditionalOnClass(OCIEmbeddingModel.class) +@EnableConfigurationProperties(OCIEmbeddingModelProperties.class) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OCI_GENAI, + matchIfMissing = true) +@ImportAutoConfiguration(OCIGenAiInferenceClientAutoConfiguration.class) +public class OCIGenAiEmbeddingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient, + OCIEmbeddingModelProperties properties) { + return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions()); + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java similarity index 66% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java index 84d872137..9188756f5 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -28,31 +28,43 @@ import com.oracle.bmc.auth.SimplePrivateKeySupplier; import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider; import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient; import com.oracle.bmc.retrier.RetryConfiguration; -import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.oci.OCIEmbeddingModel; -import org.springframework.ai.oci.cohere.OCICohereChatModel; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; /** * {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure Generative - * AI. + * AI Inference Client. * * @author Anders Swanson + * @author Ilayaperumal Gopinathan */ @AutoConfiguration -@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class }) -@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class, - OCICohereChatModelProperties.class }) -public class OCIGenAiAutoConfiguration { +@ConditionalOnClass(GenerativeAiInferenceClient.class) +@EnableConfigurationProperties(OCIConnectionProperties.class) +public class OCIGenAiInferenceClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties) + throws IOException { + ClientConfiguration clientConfiguration = ClientConfiguration.builder() + .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION) + .build(); + GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder() + .configuration(clientConfiguration); + if (StringUtils.hasText(properties.getRegion())) { + builder.region(Region.valueOf(properties.getRegion())); + } + if (StringUtils.hasText(properties.getEndpoint())) { + builder.endpoint(properties.getEndpoint()); + } + return builder.build(authenticationProvider(properties)); + } private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties) throws IOException { @@ -71,43 +83,4 @@ public class OCIGenAiAutoConfiguration { }; } - @ConditionalOnMissingBean - @Bean - public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties) - throws IOException { - ClientConfiguration clientConfiguration = ClientConfiguration.builder() - .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION) - .build(); - GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder() - .configuration(clientConfiguration); - if (StringUtils.hasText(properties.getRegion())) { - builder.region(Region.valueOf(properties.getRegion())); - } - if (StringUtils.hasText(properties.getEndpoint())) { - builder.endpoint(properties.getEndpoint()); - } - return builder.build(authenticationProvider(properties)); - } - - @Bean - @ConditionalOnProperty(prefix = OCIEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient, - OCIEmbeddingModelProperties properties) { - return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions()); - } - - @Bean - @ConditionalOnProperty(prefix = OCICohereChatModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient, - OCICohereChatModelProperties properties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(), - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index cbd6babd2..6f5a674d6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiAutoConfiguration +org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiChatAutoConfiguration +org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java index b8935152e..28c18307e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java @@ -56,7 +56,7 @@ class OCIGenAIAutoConfigurationTest { "spring.ai.oci.genai.cohere.chat.options.frequencyPenalty=0.1", "spring.ai.oci.genai.cohere.chat.options.presencePenalty=0.2" // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); + ).withConfiguration(AutoConfigurations.of(OCIGenAiChatAutoConfiguration.class)); contextRunner.run(context -> { OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java index 610ef9c97..53db7bfb9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java @@ -52,7 +52,7 @@ public class OCIGenAiAutoConfigurationIT { "spring.ai.oci.genai.embedding.servingMode=on-demand", "spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0" // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); + ).withConfiguration(AutoConfigurations.of(OCIGenAiEmbeddingAutoConfiguration.class)); private final ApplicationContextRunner cohereChatContextRunner = new ApplicationContextRunner().withPropertyValues( // @formatter:off @@ -62,7 +62,7 @@ public class OCIGenAiAutoConfigurationIT { "spring.ai.oci.genai.cohere.chat.options.servingMode=on-demand", "spring.ai.oci.genai.cohere.chat.options.model=" + this.CHAT_MODEL_ID // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); + ).withConfiguration(AutoConfigurations.of(OCIGenAiChatAutoConfiguration.class)); @Test void embeddings() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml index cfece9d07..522c75d8b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml @@ -38,28 +38,18 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-model-image-observation - ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java new file mode 100644 index 000000000..cfbd820e8 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023-2025 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.ollama.autoconfigure; + +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link AutoConfiguration Auto-configuration} for Ollama API. + * + * @author Christian Tzolov + * @author Eddú Meléndez + * @author Thomas Vitale + * @author Ilayaperumal Gopinathan + * @since 0.8.0 + */ +@AutoConfiguration +@ConditionalOnClass(OllamaApi.class) +@EnableConfigurationProperties(OllamaConnectionProperties.class) +public class OllamaApiAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(OllamaConnectionDetails.class) + public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) { + return new PropertiesOllamaConnectionDetails(properties); + } + + @Bean + @ConditionalOnMissingBean + public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails, + ObjectProvider restClientBuilderProvider, + ObjectProvider webClientBuilderProvider) { + return new OllamaApi(connectionDetails.getBaseUrl(), + restClientBuilderProvider.getIfAvailable(RestClient::builder), + webClientBuilderProvider.getIfAvailable(WebClient::builder)); + } + + static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails { + + private final OllamaConnectionProperties properties; + + PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) { + this.properties = properties; + } + + @Override + public String getBaseUrl() { + return this.properties.getBaseUrl(); + } + + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java deleted file mode 100644 index 0a0af3125..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2023-2025 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.ollama.autoconfigure; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.model.SpringAIModelProperties; -import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.OllamaEmbeddingModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.management.ModelManagementOptions; -import org.springframework.ai.ollama.management.PullModelStrategy; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * {@link AutoConfiguration Auto-configuration} for Ollama Chat Client. - * - * @author Christian Tzolov - * @author Eddú Meléndez - * @author Thomas Vitale - * @since 0.8.0 - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@ConditionalOnClass(OllamaApi.class) -@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaEmbeddingProperties.class, - OllamaConnectionProperties.class, OllamaInitializationProperties.class }) -@ImportAutoConfiguration(classes = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class, - WebClientAutoConfiguration.class }) -public class OllamaAutoConfiguration { - - @Bean - @ConditionalOnMissingBean(OllamaConnectionDetails.class) - public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) { - return new PropertiesOllamaConnectionDetails(properties); - } - - @Bean - @ConditionalOnMissingBean - public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider) { - return new OllamaApi(connectionDetails.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder)); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA, - matchIfMissing = true) - public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties, - OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy() - : PullModelStrategy.NEVER; - - var chatModel = OllamaChatModel.builder() - .ollamaApi(ollamaApi) - .defaultOptions(properties.getOptions()) - .toolCallingManager(toolCallingManager) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .modelManagementOptions( - new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(), - initProperties.getTimeout(), initProperties.getMaxRetries())) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OLLAMA, - matchIfMissing = true) - public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties, - OllamaInitializationProperties initProperties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude() - ? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER; - - var embeddingModel = OllamaEmbeddingModel.builder() - .ollamaApi(ollamaApi) - .defaultOptions(properties.getOptions()) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy, - initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(), - initProperties.getMaxRetries())) - .build(); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails { - - private final OllamaConnectionProperties properties; - - PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) { - this.properties = properties; - } - - @Override - public String getBaseUrl() { - return this.properties.getBaseUrl(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java similarity index 57% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java index 4c422e37a..6df931d11 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfiguration.java @@ -14,18 +14,21 @@ * limitations under the License. */ -package org.springframework.ai.model.anthropic.autoconfigure; +package org.springframework.ai.model.ollama.autoconfigure; import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.model.tool.ToolCallingManager; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.management.ModelManagementOptions; +import org.springframework.ai.ollama.management.PullModelStrategy; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -37,53 +40,42 @@ import org.springframework.boot.autoconfigure.web.reactive.function.client.WebCl import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; /** - * {@link AutoConfiguration Auto-configuration} for Anthropic Chat Model. + * {@link AutoConfiguration Auto-configuration} for Ollama Chat model. * * @author Christian Tzolov + * @author Eddú Meléndez * @author Thomas Vitale - * @since 1.0.0 + * @author Ilayaperumal Gopinathan + * @since 0.8.0 */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, - ToolCallingAutoConfiguration.class }) -@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class }) -@ConditionalOnClass(AnthropicApi.class) -@ConditionalOnProperty(prefix = AnthropicChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", +@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) +@ConditionalOnClass(OllamaChatModel.class) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA, matchIfMissing = true) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, +@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaInitializationProperties.class }) +@ImportAutoConfiguration(classes = { OllamaApiAutoConfiguration.class, RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class }) -public class AnthropicAutoConfiguration { +public class OllamaChatAutoConfiguration { @Bean @ConditionalOnMissingBean - public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - return new AnthropicApi(connectionProperties.getBaseUrl(), connectionProperties.getApiKey(), - connectionProperties.getVersion(), restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, - connectionProperties.getBetaVersion()); - } - - @Bean - @ConditionalOnMissingBean - public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties, - RetryTemplate retryTemplate, ToolCallingManager toolCallingManager, + public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties, + OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy() + : PullModelStrategy.NEVER; - var chatModel = AnthropicChatModel.builder() - .anthropicApi(anthropicApi) - .defaultOptions(chatProperties.getOptions()) + var chatModel = OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(properties.getOptions()) .toolCallingManager(toolCallingManager) - .retryTemplate(retryTemplate) .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) + .modelManagementOptions( + new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(), + initProperties.getTimeout(), initProperties.getMaxRetries())) .build(); observationConvention.ifAvailable(chatModel::setObservationConvention); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfiguration.java new file mode 100644 index 000000000..0d0bb0964 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfiguration.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023-2025 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.ollama.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.ollama.OllamaEmbeddingModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.management.ModelManagementOptions; +import org.springframework.ai.ollama.management.PullModelStrategy; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * {@link AutoConfiguration Auto-configuration} for Ollama Chat Client. + * + * @author Christian Tzolov + * @author Eddú Meléndez + * @author Thomas Vitale + * @author Ilayaperumal Gopinathan + * @since 0.8.0 + */ +@AutoConfiguration(after = RestClientAutoConfiguration.class) +@ConditionalOnClass(OllamaEmbeddingModel.class) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OLLAMA, + matchIfMissing = true) +@EnableConfigurationProperties({ OllamaEmbeddingProperties.class, OllamaInitializationProperties.class }) +@ImportAutoConfiguration(classes = { OllamaApiAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OllamaEmbeddingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties, + OllamaInitializationProperties initProperties, ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude() + ? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER; + + var embeddingModel = OllamaEmbeddingModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(properties.getOptions()) + .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) + .modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy, + initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(), + initProperties.getMaxRetries())) + .build(); + + observationConvention.ifAvailable(embeddingModel::setObservationConvention); + + return embeddingModel; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 82d659ccf..39ea13b57 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration +org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration +org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java index 1623d3ed3..65e388a59 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java @@ -55,7 +55,7 @@ public class OllamaChatAutoConfigurationIT extends BaseOllamaIT { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10") // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)); private final UserMessage userMessage = new UserMessage("What's the capital of Denmark?"); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java index 077073ef9..de27c35c2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java @@ -41,7 +41,8 @@ public class OllamaChatAutoConfigurationTests { "spring.ai.ollama.chat.options.topP=0.56", "spring.ai.ollama.chat.options.topK=123") // @formatter:on - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)) + .withConfiguration( + AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OllamaChatProperties.class); var connectionProperties = context.getBean(OllamaConnectionProperties.class); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java index 1a38c32ae..7bf54314f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java @@ -45,7 +45,8 @@ public class OllamaEmbeddingAutoConfigurationIT extends BaseOllamaIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.ollama.embedding.options.model=" + MODEL_NAME, "spring.ai.ollama.base-url=" + getBaseUrl()) - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)); + .withConfiguration( + AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaEmbeddingAutoConfiguration.class)); @BeforeAll public static void beforeAll() throws IOException, InterruptedException { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java index b29d88f8b..6f2443278 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java @@ -41,7 +41,8 @@ public class OllamaEmbeddingAutoConfigurationTests { "spring.ai.ollama.embedding.options.topK=13" // @formatter:on ) - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)) + .withConfiguration( + AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(OllamaEmbeddingProperties.class); var connectionProperties = context.getBean(OllamaConnectionProperties.class); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java index a02d8a02d..6555038b9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java @@ -26,56 +26,59 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; /** - * Unit Tests for {@link OllamaAutoConfiguration}'s conditional enabling of models. + * Unit Tests for Ollama auto-configurations conditional enabling of models. * * @author Ilayaperumal Gopinathan */ public class OllamaModelConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Test void chatModelActivation() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)).run(context -> { assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none") .run(context -> { - assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(OllamaChatModel.class)).isEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); }); - this.contextRunner.withPropertyValues("spring.ai.model.chat=ollama", "spring.ai.model.embedding=none") + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=ollama") .run(context -> { assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isEmpty(); assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); }); } @Test void embeddingModelActivation() { - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.model.embedding=ollama").run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(OllamaEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.embedding=ollama") + .run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java index 875a90742..76fcaf77f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java @@ -25,13 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.tool.function.FunctionToolCallback; @@ -53,7 +53,7 @@ public class FunctionCallbackInPromptIT extends BaseOllamaIT { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10") // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)); @BeforeAll public static void beforeAll() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java index 48d32380d..56f1db056 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java @@ -25,13 +25,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaOptions; @@ -57,7 +57,7 @@ public class OllamaFunctionCallbackIT extends BaseOllamaIT { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10") // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @BeforeAll diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java index 947192863..faaf0ea5f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java @@ -26,13 +26,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaOptions; @@ -64,7 +64,7 @@ public class OllamaFunctionToolBeanIT extends BaseOllamaIT { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10") // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @BeforeAll diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt similarity index 95% rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt rename to auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt index 1a5564813..27182cbd9 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt @@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test import org.slf4j.LoggerFactory import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration import org.springframework.ai.chat.messages.UserMessage import org.springframework.ai.chat.prompt.Prompt import org.springframework.ai.model.function.FunctionCallingOptions @@ -56,7 +56,7 @@ class FunctionCallbackResolverKotlinIT : BaseOllamaIT() { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10" ) - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration::class.java)) + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration::class.java)) .withUserConfiguration(Config::class.java) @Test diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt similarity index 95% rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt rename to auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt index 0c46d0292..6976c18ab 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt @@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.slf4j.LoggerFactory import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration import org.springframework.ai.chat.messages.UserMessage import org.springframework.ai.chat.prompt.Prompt import org.springframework.ai.model.tool.ToolCallingChatOptions @@ -55,7 +55,7 @@ class FunctionCallbackKotlinIT : BaseOllamaIT() { "spring.ai.ollama.chat.options.temperature=0.5", "spring.ai.ollama.chat.options.topK=10" ) - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration::class.java)) + .withConfiguration(AutoConfigurations.of(OllamaChatAutoConfiguration::class.java)) .withUserConfiguration(Config::class.java) @Test diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt similarity index 97% rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt rename to auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt index 3bcdf606e..9a73e320f 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt @@ -19,7 +19,6 @@ package org.springframework.ai.model.ollama.autoconfigure.tool import com.fasterxml.jackson.annotation.JsonClassDescription import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude.Include -import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonPropertyDescription class MockKotlinWeatherService : Function1 { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-openai/pom.xml index 0dddf7793..e5346f439 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/pom.xml @@ -30,51 +30,38 @@ org.springframework.ai spring-ai-openai ${project.parent.version} - true - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-observation-chat ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-observation-embedding ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-observation-image ${project.parent.version} - true @@ -106,6 +93,13 @@ test + + org.springframework.ai + spring-ai-autoconfigure-model-chat-client + ${project.parent.version} + test + + org.springframework.boot spring-boot-starter-test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java index c01dc0856..2b2f47975 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java @@ -19,7 +19,6 @@ package org.springframework.ai.model.openai.autoconfigure; import org.springframework.ai.model.SimpleApiKey; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiAudioApi; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java index 96e297267..ba5db2579 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java @@ -59,7 +59,7 @@ import org.springframework.web.reactive.function.client.WebClient; import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; /** - * {@link AutoConfiguration Auto-configuration} for OpenAI. + * Chat {@link AutoConfiguration Auto-configuration} for OpenAI. * * @author Christian Tzolov * @author Stefan Vassilev diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java index 3367d0d38..75f092fbe 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java @@ -43,7 +43,7 @@ import org.springframework.web.reactive.function.client.WebClient; import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; /** - * {@link AutoConfiguration Auto-configuration} for OpenAI. + * Embedding {@link AutoConfiguration Auto-configuration} for OpenAI. * * @author Christian Tzolov * @author Stefan Vassilev diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java index 4a9966072..a7eee06fd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java @@ -43,7 +43,7 @@ import org.springframework.web.client.RestClient; import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; /** - * {@link AutoConfiguration Auto-configuration} for OpenAI. + * Image {@link AutoConfiguration Auto-configuration} for OpenAI. * * @author Christian Tzolov * @author Stefan Vassilev diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java index 86b72a103..7e9f9adb2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java @@ -16,27 +16,11 @@ package org.springframework.ai.model.openai.autoconfigure; -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationConvention; import org.springframework.ai.model.SimpleApiKey; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiModerationModel; import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.OpenAiModerationApi; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; @@ -48,17 +32,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; /** - * {@link AutoConfiguration Auto-configuration} for OpenAI. + * Moderation {@link AutoConfiguration Auto-configuration} for OpenAI. * * @author Christian Tzolov * @author Stefan Vassilev diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml index 7be055456..b6a992947 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml @@ -35,25 +35,16 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfiguration.java similarity index 89% rename from auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfiguration.java index d35ed9c23..6fcbb4f29 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -36,13 +36,13 @@ import org.springframework.jdbc.core.JdbcTemplate; */ @AutoConfiguration(after = JdbcTemplateAutoConfiguration.class) @ConditionalOnClass(PostgresMlEmbeddingModel.class) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.POSTGRESML, + matchIfMissing = true) @EnableConfigurationProperties(PostgresMlEmbeddingProperties.class) -public class PostgresMlAutoConfiguration { +public class PostgresMlEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.POSTGRESML, - matchIfMissing = true) public PostgresMlEmbeddingModel postgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, PostgresMlEmbeddingProperties embeddingProperties) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index b677fa1cd..e7e0cfd8f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.postgresml.autoconfigure.PostgresMlAutoConfiguration +org.springframework.ai.model.postgresml.autoconfigure.PostgresMlEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java deleted file mode 100644 index 678c9f75f..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.model.postgresml.autoconfigure; - -import java.util.List; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; - -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.postgresml.PostgresMlEmbeddingModel; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.jdbc.core.JdbcTemplate; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Utkarsh Srivastava - */ -@JdbcTest(properties = "logging.level.sql=TRACE") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@Testcontainers -@Disabled("Disabled from automatic execution, as it requires an excessive amount of memory (over 9GB)!") -public class PostgresMlAutoConfigurationIT { - - @Container - @ServiceConnection - static PostgreSQLContainer postgres = new PostgreSQLContainer<>( - DockerImageName.parse("ghcr.io/postgresml/postgresml:2.8.1").asCompatibleSubstituteFor("postgres")) - .withCommand("sleep", "infinity") - .withUsername("postgresml") - .withPassword("postgresml") - .withDatabaseName("postgresml") - .waitingFor(Wait.forLogMessage(".*Starting dashboard.*\\s", 1)); - - @Autowired - JdbcTemplate jdbcTemplate; - - @Test - void embedding() { - ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)); - contextRunner.run(context -> { - PostgresMlEmbeddingModel embeddingModel = context.getBean(PostgresMlEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isZero(); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(768); - }); - } - - @Test - void embeddingActivation() { - new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) - .withPropertyValues("spring.ai.postgresml.embedding.enabled=false") - .run(context -> { - assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) - .withPropertyValues("spring.ai.postgresml.embedding.enabled=true") - .run(context -> { - assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfigurationIT.java similarity index 91% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfigurationIT.java index 678c9f75f..531d52a60 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingAutoConfigurationIT.java @@ -45,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat; @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Testcontainers @Disabled("Disabled from automatic execution, as it requires an excessive amount of memory (over 9GB)!") -public class PostgresMlAutoConfigurationIT { +public class PostgresMlEmbeddingAutoConfigurationIT { @Container @ServiceConnection @@ -64,7 +64,7 @@ public class PostgresMlAutoConfigurationIT { void embedding() { ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(PostgresMlEmbeddingAutoConfiguration.class)); contextRunner.run(context -> { PostgresMlEmbeddingModel embeddingModel = context.getBean(PostgresMlEmbeddingModel.class); @@ -83,7 +83,7 @@ public class PostgresMlAutoConfigurationIT { @Test void embeddingActivation() { new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(PostgresMlEmbeddingAutoConfiguration.class)) .withPropertyValues("spring.ai.postgresml.embedding.enabled=false") .run(context -> { assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); @@ -91,7 +91,7 @@ public class PostgresMlAutoConfigurationIT { }); new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(PostgresMlEmbeddingAutoConfiguration.class)) .withPropertyValues("spring.ai.postgresml.embedding.enabled=true") .run(context -> { assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); @@ -99,7 +99,7 @@ public class PostgresMlAutoConfigurationIT { }); new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate) - .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(PostgresMlEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml index abb2d58da..9fb92ef3e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml @@ -39,21 +39,18 @@ org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatAutoConfiguration.java similarity index 54% rename from auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatAutoConfiguration.java index 98d456ef9..173828d46 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatAutoConfiguration.java @@ -18,19 +18,14 @@ package org.springframework.ai.model.qianfan.autoconfigure; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationConvention; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanImageModel; import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.ai.qianfan.api.QianFanImageApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -47,21 +42,20 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for QianFan Chat, Embedding, and Image - * Models. + * Chat {@link AutoConfiguration Auto-configuration} for QianFan Chat Model. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @ConditionalOnClass(QianFanApi.class) -@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class, - QianFanEmbeddingProperties.class, QianFanImageProperties.class }) -public class QianFanAutoConfiguration { +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.QIANFAN, + matchIfMissing = true) +@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class }) +public class QianFanChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.QIANFAN, - matchIfMissing = true) public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties, QianFanChatProperties chatProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, @@ -81,64 +75,6 @@ public class QianFanAutoConfiguration { return chatModel; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.QIANFAN, - matchIfMissing = true) - public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties, - QianFanEmbeddingProperties embeddingProperties, - ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(), - commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), - responseErrorHandler); - - var embeddingModel = new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.QIANFAN, - matchIfMissing = true) - public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties, - QianFanImageProperties imageProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() - : commonProperties.getApiKey(); - - String secretKey = StringUtils.hasText(imageProperties.getSecretKey()) ? imageProperties.getSecretKey() - : commonProperties.getSecretKey(); - - String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - - Assert.hasText(apiKey, "QianFan API key must be set. Use the property: spring.ai.qianfan.api-key"); - Assert.hasText(secretKey, "QianFan secret key must be set. Use the property: spring.ai.qianfan.secret-key"); - Assert.hasText(baseUrl, "QianFan base URL must be set. Use the property: spring.ai.qianfan.base-url"); - - var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey, - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(imageModel::setObservationConvention); - - return imageModel; - } - private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, String secretKey, String commonSecretKey, RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingAutoConfiguration.java new file mode 100644 index 000000000..39bf6db3e --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingAutoConfiguration.java @@ -0,0 +1,93 @@ +/* + * 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.model.qianfan.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.qianfan.QianFanEmbeddingModel; +import org.springframework.ai.qianfan.api.QianFanApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +/** + * Embedding {@link AutoConfiguration Auto-configuration} for QianFan Embedding Model. + * + * @author Geng Rong + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) +@ConditionalOnClass(QianFanApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.QIANFAN, + matchIfMissing = true) +@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanEmbeddingProperties.class }) +public class QianFanEmbeddingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties, + QianFanEmbeddingProperties embeddingProperties, + ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), + embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(), + commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), + responseErrorHandler); + + var embeddingModel = new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(), + embeddingProperties.getOptions(), retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(embeddingModel::setObservationConvention); + + return embeddingModel; + } + + private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, + String secretKey, String commonSecretKey, RestClient.Builder restClientBuilder, + ResponseErrorHandler responseErrorHandler) { + + String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl; + Assert.hasText(resolvedBaseUrl, "QianFan base URL must be set"); + + String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey; + Assert.hasText(resolvedApiKey, "QianFan API key must be set"); + + String resolvedSecretKey = StringUtils.hasText(secretKey) ? secretKey : commonSecretKey; + Assert.hasText(resolvedSecretKey, "QianFan Secret key must be set"); + + return new QianFanApi(resolvedBaseUrl, resolvedApiKey, resolvedSecretKey, restClientBuilder, + responseErrorHandler); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageAutoConfiguration.java new file mode 100644 index 000000000..d3cd534fc --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageAutoConfiguration.java @@ -0,0 +1,87 @@ +/* + * 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.model.qianfan.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.image.observation.ImageModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.qianfan.QianFanImageModel; +import org.springframework.ai.qianfan.api.QianFanApi; +import org.springframework.ai.qianfan.api.QianFanImageApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +/** + * Image {@link AutoConfiguration Auto-configuration} for QianFan Image Model. + * + * @author Geng Rong + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) +@ConditionalOnClass(QianFanApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.QIANFAN, + matchIfMissing = true) +@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanImageProperties.class }) +public class QianFanImageAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties, + QianFanImageProperties imageProperties, ObjectProvider restClientBuilderProvider, + RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() + : commonProperties.getApiKey(); + + String secretKey = StringUtils.hasText(imageProperties.getSecretKey()) ? imageProperties.getSecretKey() + : commonProperties.getSecretKey(); + + String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() + : commonProperties.getBaseUrl(); + + Assert.hasText(apiKey, "QianFan API key must be set. Use the property: spring.ai.qianfan.api-key"); + Assert.hasText(secretKey, "QianFan secret key must be set. Use the property: spring.ai.qianfan.secret-key"); + Assert.hasText(baseUrl, "QianFan base URL must be set. Use the property: spring.ai.qianfan.base-url"); + + var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey, + restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); + + var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(imageModel::setObservationConvention); + + return imageModel; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 74529bcf0..b8ae44638 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.qianfan.autoconfigure.QianFanAutoConfiguration +org.springframework.ai.model.qianfan.autoconfigure.QianFanChatAutoConfiguration +org.springframework.ai.model.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration +org.springframework.ai.model.qianfan.autoconfigure.QianFanImageAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java index bc2334e20..570759420 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -37,6 +36,7 @@ import org.springframework.ai.image.ImageResponse; import org.springframework.ai.qianfan.QianFanChatModel; import org.springframework.ai.qianfan.QianFanEmbeddingModel; import org.springframework.ai.qianfan.QianFanImageModel; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -55,12 +55,12 @@ public class QianFanAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.qianfan.apiKey=" + System.getenv("QIANFAN_API_KEY"), "spring.ai.qianfan.secretKey=" + System.getenv("QIANFAN_SECRET_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)); + .withConfiguration( + AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class)); @Test void generate() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanChatAutoConfiguration.class)).run(context -> { QianFanChatModel client = context.getBean(QianFanChatModel.class); String response = client.call("Hello"); assertThat(response).isNotEmpty(); @@ -70,7 +70,7 @@ public class QianFanAutoConfigurationIT { @Test void generateStreaming() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanChatAutoConfiguration.class)).run(context -> { QianFanChatModel client = context.getBean(QianFanChatModel.class); Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); String response = Objects.requireNonNull(responseFlux.collectList().block()) @@ -84,31 +84,34 @@ public class QianFanAutoConfigurationIT { @Test void embedding() { - this.contextRunner.run(context -> { - QianFanEmbeddingModel embeddingClient = context.getBean(QianFanEmbeddingModel.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanEmbeddingAutoConfiguration.class)) + .run(context -> { + QianFanEmbeddingModel embeddingClient = context.getBean(QianFanEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingClient - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + EmbeddingResponse embeddingResponse = embeddingClient + .embedForResponse(List.of("Hello World", "World is big and salvation is near")); + assertThat(embeddingResponse.getResults()).hasSize(2); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); + assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - assertThat(embeddingClient.dimensions()).isEqualTo(1024); - }); + assertThat(embeddingClient.dimensions()).isEqualTo(1024); + }); } @Test void generateImage() { - this.contextRunner.withPropertyValues("spring.ai.qianfan.image.options.size=1024x1024").run(context -> { - QianFanImageModel imageModel = context.getBean(QianFanImageModel.class); - ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNull(); - assertThat(imageResponse.getResult().getOutput().getB64Json()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getB64Json()); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(QianFanImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.qianfan.image.options.size=1024x1024") + .run(context -> { + QianFanImageModel imageModel = context.getBean(QianFanImageModel.class); + ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); + assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResult().getOutput().getUrl()).isNull(); + assertThat(imageResponse.getResult().getOutput().getB64Json()).isNotEmpty(); + logger.info("Generated image: " + imageResponse.getResult().getOutput().getB64Json()); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java index d10e3b704..208e09ef6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -18,11 +18,11 @@ package org.springframework.ai.model.qianfan.autoconfigure; import org.junit.jupiter.api.Test; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.qianfan.QianFanChatModel; import org.springframework.ai.qianfan.QianFanEmbeddingModel; import org.springframework.ai.qianfan.QianFanImageModel; import org.springframework.ai.qianfan.api.QianFanApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; * {@link QianFanEmbeddingProperties}. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ public class QianFanPropertiesTests { @@ -50,7 +51,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(QianFanChatProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -82,7 +83,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(QianFanChatProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -111,7 +112,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -141,7 +142,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -179,18 +180,15 @@ public class QianFanPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(QianFanChatProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); - var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("bge_large_zh"); - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); @@ -217,7 +215,7 @@ public class QianFanPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(QianFanConnectionProperties.class); var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); @@ -237,9 +235,9 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isEmpty(); assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isEmpty(); }); @@ -247,7 +245,7 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty(); @@ -257,7 +255,7 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=qianfan") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty(); @@ -270,9 +268,9 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(QianFanChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(QianFanChatModel.class)).isEmpty(); }); @@ -280,7 +278,7 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty(); @@ -290,7 +288,7 @@ public class QianFanPropertiesTests { .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=true") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + RestClientAutoConfiguration.class, QianFanChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty(); @@ -310,7 +308,7 @@ public class QianFanPropertiesTests { // @formatter:on .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(QianFanImageProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -342,7 +340,7 @@ public class QianFanPropertiesTests { // @formatter:on .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(QianFanImageProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -379,7 +377,7 @@ public class QianFanPropertiesTests { // @formatter:on .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(QianFanImageProperties.class); var connectionProperties = context.getBean(QianFanConnectionProperties.class); @@ -405,9 +403,9 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.image=none") .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(QianFanImageProperties.class)).isEmpty(); assertThat(context.getBeansOfType(QianFanImageModel.class)).isEmpty(); }); @@ -416,7 +414,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.base-url=TEST_BASE_URL") .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty(); @@ -427,7 +425,7 @@ public class QianFanPropertiesTests { "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=qianfan") .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) + WebClientAutoConfiguration.class, QianFanImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml index f73cc84ad..0bb37d261 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml @@ -39,14 +39,12 @@ org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-image-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java index c6d83e9cd..b38df3132 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -37,10 +37,13 @@ import org.springframework.web.client.RestClient; * * @author Mark Pollack * @author Christian Tzolov + * @author Ilayaperumal Gopinathan * @since 0.8.0 */ @AutoConfiguration(after = { RestClientAutoConfiguration.class }) @ConditionalOnClass(StabilityAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.STABILITY_AI, + matchIfMissing = true) @EnableConfigurationProperties({ StabilityAiConnectionProperties.class, StabilityAiImageProperties.class }) public class StabilityAiImageAutoConfiguration { @@ -64,8 +67,6 @@ public class StabilityAiImageAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.STABILITY, - matchIfMissing = true) public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi, StabilityAiImageProperties stabilityAiImageProperties) { return new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions()); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java index 9cbc4e9f1..cfd9e7132 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -80,7 +80,7 @@ public class StabilityAiImagePropertiesTests { "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.model.image=none") .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isEmpty(); assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isEmpty(); }); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml index 248c9968a..bb6601bc6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml @@ -39,7 +39,6 @@ org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java index d4896a80d..e2c25760c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java @@ -39,13 +39,13 @@ import org.springframework.context.annotation.Bean; */ @AutoConfiguration @EnableConfigurationProperties({ TransformersEmbeddingModelProperties.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.TRANSFORMERS, + matchIfMissing = true) @ConditionalOnClass({ OrtSession.class, HuggingFaceTokenizer.class, TransformersEmbeddingModel.class }) public class TransformersEmbeddingModelAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.TRANSFORMERS, - matchIfMissing = true) public TransformersEmbeddingModel embeddingModel(TransformersEmbeddingModelProperties properties, ObjectProvider observationRegistry, ObjectProvider observationConvention) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml index c4b091a78..18a0515b4 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml @@ -48,35 +48,24 @@ org.springframework.ai spring-ai-autoconfigure-model-tool ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-model-image-observation - ${project.parent.version} - true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionAutoConfiguration.java new file mode 100644 index 000000000..4dd06fc0e --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023-2025 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.vertexai.autoconfigure.embedding; + +import com.google.cloud.vertexai.VertexAI; + +import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Auto-configuration for Vertex AI Embedding Connection. + * + * @author Christian Tzolov + * @author Mark Pollack + * @author Ilayaperumal Gopinathan + * @since 1.0.0 + */ +@AutoConfiguration +@ConditionalOnClass(VertexAI.class) +@EnableConfigurationProperties(VertexAiEmbeddingConnectionProperties.class) +public class VertexAiEmbeddingConnectionAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public VertexAiEmbeddingConnectionDetails connectionDetails( + VertexAiEmbeddingConnectionProperties connectionProperties) { + + Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!"); + Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!"); + + var connectionBuilder = VertexAiEmbeddingConnectionDetails.builder() + .projectId(connectionProperties.getProjectId()) + .location(connectionProperties.getLocation()); + + if (StringUtils.hasText(connectionProperties.getApiEndpoint())) { + connectionBuilder.apiEndpoint(connectionProperties.getApiEndpoint()); + } + + return connectionBuilder.build(); + + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultiModalEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultiModalEmbeddingAutoConfiguration.java new file mode 100644 index 000000000..5015bec2f --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultiModalEmbeddingAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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.model.vertexai.autoconfigure.embedding; + +import java.io.IOException; + +import com.google.cloud.vertexai.VertexAI; + +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails; +import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Auto-configuration for Vertex AI Gemini Chat. + * + * @author Christian Tzolov + * @author Mark Pollack + * @author Ilayaperumal Gopinathan + * @since 1.0.0 + */ +@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class }) +@ConditionalOnClass({ VertexAI.class, VertexAiMultimodalEmbeddingModel.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.MULTI_MODAL_EMBEDDING_MODEL, + havingValue = SpringAIModels.VERTEX_AI, matchIfMissing = true) +@EnableConfigurationProperties(VertexAiMultimodalEmbeddingProperties.class) +@ImportAutoConfiguration( + classes = { SpringAiRetryAutoConfiguration.class, VertexAiEmbeddingConnectionAutoConfiguration.class }) +public class VertexAiMultiModalEmbeddingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public VertexAiMultimodalEmbeddingModel multimodalEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, + VertexAiMultimodalEmbeddingProperties multimodalEmbeddingProperties) throws IOException { + + return new VertexAiMultimodalEmbeddingModel(connectionDetails, multimodalEmbeddingProperties.getOptions()); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingAutoConfiguration.java similarity index 57% rename from auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingAutoConfiguration.java index cd67ccf95..54fc82e48 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingAutoConfiguration.java @@ -16,17 +16,13 @@ package org.springframework.ai.model.vertexai.autoconfigure.embedding; -import java.io.IOException; - -import com.google.cloud.vertexai.VertexAI; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails; -import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel; import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingModel; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -37,47 +33,26 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; /** * Auto-configuration for Vertex AI Gemini Chat. * * @author Christian Tzolov * @author Mark Pollack + * @author Ilayaperumal Gopinathan * @since 1.0.0 */ @AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class }) -@ConditionalOnClass({ VertexAI.class, VertexAiTextEmbeddingModel.class }) -@EnableConfigurationProperties({ VertexAiEmbeddingConnectionProperties.class, VertexAiTextEmbeddingProperties.class, - VertexAiMultimodalEmbeddingProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class }) -public class VertexAiEmbeddingAutoConfiguration { +@ConditionalOnClass(VertexAiTextEmbeddingModel.class) +@ConditionalOnProperty(name = SpringAIModelProperties.TEXT_EMBEDDING_MODEL, havingValue = SpringAIModels.VERTEX_AI, + matchIfMissing = true) +@EnableConfigurationProperties(VertexAiTextEmbeddingProperties.class) +@ImportAutoConfiguration( + classes = { SpringAiRetryAutoConfiguration.class, VertexAiEmbeddingConnectionAutoConfiguration.class }) +public class VertexAiTextEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - public VertexAiEmbeddingConnectionDetails connectionDetails( - VertexAiEmbeddingConnectionProperties connectionProperties) { - - Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!"); - Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!"); - - var connectionBuilder = VertexAiEmbeddingConnectionDetails.builder() - .projectId(connectionProperties.getProjectId()) - .location(connectionProperties.getLocation()); - - if (StringUtils.hasText(connectionProperties.getApiEndpoint())) { - connectionBuilder.apiEndpoint(connectionProperties.getApiEndpoint()); - } - - return connectionBuilder.build(); - - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.TEXT_EMBEDDING_MODEL, havingValue = SpringAIModels.VERTEX_AI, - matchIfMissing = true) public VertexAiTextEmbeddingModel textEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, VertexAiTextEmbeddingProperties textEmbeddingProperties, RetryTemplate retryTemplate, ObjectProvider observationRegistry, @@ -91,14 +66,4 @@ public class VertexAiEmbeddingAutoConfiguration { return embeddingModel; } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.MULTI_MODAL_EMBEDDING_MODEL, - havingValue = SpringAIModels.VERTEX_AI, matchIfMissing = true) - public VertexAiMultimodalEmbeddingModel multimodalEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, - VertexAiMultimodalEmbeddingProperties multimodalEmbeddingProperties) throws IOException { - - return new VertexAiMultimodalEmbeddingModel(connectionDetails, multimodalEmbeddingProperties.getOptions()); - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfiguration.java similarity index 96% rename from auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfiguration.java index 7a4ceec79..a7e2e9590 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfiguration.java @@ -22,12 +22,12 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.vertexai.VertexAI; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -49,13 +49,16 @@ import org.springframework.util.StringUtils; * @author Christian Tzolov * @author Soby Chacko * @author Mark Pollack + * @author Ilayaperumal Gopinathan * @since 1.0.0 */ @AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) @ConditionalOnClass({ VertexAI.class, VertexAiGeminiChatModel.class }) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.VERTEX_AI, + matchIfMissing = true) @EnableConfigurationProperties({ VertexAiGeminiChatProperties.class, VertexAiGeminiConnectionProperties.class }) @ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -public class VertexAiGeminiAutoConfiguration { +public class VertexAiGeminiChatAutoConfiguration { @Bean @ConditionalOnMissingBean @@ -87,8 +90,6 @@ public class VertexAiGeminiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.VERTEX_AI, - matchIfMissing = true) public VertexAiGeminiChatModel vertexAiGeminiChat(VertexAI vertexAi, VertexAiGeminiChatProperties chatProperties, ToolCallingManager toolCallingManager, ApplicationContext context, RetryTemplate retryTemplate, ObjectProvider observationRegistry, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1f2a06e16..0cf7cd5b9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,5 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiEmbeddingAutoConfiguration -org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration +org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiTextEmbeddingAutoConfiguration +org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiMultiModalEmbeddingAutoConfiguration +org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java index c1ed7eaf3..7955ce2ac 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java @@ -37,107 +37,119 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Christian Tzolov + * @author Ilayaperumal Gopinathan */ @EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") @EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") public class VertexAiTextEmbeddingModelAutoConfigurationIT { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.embedding.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.embedding.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiEmbeddingAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( + "spring.ai.vertex.ai.embedding.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), + "spring.ai.vertex.ai.embedding.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")); @TempDir File tempDir; @Test public void textEmbedding() { - this.contextRunner.run(context -> { - var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); - var textEmbeddingProperties = context.getBean(VertexAiTextEmbeddingProperties.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiTextEmbeddingAutoConfiguration.class)) + .run(context -> { + var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); + var textEmbeddingProperties = context.getBean(VertexAiTextEmbeddingProperties.class); - assertThat(conntectionProperties).isNotNull(); - assertThat(textEmbeddingProperties.isEnabled()).isTrue(); + assertThat(conntectionProperties).isNotNull(); + assertThat(textEmbeddingProperties.isEnabled()).isTrue(); - VertexAiTextEmbeddingModel embeddingModel = context.getBean(VertexAiTextEmbeddingModel.class); - assertThat(embeddingModel).isInstanceOf(VertexAiTextEmbeddingModel.class); + VertexAiTextEmbeddingModel embeddingModel = context.getBean(VertexAiTextEmbeddingModel.class); + assertThat(embeddingModel).isInstanceOf(VertexAiTextEmbeddingModel.class); - List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI")); + List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI")); - assertThat(embeddings.size()).isEqualTo(2); // batch size - assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); - }); + assertThat(embeddings.size()).isEqualTo(2); // batch size + assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); + }); } @Test void textEmbeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=false").run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiTextEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.text.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=true").run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiTextEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.text.embedding=vertexai") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); + }); - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiTextEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); + }); } @Test public void multimodalEmbedding() { - this.contextRunner.run(context -> { - var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); - var multimodalEmbeddingProperties = context.getBean(VertexAiMultimodalEmbeddingProperties.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiMultiModalEmbeddingAutoConfiguration.class)) + .run(context -> { + var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); + var multimodalEmbeddingProperties = context.getBean(VertexAiMultimodalEmbeddingProperties.class); - assertThat(conntectionProperties).isNotNull(); - assertThat(multimodalEmbeddingProperties.isEnabled()).isTrue(); + assertThat(conntectionProperties).isNotNull(); + assertThat(multimodalEmbeddingProperties.isEnabled()).isTrue(); - VertexAiMultimodalEmbeddingModel multiModelEmbeddingModel = context - .getBean(VertexAiMultimodalEmbeddingModel.class); + VertexAiMultimodalEmbeddingModel multiModelEmbeddingModel = context + .getBean(VertexAiMultimodalEmbeddingModel.class); - assertThat(multiModelEmbeddingModel).isNotNull(); + assertThat(multiModelEmbeddingModel).isNotNull(); - var document = new Document("Hello World"); + var document = new Document("Hello World"); - DocumentEmbeddingRequest embeddingRequest = new DocumentEmbeddingRequest(List.of(document), - EmbeddingOptionsBuilder.builder().build()); + DocumentEmbeddingRequest embeddingRequest = new DocumentEmbeddingRequest(List.of(document), + EmbeddingOptionsBuilder.builder().build()); - EmbeddingResponse embeddingResponse = multiModelEmbeddingModel.call(embeddingRequest); - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0)).isNotNull(); - assertThat(embeddingResponse.getResults().get(0).getMetadata().getModalityType()) - .isEqualTo(EmbeddingResultMetadata.ModalityType.TEXT); - assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1408); + EmbeddingResponse embeddingResponse = multiModelEmbeddingModel.call(embeddingRequest); + assertThat(embeddingResponse.getResults()).hasSize(1); + assertThat(embeddingResponse.getResults().get(0)).isNotNull(); + assertThat(embeddingResponse.getResults().get(0).getMetadata().getModalityType()) + .isEqualTo(EmbeddingResultMetadata.ModalityType.TEXT); + assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1408); - assertThat(embeddingResponse.getMetadata().getModel()).isEqualTo("multimodalembedding@001"); - assertThat(embeddingResponse.getMetadata().getUsage().getPromptTokens()).isEqualTo(0); + assertThat(embeddingResponse.getMetadata().getModel()).isEqualTo("multimodalembedding@001"); + assertThat(embeddingResponse.getMetadata().getUsage().getPromptTokens()).isEqualTo(0); - assertThat(multiModelEmbeddingModel.dimensions()).isEqualTo(1408); + assertThat(multiModelEmbeddingModel.dimensions()).isEqualTo(1408); - }); + }); } @Test void multimodalEmbeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=false").run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiMultiModalEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.multi-modal.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isEmpty(); + }); - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=true").run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiMultiModalEmbeddingAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.multi-modal.embedding=vertexai") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); + }); - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiMultiModalEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); + }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java deleted file mode 100644 index a6cdfb3dd..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini; - -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -public class VertexAiGeminiAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(VertexAiGeminiAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfigurationIT.java similarity index 93% rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java rename to auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfigurationIT.java index a6cdfb3dd..a623599d6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatAutoConfigurationIT.java @@ -35,14 +35,14 @@ import static org.assertj.core.api.Assertions.assertThat; @EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") @EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -public class VertexAiGeminiAutoConfigurationIT { +public class VertexAiGeminiChatAutoConfigurationIT { - private static final Log logger = LogFactory.getLog(VertexAiGeminiAutoConfigurationIT.class); + private static final Log logger = LogFactory.getLog(VertexAiGeminiChatAutoConfigurationIT.class); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)); @Test void generate() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiModelConfigurationTests.java new file mode 100644 index 000000000..f3314dd0d --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiModelConfigurationTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025-2025 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.vertexai.autoconfigure.gemini; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit Tests for OpenAI auto configurations' conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") +@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") +public class VertexAiModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( + "spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), + "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")); + + @Test + void chatModelActivation() { + + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiGeminiChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(VertexAiGeminiChatModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=vertexai") + .run(context -> { + assertThat(context.getBeansOfType(VertexAiGeminiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(VertexAiGeminiChatModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java index 2cbb71c49..99e6df1ba 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; +import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -49,7 +49,7 @@ class FunctionCallWithFunctionBeanIT { .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java index 32cc29327..a0c38b417 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; +import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -47,7 +47,7 @@ public class FunctionCallWithFunctionWrapperIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java index 3d9d04a86..62870fdc4 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; +import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiChatAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; @@ -44,7 +44,7 @@ public class FunctionCallWithPromptFunctionIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(VertexAiGeminiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml index 00e937e6a..22ceb2fa7 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml @@ -39,14 +39,12 @@ org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatAutoConfiguration.java similarity index 72% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatAutoConfiguration.java index fbb20c697..506447d42 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -16,8 +16,9 @@ package org.springframework.ai.model.watsonxai.autoconfigure; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.watsonx.WatsonxAiChatModel; -import org.springframework.ai.watsonx.WatsonxAiEmbeddingModel; import org.springframework.ai.watsonx.api.WatsonxAiApi; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -30,20 +31,20 @@ import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestClient; /** - * WatsonX.ai autoconfiguration class. + * WatsonX.ai chat autoconfiguration class. * * @author Pablo Sanchidrian Herrera * @author John Jario Moreno Rojas * @author Christian Tzolov + * @author Ilayaperumal Gopinathan * @since 1.0.0 */ @AutoConfiguration(after = RestClientAutoConfiguration.class) @ConditionalOnClass(WatsonxAiApi.class) -@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class, - WatsonxAiEmbeddingProperties.class }) -@ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.WATSONX_AI, matchIfMissing = true) -public class WatsonxAiAutoConfiguration { +@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class }) +public class WatsonxAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean @@ -56,19 +57,8 @@ public class WatsonxAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) public WatsonxAiChatModel watsonxChatModel(WatsonxAiApi watsonxApi, WatsonxAiChatProperties chatProperties) { return new WatsonxAiChatModel(watsonxApi, chatProperties.getOptions()); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = WatsonxAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public WatsonxAiEmbeddingModel watsonxAiEmbeddingModel(WatsonxAiApi watsonxApi, - WatsonxAiEmbeddingProperties properties) { - return new WatsonxAiEmbeddingModel(watsonxApi, properties.getOptions()); - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingAutoConfiguration.java similarity index 78% rename from auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingAutoConfiguration.java index 2477069c6..1210cb3aa 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingAutoConfiguration.java @@ -18,7 +18,6 @@ package org.springframework.ai.model.watsonxai.autoconfigure; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; -import org.springframework.ai.watsonx.WatsonxAiChatModel; import org.springframework.ai.watsonx.WatsonxAiEmbeddingModel; import org.springframework.ai.watsonx.api.WatsonxAiApi; import org.springframework.beans.factory.ObjectProvider; @@ -32,18 +31,20 @@ import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestClient; /** - * WatsonX.ai autoconfiguration class. + * WatsonX.ai embedding autoconfiguration class. * * @author Pablo Sanchidrian Herrera * @author John Jario Moreno Rojas * @author Christian Tzolov + * @author Ilayaperumal Gopinathan * @since 1.0.0 */ @AutoConfiguration(after = RestClientAutoConfiguration.class) @ConditionalOnClass(WatsonxAiApi.class) -@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class, - WatsonxAiEmbeddingProperties.class }) -public class WatsonxAiAutoConfiguration { +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.WATSONX_AI, + matchIfMissing = true) +@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiEmbeddingProperties.class }) +public class WatsonxAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean @@ -56,16 +57,6 @@ public class WatsonxAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.WATSONX_AI, - matchIfMissing = true) - public WatsonxAiChatModel watsonxChatModel(WatsonxAiApi watsonxApi, WatsonxAiChatProperties chatProperties) { - return new WatsonxAiChatModel(watsonxApi, chatProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.WATSONX_AI, - matchIfMissing = true) public WatsonxAiEmbeddingModel watsonxAiEmbeddingModel(WatsonxAiApi watsonxApi, WatsonxAiEmbeddingProperties properties) { return new WatsonxAiEmbeddingModel(watsonxApi, properties.getOptions()); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 6c9942c1c..1ca223068 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.watsonxai.autoconfigure.WatsonxAiAutoConfiguration +org.springframework.ai.model.watsonxai.autoconfigure.WatsonxAiChatAutoConfiguration +org.springframework.ai.model.watsonxai.autoconfigure.WatsonxAiEmbeddingAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java index 86f6fca45..f9fe70ff0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java @@ -37,8 +37,8 @@ public class WatsonxAiAutoConfigurationTests { "spring.ai.watsonx.ai.projectId=1", "spring.ai.watsonx.ai.IAMToken=123456") // @formatter:on - .withConfiguration( - AutoConfigurations.of(RestClientAutoConfiguration.class, WatsonxAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, + WatsonxAiChatAutoConfiguration.class, WatsonxAiEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(WatsonxAiConnectionProperties.class); assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml index 61719f3a0..05189fe1a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml @@ -39,28 +39,24 @@ org.springframework.ai spring-ai-autoconfigure-retry ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-chat-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} - true org.springframework.ai spring-ai-autoconfigure-model-image-observation ${project.parent.version} - true diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java similarity index 65% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java index 51674fc76..20df42b7c 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -14,19 +14,21 @@ * limitations under the License. */ -package org.springframework.ai.model.moonshot.autoconfigure; +package org.springframework.ai.model.zhipuai.autoconfigure; import java.util.List; import io.micrometer.observation.ObservationRegistry; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.api.MoonshotApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.zhipuai.ZhiPuAiChatModel; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -43,37 +45,51 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for Moonshot Chat Model. + * Chat {@link AutoConfiguration Auto-configuration} for ZhiPuAI. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) -@EnableConfigurationProperties({ MoonshotCommonProperties.class, MoonshotChatProperties.class }) -@ConditionalOnClass(MoonshotApi.class) -public class MoonshotAutoConfiguration { +@ConditionalOnClass(ZhiPuAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ZHIPUAI, + matchIfMissing = true) +@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class }) +public class ZhiPuAiChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties, - MoonshotChatProperties chatProperties, ObjectProvider restClientBuilderProvider, + public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, + ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { - var moonshotApi = moonshotApi(chatProperties.getApiKey(), commonProperties.getApiKey(), - chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), + var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), + chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - var chatModel = new MoonshotChatModel(moonshotApi, chatProperties.getOptions(), functionCallbackResolver, + var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver, toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); observationConvention.ifAvailable(chatModel::setObservationConvention); + return chatModel; } + private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, + RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { + + String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl; + Assert.hasText(resolvedBaseUrl, "ZhiPuAI base URL must be set"); + + String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey; + Assert.hasText(resolvedApiKey, "ZhiPuAI API key must be set"); + + return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); + } + @Bean @ConditionalOnMissingBean public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { @@ -82,16 +98,4 @@ public class MoonshotAutoConfiguration { return manager; } - private MoonshotApi moonshotApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl, - RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { - - var resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey; - var resoledBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl; - - Assert.hasText(resolvedApiKey, "Moonshot API key must be set"); - Assert.hasText(resoledBaseUrl, "Moonshot base URL must be set"); - - return new MoonshotApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java similarity index 52% rename from auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java rename to auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java index 4202ce2ef..52fd055e4 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -16,23 +16,14 @@ package org.springframework.ai.model.zhipuai.autoconfigure; -import java.util.List; - import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -40,7 +31,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -49,43 +39,20 @@ import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; /** - * {@link AutoConfiguration Auto-configuration} for ZhiPuAI. + * Embedding {@link AutoConfiguration Auto-configuration} for ZhiPuAI. * * @author Geng Rong + * @author Ilayaperumal Gopinathan */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @ConditionalOnClass(ZhiPuAiApi.class) -@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class, - ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class }) -public class ZhiPuAiAutoConfiguration { +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.ZHIPUAI, + matchIfMissing = true) +@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiEmbeddingProperties.class }) +public class ZhiPuAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ZHIPUAI, - matchIfMissing = true) - public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, - ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver, - toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.ZHIPUAI, - matchIfMissing = true) public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, @@ -116,34 +83,4 @@ public class ZhiPuAiAutoConfiguration { return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.ZHIPUAI, - matchIfMissing = true) - public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, - ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() - : commonProperties.getApiKey(); - - String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - - Assert.hasText(apiKey, "ZhiPuAI API key must be set"); - Assert.hasText(baseUrl, "ZhiPuAI base URL must be set"); - - var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler); - - return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java new file mode 100644 index 000000000..4bf5a17e2 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2023-2025 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.zhipuai.autoconfigure; + +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +/** + * Image {@link AutoConfiguration Auto-configuration} for ZhiPuAI. + * + * @author Geng Rong + * @author Ilayaperumal Gopinathan + */ +@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) +@ConditionalOnClass(ZhiPuAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.ZHIPUAI, + matchIfMissing = true) +@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiImageProperties.class }) +public class ZhiPuAiImageAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, + ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler) { + + String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() + : commonProperties.getApiKey(); + + String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() + : commonProperties.getBaseUrl(); + + Assert.hasText(apiKey, "ZhiPuAI API key must be set"); + Assert.hasText(baseUrl, "ZhiPuAI base URL must be set"); + + var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler); + + return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1cf07eded..59e61da56 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration +org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration +org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration +org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java index d36e0a691..51d3e1998 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java @@ -25,13 +25,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.embedding.EmbeddingResponse; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; @@ -51,12 +51,12 @@ public class ZhiPuAiAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)); + .withConfiguration( + AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class)); @Test void generate() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)).run(context -> { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); String response = chatModel.call("Hello"); assertThat(response).isNotEmpty(); @@ -66,7 +66,7 @@ public class ZhiPuAiAutoConfigurationIT { @Test void generateStreaming() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)).run(context -> { ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); String response = responseFlux.collectList() @@ -82,30 +82,33 @@ public class ZhiPuAiAutoConfigurationIT { @Test void embedding() { - this.contextRunner.run(context -> { - ZhiPuAiEmbeddingModel embeddingModel = context.getBean(ZhiPuAiEmbeddingModel.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiEmbeddingAutoConfiguration.class)) + .run(context -> { + ZhiPuAiEmbeddingModel embeddingModel = context.getBean(ZhiPuAiEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); + EmbeddingResponse embeddingResponse = embeddingModel + .embedForResponse(List.of("Hello World", "World is big and salvation is near")); + assertThat(embeddingResponse.getResults()).hasSize(2); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); + assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); + assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); + assertThat(embeddingModel.dimensions()).isEqualTo(1024); + }); } @Test void generateImage() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.image.options.size=1024x1024").run(context -> { - ZhiPuAiImageModel ImageModel = context.getBean(ZhiPuAiImageModel.class); - ImageResponse imageResponse = ImageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl()); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(ZhiPuAiImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.zhipuai.image.options.size=1024x1024") + .run(context -> { + ZhiPuAiImageModel ImageModel = context.getBean(ZhiPuAiImageModel.class); + ImageResponse imageResponse = ImageModel.call(new ImagePrompt("forest")); + assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty(); + logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl()); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java index 79b98dfed..e18351d80 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java @@ -20,8 +20,8 @@ import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; @@ -51,7 +51,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(ZhiPuAiChatProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -80,7 +80,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.chat.options.temperature=0.55") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(ZhiPuAiChatProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -106,7 +106,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -133,7 +133,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -157,7 +157,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.image.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(ZhiPuAiImageProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -183,7 +183,7 @@ public class ZhiPuAiPropertiesTests { "spring.ai.zhipuai.image.options.model=MODEL_XYZ") // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(ZhiPuAiImageProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -247,17 +247,14 @@ public class ZhiPuAiPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(ZhiPuAiChatProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("Embedding-2"); - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); @@ -295,7 +292,7 @@ public class ZhiPuAiPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); @@ -318,7 +315,7 @@ public class ZhiPuAiPropertiesTests { ) // @formatter:on .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(ZhiPuAiImageProperties.class); var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); @@ -337,16 +334,16 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isEmpty(); assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty(); @@ -356,7 +353,7 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.embedding=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty(); @@ -369,16 +366,16 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isEmpty(); assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty(); @@ -388,7 +385,7 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.chat=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty(); @@ -402,16 +399,16 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.image=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isEmpty(); assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isEmpty(); }); new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty(); @@ -421,7 +418,7 @@ public class ZhiPuAiPropertiesTests { .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", "spring.ai.model.image=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java index 12dc83d6d..6b640953c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java @@ -25,14 +25,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -52,7 +52,7 @@ public class FunctionCallbackInPromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)); + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java index c384ca4f6..0ab9a8758 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -26,14 +26,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallingOptions; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -56,7 +56,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java index b7e636592..edee1c1bd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java @@ -25,14 +25,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -54,7 +54,7 @@ public class ZhipuAiFunctionCallbackIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) + RestClientAutoConfiguration.class, ZhiPuAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml index 6eebc504d..c632a6553 100644 --- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml +++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml @@ -28,6 +28,7 @@ org.springframework.ai spring-ai-core ${project.parent.version} + true diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/pom.xml b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/pom.xml deleted file mode 100644 index 3a77ea3ed..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - 4.0.0 - - org.springframework.ai - spring-ai - 1.0.0-SNAPSHOT - ../../../../pom.xml - - spring-ai-autoconfigure-observation-chat - jar - Spring AI Chat Observation Auto Configuration - Spring AI Chat Observation Auto Configuration - https://github.com/spring-projects/spring-ai - - - https://github.com/spring-projects/spring-ai - git://github.com/spring-projects/spring-ai.git - git@github.com:spring-projects/spring-ai.git - - - - - - - org.springframework.ai - spring-ai-core - ${parent.version} - - - - io.micrometer - micrometer-tracing-bridge-otel - true - - - - - org.springframework.boot - spring-boot-starter - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - org.springframework.ai - spring-ai-test - ${project.parent.version} - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.mockito - mockito-core - test - - - - diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java deleted file mode 100644 index f439263fd..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import java.util.List; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext; -import org.springframework.ai.chat.client.observation.ChatClientObservationContext; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelObservationContext; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext; -import org.springframework.ai.image.observation.ImageModelObservationContext; -import org.springframework.ai.model.observation.ErrorLoggingObservationHandler; -import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Auto-configuration for Spring AI chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = { "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration" }) -@ConditionalOnClass(ChatModel.class) -@EnableConfigurationProperties({ ChatObservationProperties.class }) -public class ChatObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ChatObservationAutoConfiguration.class); - - private static void logPromptContentWarning() { - logger.warn( - "You have enabled the inclusion of the prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - private static void logCompletionWarning() { - logger.warn( - "You have enabled the inclusion of the completion content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - ChatModelMeterObservationHandler chatModelMeterObservationHandler(ObjectProvider meterRegistry) { - return new ChatModelMeterObservationHandler(meterRegistry.getObject()); - } - - /** - * The chat content is typically too big to be included in an observation as span - * attributes. That's why the preferred way to store it is as span events, which are - * supported by OpenTelemetry but not yet surfaced through the Micrometer APIs. This - * primary/fallback configuration is a temporary solution until - * https://github.com/micrometer-metrics/micrometer/issues/5238 is delivered. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OtelTracer.class) - @ConditionalOnBean(OtelTracer.class) - static class PrimaryChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationHandler(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationHandler chatModelCompletionObservationHandler() { - logCompletionWarning(); - return new ChatModelCompletionObservationHandler(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass("io.micrometer.tracing.otel.bridge.OtelTracer") - static class FallbackChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationFilter chatModelPromptObservationFilter() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationFilter(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationFilter chatModelCompletionObservationFilter() { - logCompletionWarning(); - return new ChatModelCompletionObservationFilter(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Tracer.class) - @ConditionalOnBean(Tracer.class) - static class TracingChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-error-logging", - havingValue = "true") - public ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer) { - return new ErrorLoggingObservationHandler(tracer, - List.of(EmbeddingModelObservationContext.class, ImageModelObservationContext.class, - ChatModelObservationContext.class, ChatClientObservationContext.class, - AdvisorObservationContext.class, VectorStoreObservationContext.class)); - } - - } - -} diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java deleted file mode 100644 index cd353ac0b..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ChatObservationProperties.CONFIG_PREFIX) -public class ChatObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.observations"; - - /** - * Whether to include the completion content in the observations. - */ - private boolean includeCompletion = false; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - /** - * Whether to include error logging in the observations. - */ - private boolean includeErrorLogging = false; - - public boolean isIncludeCompletion() { - return this.includeCompletion; - } - - public void setIncludeCompletion(boolean includeCompletion) { - this.includeCompletion = includeCompletion; - } - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - - public boolean isIncludeErrorLogging() { - return this.includeErrorLogging; - } - - public void setIncludeErrorLogging(boolean includeErrorLogging) { - this.includeErrorLogging = includeErrorLogging; - } - -} diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java deleted file mode 100644 index 6032da63a..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for chat observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.chat.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 9493b7587..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2025-2025 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. -# -org.springframework.ai.autoconfigure.chat.observation.ChatObservationAutoConfiguration diff --git a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java b/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java deleted file mode 100644 index d16010981..000000000 --- a/auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ChatObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ChatObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ChatObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationFilter.class)); - } - - @Test - void promptHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void completionFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationFilter.class)); - } - - @Test - void completionHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - -} diff --git a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/pom.xml b/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/pom.xml deleted file mode 100644 index 93f3fc00b..000000000 --- a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - 4.0.0 - - org.springframework.ai - spring-ai - 1.0.0-SNAPSHOT - ../../../../pom.xml - - spring-ai-autoconfigure-observation-embedding - jar - Spring AI Embedding Observation Auto Configuration - Spring AI Embedding Observation Auto Configuration - https://github.com/spring-projects/spring-ai - - - https://github.com/spring-projects/spring-ai - git://github.com/spring-projects/spring-ai.git - git@github.com:spring-projects/spring-ai.git - - - - - - - org.springframework.ai - spring-ai-core - ${parent.version} - - - - - org.springframework.boot - spring-boot-starter - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - org.springframework.ai - spring-ai-test - ${project.parent.version} - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.mockito - mockito-core - test - - - - diff --git a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java b/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java deleted file mode 100644 index ba1d61886..000000000 --- a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.autoconfigure.embedding.observation; - -import io.micrometer.core.instrument.MeterRegistry; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI embedding model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration") -@ConditionalOnClass(EmbeddingModel.class) -public class EmbeddingObservationAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - EmbeddingModelMeterObservationHandler embeddingModelMeterObservationHandler( - ObjectProvider meterRegistry) { - return new EmbeddingModelMeterObservationHandler(meterRegistry.getObject()); - } - -} diff --git a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java b/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java deleted file mode 100644 index 2275a771e..000000000 --- a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for embedding observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.embedding.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 162692d8a..000000000 --- a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2025-2025 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. -# -org.springframework.ai.autoconfigure.embedding.observation.EmbeddingObservationAutoConfiguration diff --git a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java b/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java deleted file mode 100644 index 42037c9e0..000000000 --- a/auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.autoconfigure.embedding.observation; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link EmbeddingObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class EmbeddingObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddingObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(EmbeddingModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(EmbeddingModelMeterObservationHandler.class)); - } - -} diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/pom.xml b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/pom.xml deleted file mode 100644 index 2172c6c32..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - 4.0.0 - - org.springframework.ai - spring-ai - 1.0.0-SNAPSHOT - ../../../../pom.xml - - spring-ai-autoconfigure-observation-image - jar - Spring AI Image Observation Auto Configuration - Spring AI Image Observation Auto Configuration - https://github.com/spring-projects/spring-ai - - - https://github.com/spring-projects/spring-ai - git://github.com/spring-projects/spring-ai.git - git@github.com:spring-projects/spring-ai.git - - - - - - - org.springframework.ai - spring-ai-core - ${parent.version} - - - - - org.springframework.boot - spring-boot-starter - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - org.springframework.ai - spring-ai-test - ${project.parent.version} - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.mockito - mockito-core - test - - - - diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java deleted file mode 100644 index a89a42102..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration.class") -@ConditionalOnClass(ImageModel.class) -@EnableConfigurationProperties({ ImageObservationProperties.class }) -public class ImageObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ImageObservationAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ImageObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ImageModelPromptContentObservationFilter imageModelPromptObservationFilter() { - logger.warn( - "You have enabled the inclusion of the image prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new ImageModelPromptContentObservationFilter(); - } - -} diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java deleted file mode 100644 index 3e454ee8d..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ImageObservationProperties.CONFIG_PREFIX) -public class ImageObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.image.observations"; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - -} diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java deleted file mode 100644 index 559f61df9..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for image observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.image.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 78af4bcff..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright 2025-2025 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. -# -org.springframework.ai.autoconfigure.image.observation.ImageObservationAutoConfiguration diff --git a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java b/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java deleted file mode 100644 index deb0a22ad..000000000 --- a/auto-configurations/observation/image/spring-ai-autoconfigure-observation-image/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ImageObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ImageObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ImageObservationAutoConfiguration.class)); - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationFilter.class)); - } - - @Test - void promptFilterEnabled() { - this.contextRunner.withPropertyValues("spring.ai.image.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationFilter.class)); - } - -} diff --git a/pom.xml b/pom.xml index 6643a452d..a869c5626 100644 --- a/pom.xml +++ b/pom.xml @@ -91,8 +91,6 @@ auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-weaviate auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-pgvector - spring-ai-spring-boot-autoconfigure - spring-ai-retry spring-ai-spring-boot-docker-compose spring-ai-spring-boot-testcontainers diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java index f937d7671..b85d41e56 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java @@ -46,11 +46,11 @@ public class SpringAIModels { public static final String QIANFAN = "qianfan"; - public static final String STABILITY = "stabilityai"; + public static final String STABILITY_AI = "stabilityai"; public static final String TRANSFORMERS = "transformers"; - public static final String VERTEX_AI = "vertex"; + public static final String VERTEX_AI = "vertexai"; public static final String WATSONX_AI = "watsonx"; diff --git a/spring-ai-integration-tests/pom.xml b/spring-ai-integration-tests/pom.xml index 948d8b624..303e31d7b 100644 --- a/spring-ai-integration-tests/pom.xml +++ b/spring-ai-integration-tests/pom.xml @@ -56,14 +56,14 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai ${project.parent.version} test org.springframework.ai - spring-ai-pgvector-store-spring-boot-starter + spring-ai-starter-vector-store-pgvector ${project.parent.version} test diff --git a/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/TestcontainersConfiguration.java b/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/TestcontainersConfiguration.java index 2db1e5bc4..524dbf9f3 100644 --- a/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/TestcontainersConfiguration.java +++ b/spring-ai-integration-tests/src/test/java/org/springframework/ai/integration/tests/TestcontainersConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.ai.integration.tests; +import java.time.Duration; + import org.testcontainers.containers.PostgreSQLContainer; import org.springframework.boot.test.context.TestConfiguration; @@ -33,7 +35,7 @@ class TestcontainersConfiguration { @Bean @ServiceConnection PostgreSQLContainer pgvectorContainer() { - return new PostgreSQLContainer<>("pgvector/pgvector:pg17"); + return new PostgreSQLContainer<>("pgvector/pgvector:pg17").withStartupTimeout(Duration.ofMinutes(6)); } } diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml deleted file mode 100644 index 3cb3683fe..000000000 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ /dev/null @@ -1,365 +0,0 @@ - - - 4.0.0 - - org.springframework.ai - spring-ai - 1.0.0-SNAPSHOT - - spring-ai-spring-boot-autoconfigure - jar - Spring AI Auto Configuration - Spring AI Auto Configuration - https://github.com/spring-projects/spring-ai - - - https://github.com/spring-projects/spring-ai - git://github.com/spring-projects/spring-ai.git - git@github.com:spring-projects/spring-ai.git - - - - - - com.google.protobuf - protobuf-java - ${protobuf-java.version} - true - - - - - - org.springframework.boot - spring-boot-starter - - - - io.micrometer - micrometer-tracing-bridge-otel - true - - - - - org.springframework.ai - spring-ai-mcp - ${project.parent.version} - true - - - - io.modelcontextprotocol.sdk - mcp-spring-webflux - ${mcp.sdk.version} - true - - - - io.modelcontextprotocol.sdk - mcp-spring-webmvc - ${mcp.sdk.version} - true - - - - - org.springframework.ai - spring-ai-openai - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-postgresml - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-azure-openai - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-oci-genai - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-huggingface - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-ollama - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-transformers - ${project.parent.version} - true - - - - - io.netty - netty-codec-http2 - 4.1.100.Final - true - - - - - org.springframework.ai - spring-ai-cassandra-store - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-vertex-ai-embedding - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-vertex-ai-gemini - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-stability-ai - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-watsonx-ai - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-bedrock - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-bedrock-converse - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-mistral-ai - ${project.parent.version} - true - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - org.springframework.ai - spring-ai-anthropic - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-minimax - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-zhipuai - ${project.parent.version} - true - - - - - org.springframework.ai - spring-ai-moonshot - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-qianfan - ${project.parent.version} - true - - - - software.amazon.awssdk - apache-client - ${awssdk.version} - true - - - - - org.springframework.ai - spring-ai-azure-cosmos-db-store - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-couchbase-store - ${project.parent.version} - true - - - - - - org.springframework.ai - spring-ai-test - ${project.parent.version} - test - - - - com.oracle.oci.sdk - oci-java-sdk-common - ${oci-sdk-version} - test - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-starter-jdbc - test - - - - org.springframework.boot - spring-boot-testcontainers - test - - - - org.testcontainers - testcontainers - test - - - - org.testcontainers - ollama - test - - - - org.testcontainers - postgresql - test - - - - org.testcontainers - junit-jupiter - test - - - - com.redis - testcontainers-redis - 2.2.0 - test - - - - org.awaitility - awaitility - test - - - - org.testcontainers - localstack - test - - - - org.skyscreamer - jsonassert - 1.5.0 - test - - - - io.micrometer - micrometer-observation-test - test - - - - com.fasterxml.jackson.module - jackson-module-kotlin - test - - - - org.testcontainers - couchbase - test - - - - org.testcontainers - cassandra - test - - - - - diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java deleted file mode 100644 index f439263fd..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import java.util.List; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext; -import org.springframework.ai.chat.client.observation.ChatClientObservationContext; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelObservationContext; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext; -import org.springframework.ai.image.observation.ImageModelObservationContext; -import org.springframework.ai.model.observation.ErrorLoggingObservationHandler; -import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Auto-configuration for Spring AI chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = { "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration" }) -@ConditionalOnClass(ChatModel.class) -@EnableConfigurationProperties({ ChatObservationProperties.class }) -public class ChatObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ChatObservationAutoConfiguration.class); - - private static void logPromptContentWarning() { - logger.warn( - "You have enabled the inclusion of the prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - private static void logCompletionWarning() { - logger.warn( - "You have enabled the inclusion of the completion content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - ChatModelMeterObservationHandler chatModelMeterObservationHandler(ObjectProvider meterRegistry) { - return new ChatModelMeterObservationHandler(meterRegistry.getObject()); - } - - /** - * The chat content is typically too big to be included in an observation as span - * attributes. That's why the preferred way to store it is as span events, which are - * supported by OpenTelemetry but not yet surfaced through the Micrometer APIs. This - * primary/fallback configuration is a temporary solution until - * https://github.com/micrometer-metrics/micrometer/issues/5238 is delivered. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OtelTracer.class) - @ConditionalOnBean(OtelTracer.class) - static class PrimaryChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationHandler(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationHandler chatModelCompletionObservationHandler() { - logCompletionWarning(); - return new ChatModelCompletionObservationHandler(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass("io.micrometer.tracing.otel.bridge.OtelTracer") - static class FallbackChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationFilter chatModelPromptObservationFilter() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationFilter(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationFilter chatModelCompletionObservationFilter() { - logCompletionWarning(); - return new ChatModelCompletionObservationFilter(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Tracer.class) - @ConditionalOnBean(Tracer.class) - static class TracingChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-error-logging", - havingValue = "true") - public ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer) { - return new ErrorLoggingObservationHandler(tracer, - List.of(EmbeddingModelObservationContext.class, ImageModelObservationContext.class, - ChatModelObservationContext.class, ChatClientObservationContext.class, - AdvisorObservationContext.class, VectorStoreObservationContext.class)); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java deleted file mode 100644 index cd353ac0b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ChatObservationProperties.CONFIG_PREFIX) -public class ChatObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.observations"; - - /** - * Whether to include the completion content in the observations. - */ - private boolean includeCompletion = false; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - /** - * Whether to include error logging in the observations. - */ - private boolean includeErrorLogging = false; - - public boolean isIncludeCompletion() { - return this.includeCompletion; - } - - public void setIncludeCompletion(boolean includeCompletion) { - this.includeCompletion = includeCompletion; - } - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - - public boolean isIncludeErrorLogging() { - return this.includeErrorLogging; - } - - public void setIncludeErrorLogging(boolean includeErrorLogging) { - this.includeErrorLogging = includeErrorLogging; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java deleted file mode 100644 index 6032da63a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for chat observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.chat.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java deleted file mode 100644 index ba1d61886..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.autoconfigure.embedding.observation; - -import io.micrometer.core.instrument.MeterRegistry; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI embedding model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration") -@ConditionalOnClass(EmbeddingModel.class) -public class EmbeddingObservationAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - EmbeddingModelMeterObservationHandler embeddingModelMeterObservationHandler( - ObjectProvider meterRegistry) { - return new EmbeddingModelMeterObservationHandler(meterRegistry.getObject()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java deleted file mode 100644 index 2275a771e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for embedding observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.embedding.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java deleted file mode 100644 index a89a42102..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration.class") -@ConditionalOnClass(ImageModel.class) -@EnableConfigurationProperties({ ImageObservationProperties.class }) -public class ImageObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ImageObservationAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ImageObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ImageModelPromptContentObservationFilter imageModelPromptObservationFilter() { - logger.warn( - "You have enabled the inclusion of the image prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new ImageModelPromptContentObservationFilter(); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java deleted file mode 100644 index 3e454ee8d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ImageObservationProperties.CONFIG_PREFIX) -public class ImageObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.image.observations"; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java deleted file mode 100644 index 559f61df9..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for image observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.autoconfigure.image.observation; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatProperties.java deleted file mode 100644 index 95ddedd7e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure; - -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.anthropic.AnthropicChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Anthropic Chat autoconfiguration properties. - * - * @author Christian Tzolov - * @author Alexandros Pappas - * @since 1.0.0 - */ -@ConfigurationProperties(AnthropicChatProperties.CONFIG_PREFIX) -public class AnthropicChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.anthropic.chat"; - - /** - * Enable Anthropic chat model. - */ - private boolean enabled = true; - - /** - * Client lever Ollama options. Use this property to configure generative temperature, - * topK and topP and alike parameters. The null values are ignored defaulting to the - * generative's defaults. - */ - @NestedConfigurationProperty - private AnthropicChatOptions options = AnthropicChatOptions.builder() - .model(AnthropicChatModel.DEFAULT_MODEL_NAME) - .maxTokens(AnthropicChatModel.DEFAULT_MAX_TOKENS) - .temperature(AnthropicChatModel.DEFAULT_TEMPERATURE) - .build(); - - public AnthropicChatOptions getOptions() { - return this.options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java deleted file mode 100644 index 7f56f2d05..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure; - -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Anthropic API connection properties. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@ConfigurationProperties(AnthropicConnectionProperties.CONFIG_PREFIX) -public class AnthropicConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.anthropic"; - - /** - * Anthropic API access key. - */ - private String apiKey; - - /** - * Anthropic API base URL. - */ - private String baseUrl = AnthropicApi.DEFAULT_BASE_URL; - - /** - * Anthropic API version. - */ - private String version = AnthropicApi.DEFAULT_ANTHROPIC_VERSION; - - /** - * Beta features version. Such as tools-2024-04-04 or - * max-tokens-3-5-sonnet-2024-07-15. - */ - private String betaVersion = AnthropicApi.DEFAULT_ANTHROPIC_BETA_VERSION; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getVersion() { - return this.version; - } - - public void setVersion(String version) { - this.version = version; - } - - public String getBetaVersion() { - return this.betaVersion; - } - - public void setBetaVersion(String betaVersion) { - this.betaVersion = betaVersion; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAIClientBuilderCustomizer.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAIClientBuilderCustomizer.java deleted file mode 100644 index 0d4e3061b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAIClientBuilderCustomizer.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.springframework.ai.model.azure.openai.autoconfigure; - -import com.azure.ai.openai.OpenAIClientBuilder; - -/** - * Callback interface that can be implemented by beans wishing to customize the - * {@link OpenAIClientBuilder} whilst retaining the default auto-configuration. - * - * @author Manuel Andreo Garcia - * @since 1.0.0-M6 - */ -@FunctionalInterface -public interface AzureOpenAIClientBuilderCustomizer { - - /** - * Customize the {@link OpenAIClientBuilder}. - * @param clientBuilder the {@link OpenAIClientBuilder} to customize - */ - void customize(OpenAIClientBuilder clientBuilder); - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionProperties.java deleted file mode 100644 index 8a1fe78db..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAudioTranscriptionProperties.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for Azure OpenAI audio transcription. - * - * @author Piotr Olaszewski - */ -@ConfigurationProperties(AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX) -public class AzureOpenAiAudioTranscriptionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.azure.openai.audio.transcription"; - - /** - * Enable AzureOpenAI audio transcription model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private AzureOpenAiAudioTranscriptionOptions options = AzureOpenAiAudioTranscriptionOptions.builder().build(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public AzureOpenAiAudioTranscriptionOptions getOptions() { - return this.options; - } - - public void setOptions(AzureOpenAiAudioTranscriptionOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatProperties.java deleted file mode 100644 index 42c3a2c2a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatProperties.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -@ConfigurationProperties(AzureOpenAiChatProperties.CONFIG_PREFIX) -public class AzureOpenAiChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.azure.openai.chat"; - - public static final String DEFAULT_DEPLOYMENT_NAME = "gpt-4o"; - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable Azure OpenAI chat model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private AzureOpenAiChatOptions options = AzureOpenAiChatOptions.builder() - .deploymentName(DEFAULT_DEPLOYMENT_NAME) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public AzureOpenAiChatOptions getOptions() { - return this.options; - } - - public void setOptions(AzureOpenAiChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java deleted file mode 100644 index a3c2191cc..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(AzureOpenAiConnectionProperties.CONFIG_PREFIX) -public class AzureOpenAiConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.azure.openai"; - - /** - * Azure OpenAI API key. From the Azure AI OpenAI `Keys and Endpoint` section under - * `Resource Management`. - */ - private String apiKey; - - /** - * (non Azure) OpenAI API key. Used to authenticate with the OpenAI service, instead - * of Azure OpenAI. This automatically sets the endpoint to https://api.openai.com/v1. - */ - private String openAiApiKey; - - /** - * Azure OpenAI API endpoint. From the Azure AI OpenAI `Keys and Endpoint` section - * under `Resource Management`. - */ - private String endpoint; - - private Map customHeaders = new HashMap<>(); - - public String getEndpoint() { - return this.endpoint; - } - - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getOpenAiApiKey() { - return this.openAiApiKey; - } - - public void setOpenAiApiKey(String openAiApiKey) { - this.openAiApiKey = openAiApiKey; - } - - public Map getCustomHeaders() { - return this.customHeaders; - } - - public void setCustomHeaders(Map customHeaders) { - this.customHeaders = customHeaders; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingProperties.java deleted file mode 100644 index a0d4697fa..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiEmbeddingProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingOptions; -import org.springframework.ai.document.MetadataMode; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.util.Assert; - -@ConfigurationProperties(AzureOpenAiEmbeddingProperties.CONFIG_PREFIX) -public class AzureOpenAiEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.azure.openai.embedding"; - - /** - * Enable Azure OpenAI embedding model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private AzureOpenAiEmbeddingOptions options = AzureOpenAiEmbeddingOptions.builder() - .deploymentName("text-embedding-ada-002") - .build(); - - private MetadataMode metadataMode = MetadataMode.EMBED; - - public AzureOpenAiEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(AzureOpenAiEmbeddingOptions options) { - Assert.notNull(options, "Options must not be null"); - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - Assert.notNull(metadataMode, "Metadata mode must not be null"); - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageOptionsProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageOptionsProperties.java deleted file mode 100644 index 51465008b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiImageOptionsProperties.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import org.springframework.ai.azure.openai.AzureOpenAiImageOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for Azure OpenAI image generation options. - * - * @author Benoit Moussaud - * @since 1.0.0 M1 - */ -@ConfigurationProperties(AzureOpenAiImageOptionsProperties.CONFIG_PREFIX) -public class AzureOpenAiImageOptionsProperties { - - public static final String CONFIG_PREFIX = "spring.ai.azure.openai.image"; - - /** - * Enable Azure OpenAI chat client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private AzureOpenAiImageOptions options = AzureOpenAiImageOptions.builder().build(); - - public AzureOpenAiImageOptions getOptions() { - return this.options; - } - - public void setOptions(AzureOpenAiImageOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java deleted file mode 100644 index f6abd70db..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfiguration.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure; - -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.regions.providers.AwsRegionProvider; -import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.StringUtils; - -/** - * {@link Configuration} for AWS connection. - * - * @author Christian Tzolov - * @author Wei Jiang - */ -@Configuration -@EnableConfigurationProperties({ BedrockAwsConnectionProperties.class }) -public class BedrockAwsConnectionConfiguration { - - @Bean - @ConditionalOnMissingBean - public AwsCredentialsProvider credentialsProvider(BedrockAwsConnectionProperties properties) { - - if (StringUtils.hasText(properties.getAccessKey()) && StringUtils.hasText(properties.getSecretKey())) { - - if (StringUtils.hasText(properties.getSessionToken())) { - return StaticCredentialsProvider.create(AwsSessionCredentials.create(properties.getAccessKey(), - properties.getSecretKey(), properties.getSessionToken())); - } - - return StaticCredentialsProvider - .create(AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey())); - } - - return DefaultCredentialsProvider.create(); - } - - @Bean - @ConditionalOnMissingBean - public AwsRegionProvider regionProvider(BedrockAwsConnectionProperties properties) { - - if (StringUtils.hasText(properties.getRegion())) { - return new StaticRegionProvider(properties.getRegion()); - } - - return DefaultAwsRegionProviderChain.builder().build(); - } - - static class StaticRegionProvider implements AwsRegionProvider { - - private final Region region; - - StaticRegionProvider(String region) { - try { - this.region = Region.of(region); - } - catch (IllegalArgumentException e) { - throw new IllegalArgumentException("The region '" + region + "' is not a valid region!", e); - } - } - - @Override - public Region getRegion() { - return this.region; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java deleted file mode 100644 index cc47b675b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionProperties.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure; - -import java.time.Duration; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Bedrock AWS connection. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(BedrockAwsConnectionProperties.CONFIG_PREFIX) -public class BedrockAwsConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.bedrock.aws"; - - /** - * AWS region to use. Defaults to us-east-1. - */ - private String region = "us-east-1"; - - /** - * AWS access key. - */ - private String accessKey; - - /** - * AWS secret key. - */ - private String secretKey; - - /** - * AWS session token. (optional) When provided the AwsSessionCredentials are used. - * Otherwise the AwsBasicCredentials are used. - */ - private String sessionToken; - - /** - * Set model timeout, Defaults 5 min. - */ - private Duration timeout = Duration.ofMinutes(5L); - - public String getRegion() { - return this.region; - } - - public void setRegion(String awsRegion) { - this.region = awsRegion; - } - - public String getAccessKey() { - return this.accessKey; - } - - public void setAccessKey(String accessKey) { - this.accessKey = accessKey; - } - - public String getSecretKey() { - return this.secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - - public Duration getTimeout() { - return this.timeout; - } - - public void setTimeout(Duration timeout) { - this.timeout = timeout; - } - - public String getSessionToken() { - return this.sessionToken; - } - - public void setSessionToken(String sessionToken) { - this.sessionToken = sessionToken; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java deleted file mode 100644 index ce60731c0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.cohere; - -import com.fasterxml.jackson.databind.ObjectMapper; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.regions.providers.AwsRegionProvider; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; -import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; - -/** - * {@link AutoConfiguration Auto-configuration} for Bedrock Cohere Embedding Model. - * - * @author Christian Tzolov - * @author Wei Jiang - * @since 0.8.0 - */ -@AutoConfiguration -@ConditionalOnClass(CohereEmbeddingBedrockApi.class) -@EnableConfigurationProperties({ BedrockCohereEmbeddingProperties.class, BedrockAwsConnectionProperties.class }) -@ConditionalOnProperty(prefix = BedrockCohereEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") -@Import(BedrockAwsConnectionConfiguration.class) -public class BedrockCohereEmbeddingAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) - public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider, - AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { - return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - objectMapper, awsProperties.getTimeout()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(CohereEmbeddingBedrockApi.class) - public BedrockCohereEmbeddingModel cohereEmbeddingModel(CohereEmbeddingBedrockApi cohereEmbeddingApi, - BedrockCohereEmbeddingProperties properties) { - - return new BedrockCohereEmbeddingModel(cohereEmbeddingApi, properties.getOptions()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java deleted file mode 100644 index 131a9832a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingProperties.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.cohere; - -import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingOptions; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest.InputType; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Bedrock Cohere Embedding autoconfiguration properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(BedrockCohereEmbeddingProperties.CONFIG_PREFIX) -public class BedrockCohereEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.bedrock.cohere.embedding"; - - /** - * Enable Bedrock Cohere Embedding Model. False by default. - */ - private boolean enabled = false; - - /** - * Bedrock Cohere Embedding generative name. Defaults to - * 'cohere.embed-multilingual-v3'. - */ - private String model = CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id(); - - @NestedConfigurationProperty - private BedrockCohereEmbeddingOptions options = BedrockCohereEmbeddingOptions.builder() - .inputType(InputType.SEARCH_DOCUMENT) - .truncate(CohereEmbeddingRequest.Truncate.NONE) - .build(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getModel() { - return this.model; - } - - public void setModel(String model) { - this.model = model; - } - - public BedrockCohereEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(BedrockCohereEmbeddingOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java deleted file mode 100644 index 1cfea4c07..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.titan; - -import com.fasterxml.jackson.databind.ObjectMapper; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.regions.providers.AwsRegionProvider; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; -import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; -import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; - -/** - * {@link AutoConfiguration Auto-configuration} for Bedrock Titan Embedding Model. - * - * @author Christian Tzolov - * @author Wei Jiang - * @since 0.8.0 - */ -@AutoConfiguration -@ConditionalOnClass(TitanEmbeddingBedrockApi.class) -@EnableConfigurationProperties({ BedrockTitanEmbeddingProperties.class, BedrockAwsConnectionProperties.class }) -@ConditionalOnProperty(prefix = BedrockTitanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") -@Import(BedrockAwsConnectionConfiguration.class) -public class BedrockTitanEmbeddingAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class }) - public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider credentialsProvider, - AwsRegionProvider regionProvider, BedrockTitanEmbeddingProperties properties, - BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) { - return new TitanEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(), - objectMapper, awsProperties.getTimeout()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(TitanEmbeddingBedrockApi.class) - public BedrockTitanEmbeddingModel titanEmbeddingModel(TitanEmbeddingBedrockApi titanEmbeddingApi, - BedrockTitanEmbeddingProperties properties) { - - return new BedrockTitanEmbeddingModel(titanEmbeddingApi).withInputType(properties.getInputType()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java deleted file mode 100644 index 89d790a9f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.model.chat.client.autoconfigure; - -import io.micrometer.observation.ObservationRegistry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.ChatClientCustomizer; -import org.springframework.ai.chat.client.observation.ChatClientInputContentObservationFilter; -import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Scope; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for {@link ChatClient}. - *

- * This will produce a {@link ChatClient.Builder ChatClient.Builder} bean with the - * {@code prototype} scope, meaning each injection point will receive a newly cloned - * instance of the builder. - * - * @author Christian Tzolov - * @author Mark Pollack - * @author Josh Long - * @author Arjen Poutsma - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration -@ConditionalOnClass(ChatClient.class) -@EnableConfigurationProperties(ChatClientBuilderProperties.class) -@ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) -public class ChatClientAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ChatClientAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider customizerProvider) { - ChatClientBuilderConfigurer configurer = new ChatClientBuilderConfigurer(); - configurer.setChatClientCustomizers(customizerProvider.orderedStream().toList()); - return configurer; - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - ChatClient.Builder builder = ChatClient.builder(chatModel, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP), - observationConvention.getIfUnique(() -> null)); - return chatClientBuilderConfigurer.configure(builder); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX + ".observations", name = "include-input", - havingValue = "true") - ChatClientInputContentObservationFilter chatClientInputContentObservationFilter() { - logger.warn( - "You have enabled the inclusion of the input content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new ChatClientInputContentObservationFilter(); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java deleted file mode 100644 index f0448058c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.model.chat.client.autoconfigure; - -import java.util.List; - -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.ChatClientCustomizer; - -/** - * Builder for configuring a {@link ChatClient.Builder}. - * - * @author Christian Tzolov - * @author Mark Pollack - * @author Josh Long - * @author Arjen Poutsma - * @since 1.0.0 M1 - */ -public class ChatClientBuilderConfigurer { - - private List customizers; - - void setChatClientCustomizers(List customizers) { - this.customizers = customizers; - } - - /** - * Configure the specified {@link ChatClient.Builder}. The builder can be further - * tuned and default settings can be overridden. - * @param builder the {@link ChatClient.Builder} instance to configure - * @return the configured builder - */ - public ChatClient.Builder configure(ChatClient.Builder builder) { - applyCustomizers(builder); - return builder; - } - - private void applyCustomizers(ChatClient.Builder builder) { - if (this.customizers != null) { - for (ChatClientCustomizer customizer : this.customizers) { - customizer.customize(builder); - } - } - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java deleted file mode 100644 index 0ca9023d7..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.chat.client.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for the chat client builder. - * - * @author Christian Tzolov - * @author Mark Pollack - * @author Josh Long - * @author Arjen Poutsma - * @since 1.0.0 - */ -@ConfigurationProperties(ChatClientBuilderProperties.CONFIG_PREFIX) -public class ChatClientBuilderProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.client"; - - /** - * Enable chat client builder. - */ - private boolean enabled = true; - - private Observations observations = new Observations(); - - public Observations getObservations() { - return this.observations; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public static class Observations { - - /** - * Whether to include the input content in the observations. - */ - private boolean includeInput = false; - - public boolean isIncludeInput() { - return this.includeInput; - } - - public void setIncludeInput(boolean includeCompletion) { - this.includeInput = includeCompletion; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java deleted file mode 100644 index 00ea424c4..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.model.chat.memory.autoconfigure; - -/** - * Configuration properties for the common chat memory. - * - * @author Mick Semb Wever - * @since 1.0.0 - */ -public class CommonChatMemoryProperties { - - private boolean initializeSchema = true; - - public boolean isInitializeSchema() { - return this.initializeSchema; - } - - public void setInitializeSchema(boolean initializeSchema) { - this.initializeSchema = initializeSchema; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java deleted file mode 100644 index 937a49beb..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.chat.memory.autoconfigure.cassandra; - -import com.datastax.oss.driver.api.core.CqlSession; - -import org.springframework.ai.chat.memory.cassandra.CassandraChatMemory; -import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * {@link AutoConfiguration Auto-configuration} for {@link CassandraChatMemory}. - * - * @author Mick Semb Wever - * @author Jihoon Kim - * @since 1.0.0 - */ -@AutoConfiguration(after = CassandraAutoConfiguration.class) -@ConditionalOnClass({ CassandraChatMemory.class, CqlSession.class }) -@EnableConfigurationProperties(CassandraChatMemoryProperties.class) -public class CassandraChatMemoryAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public CassandraChatMemory chatMemory(CassandraChatMemoryProperties properties, CqlSession cqlSession) { - - var builder = CassandraChatMemoryConfig.builder().withCqlSession(cqlSession); - - builder = builder.withKeyspaceName(properties.getKeyspace()) - .withTableName(properties.getTable()) - .withAssistantColumnName(properties.getAssistantColumn()) - .withUserColumnName(properties.getUserColumn()); - - if (!properties.isInitializeSchema()) { - builder = builder.disallowSchemaChanges(); - } - if (null != properties.getTimeToLive()) { - builder = builder.withTimeToLive(properties.getTimeToLive()); - } - - return CassandraChatMemory.create(builder.build()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java deleted file mode 100644 index 14ae105ab..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.model.chat.memory.autoconfigure.cassandra; - -import java.time.Duration; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.chat.memory.autoconfigure.CommonChatMemoryProperties; -import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.lang.Nullable; - -/** - * Configuration properties for Cassandra chat memory. - * - * @author Mick Semb Wever - * @author Jihoon Kim - * @since 1.0.0 - */ -@ConfigurationProperties(CassandraChatMemoryProperties.CONFIG_PREFIX) -public class CassandraChatMemoryProperties extends CommonChatMemoryProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.memory.cassandra"; - - private static final Logger logger = LoggerFactory.getLogger(CassandraChatMemoryProperties.class); - - private String keyspace = CassandraChatMemoryConfig.DEFAULT_KEYSPACE_NAME; - - private String table = CassandraChatMemoryConfig.DEFAULT_TABLE_NAME; - - private String assistantColumn = CassandraChatMemoryConfig.DEFAULT_ASSISTANT_COLUMN_NAME; - - private String userColumn = CassandraChatMemoryConfig.DEFAULT_USER_COLUMN_NAME; - - private Duration timeToLive = null; - - public String getKeyspace() { - return this.keyspace; - } - - public void setKeyspace(String keyspace) { - this.keyspace = keyspace; - } - - public String getTable() { - return this.table; - } - - public void setTable(String table) { - this.table = table; - } - - public String getAssistantColumn() { - return this.assistantColumn; - } - - public void setAssistantColumn(String assistantColumn) { - this.assistantColumn = assistantColumn; - } - - public String getUserColumn() { - return this.userColumn; - } - - public void setUserColumn(String userColumn) { - this.userColumn = userColumn; - } - - @Nullable - public Duration getTimeToLive() { - return this.timeToLive; - } - - public void setTimeToLive(Duration timeToLive) { - this.timeToLive = timeToLive; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java deleted file mode 100644 index 81aee1992..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.model.chat.observation.autoconfigure; - -import java.util.List; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext; -import org.springframework.ai.chat.client.observation.ChatClientObservationContext; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelObservationContext; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext; -import org.springframework.ai.image.observation.ImageModelObservationContext; -import org.springframework.ai.model.observation.ErrorLoggingObservationHandler; -import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Auto-configuration for Spring AI chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = { "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration" }) -@ConditionalOnClass(ChatModel.class) -@EnableConfigurationProperties({ ChatObservationProperties.class }) -public class ChatObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ChatObservationAutoConfiguration.class); - - private static void logPromptContentWarning() { - logger.warn( - "You have enabled the inclusion of the prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - private static void logCompletionWarning() { - logger.warn( - "You have enabled the inclusion of the completion content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - ChatModelMeterObservationHandler chatModelMeterObservationHandler(ObjectProvider meterRegistry) { - return new ChatModelMeterObservationHandler(meterRegistry.getObject()); - } - - /** - * The chat content is typically too big to be included in an observation as span - * attributes. That's why the preferred way to store it is as span events, which are - * supported by OpenTelemetry but not yet surfaced through the Micrometer APIs. This - * primary/fallback configuration is a temporary solution until - * https://github.com/micrometer-metrics/micrometer/issues/5238 is delivered. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(OtelTracer.class) - @ConditionalOnBean(OtelTracer.class) - static class PrimaryChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationHandler(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationHandler chatModelCompletionObservationHandler() { - logCompletionWarning(); - return new ChatModelCompletionObservationHandler(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingClass("io.micrometer.tracing.otel.bridge.OtelTracer") - static class FallbackChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ChatModelPromptContentObservationFilter chatModelPromptObservationFilter() { - logPromptContentWarning(); - return new ChatModelPromptContentObservationFilter(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion", - havingValue = "true") - ChatModelCompletionObservationFilter chatModelCompletionObservationFilter() { - logCompletionWarning(); - return new ChatModelCompletionObservationFilter(); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(Tracer.class) - @ConditionalOnBean(Tracer.class) - static class TracingChatContentObservationConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-error-logging", - havingValue = "true") - public ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer) { - return new ErrorLoggingObservationHandler(tracer, - List.of(EmbeddingModelObservationContext.class, ImageModelObservationContext.class, - ChatModelObservationContext.class, ChatClientObservationContext.class, - AdvisorObservationContext.class, VectorStoreObservationContext.class)); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java deleted file mode 100644 index be37d1b26..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.chat.observation.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for chat model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ChatObservationProperties.CONFIG_PREFIX) -public class ChatObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.chat.observations"; - - /** - * Whether to include the completion content in the observations. - */ - private boolean includeCompletion = false; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - /** - * Whether to include error logging in the observations. - */ - private boolean includeErrorLogging = false; - - public boolean isIncludeCompletion() { - return this.includeCompletion; - } - - public void setIncludeCompletion(boolean includeCompletion) { - this.includeCompletion = includeCompletion; - } - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - - public boolean isIncludeErrorLogging() { - return this.includeErrorLogging; - } - - public void setIncludeErrorLogging(boolean includeErrorLogging) { - this.includeErrorLogging = includeErrorLogging; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java deleted file mode 100644 index 66bef736b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for chat observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.model.chat.observation.autoconfigure; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java deleted file mode 100644 index ee0902343..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.model.embedding.observation.autoconfigure; - -import io.micrometer.core.instrument.MeterRegistry; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI embedding model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration") -@ConditionalOnClass(EmbeddingModel.class) -public class EmbeddingObservationAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnBean(MeterRegistry.class) - EmbeddingModelMeterObservationHandler embeddingModelMeterObservationHandler( - ObjectProvider meterRegistry) { - return new EmbeddingModelMeterObservationHandler(meterRegistry.getObject()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java deleted file mode 100644 index 654b59b6f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for embedding observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.model.embedding.observation.autoconfigure; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java deleted file mode 100644 index a79a6517d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.model.huggingface.autoconfigure; - -import org.springframework.ai.huggingface.HuggingfaceChatModel; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -@AutoConfiguration -@ConditionalOnClass(HuggingfaceChatModel.class) -@EnableConfigurationProperties(HuggingfaceChatProperties.class) -public class HuggingfaceChatAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = HuggingfaceChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public HuggingfaceChatModel huggingfaceChatModel(HuggingfaceChatProperties huggingfaceChatProperties) { - return new HuggingfaceChatModel(huggingfaceChatProperties.getApiKey(), huggingfaceChatProperties.getUrl()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java deleted file mode 100644 index 371df322f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.model.huggingface.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Hugging Face chat model. - * - * @author Christian Tzolov - * @author Josh Long - * @author Mark Pollack - * @author Thomas Vitale - */ -@ConfigurationProperties(HuggingfaceChatProperties.CONFIG_PREFIX) -public class HuggingfaceChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.huggingface.chat"; - - /** - * API Key to authenticate with the Inference Endpoint. - */ - private String apiKey; - - /** - * URL of the Inference Endpoint. - */ - private String url; - - /** - * Enable Hugging Face chat model. - */ - private boolean enabled = true; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getUrl() { - return this.url; - } - - public void setUrl(String url) { - this.url = url; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java deleted file mode 100644 index f97e12f03..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.model.image.observation.autoconfigure; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * Auto-configuration for Spring AI image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@AutoConfiguration( - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration.class") -@ConditionalOnClass(ImageModel.class) -@EnableConfigurationProperties({ ImageObservationProperties.class }) -public class ImageObservationAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(ImageObservationAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ImageObservationProperties.CONFIG_PREFIX, name = "include-prompt", - havingValue = "true") - ImageModelPromptContentObservationFilter imageModelPromptObservationFilter() { - logger.warn( - "You have enabled the inclusion of the image prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!"); - return new ImageModelPromptContentObservationFilter(); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java deleted file mode 100644 index 7d372d986..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.model.image.observation.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for image model observations. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(ImageObservationProperties.CONFIG_PREFIX) -public class ImageObservationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.image.observations"; - - /** - * Whether to include the prompt content in the observations. - */ - private boolean includePrompt = false; - - public boolean isIncludePrompt() { - return this.includePrompt; - } - - public void setIncludePrompt(boolean includePrompt) { - this.includePrompt = includePrompt; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java deleted file mode 100644 index 983cc5a16..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for image observation. - */ -@NonNullApi -@NonNullFields -package org.springframework.ai.model.image.observation.autoconfigure; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java deleted file mode 100644 index a04b1655c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for MiniMax chat model. - * - * @author Geng Rong - */ -@ConfigurationProperties(MiniMaxChatProperties.CONFIG_PREFIX) -public class MiniMaxChatProperties extends MiniMaxParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.minimax.chat"; - - public static final String DEFAULT_CHAT_MODEL = MiniMaxApi.ChatModel.ABAB_5_5_Chat.value; - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable MiniMax chat model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private MiniMaxChatOptions options = MiniMaxChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public MiniMaxChatOptions getOptions() { - return this.options; - } - - public void setOptions(MiniMaxChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java deleted file mode 100644 index e41162a39..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(MiniMaxConnectionProperties.CONFIG_PREFIX) -public class MiniMaxConnectionProperties extends MiniMaxParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.minimax"; - - public static final String DEFAULT_BASE_URL = "https://api.minimax.chat"; - - public MiniMaxConnectionProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java deleted file mode 100644 index 465a573f9..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; -import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for MiniMax embedding model. - * - * @author Geng Rong - */ -@ConfigurationProperties(MiniMaxEmbeddingProperties.CONFIG_PREFIX) -public class MiniMaxEmbeddingProperties extends MiniMaxParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.minimax.embedding"; - - public static final String DEFAULT_EMBEDDING_MODEL = MiniMaxApi.EmbeddingModel.Embo_01.value; - - /** - * Enable MiniMax embedding model. - */ - private boolean enabled = true; - - private MetadataMode metadataMode = MetadataMode.EMBED; - - @NestedConfigurationProperty - private MiniMaxEmbeddingOptions options = MiniMaxEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build(); - - public MiniMaxEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(MiniMaxEmbeddingOptions options) { - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java deleted file mode 100644 index 2927c6ea3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -/** - * @author Geng Rong - */ -class MiniMaxParentProperties { - - private String apiKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java deleted file mode 100644 index 19a782a8e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -import org.springframework.ai.mistralai.MistralAiChatOptions; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for Mistral AI chat. - * - * @author Ricken Bazolo - * @author Christian Tzolov - * @author Thomas Vitale - * @author Alexandros Pappas - * @since 0.8.1 - */ -@ConfigurationProperties(MistralAiChatProperties.CONFIG_PREFIX) -public class MistralAiChatProperties extends MistralAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.mistralai.chat"; - - public static final String DEFAULT_CHAT_MODEL = MistralAiApi.ChatModel.SMALL.getValue(); - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - private static final Double DEFAULT_TOP_P = 1.0; - - private static final Boolean IS_ENABLED = false; - - /** - * Enable OpenAI chat model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private MistralAiChatOptions options = MistralAiChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .safePrompt(!IS_ENABLED) - .topP(DEFAULT_TOP_P) - .build(); - - public MistralAiChatProperties() { - super.setBaseUrl(MistralAiCommonProperties.DEFAULT_BASE_URL); - } - - public MistralAiChatOptions getOptions() { - return this.options; - } - - public void setOptions(MistralAiChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java deleted file mode 100644 index 3d1a90bfc..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Common properties for Mistral AI. - * - * @author Ricken Bazolo - * @author Christian Tzolov - * @since 0.8.1 - */ -@ConfigurationProperties(MistralAiCommonProperties.CONFIG_PREFIX) -public class MistralAiCommonProperties extends MistralAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.mistralai"; - - public static final String DEFAULT_BASE_URL = "https://api.mistral.ai"; - - public MistralAiCommonProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java deleted file mode 100644 index f8c3498be..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.mistralai.MistralAiEmbeddingOptions; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for MistralAI embedding model. - * - * @author Ricken Bazolo - * @since 0.8.1 - */ -@ConfigurationProperties(MistralAiEmbeddingProperties.CONFIG_PREFIX) -public class MistralAiEmbeddingProperties extends MistralAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.mistralai.embedding"; - - public static final String DEFAULT_EMBEDDING_MODEL = MistralAiApi.EmbeddingModel.EMBED.getValue(); - - public static final String DEFAULT_ENCODING_FORMAT = "float"; - - public MetadataMode metadataMode = MetadataMode.EMBED; - - /** - * Enable MistralAI embedding model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private MistralAiEmbeddingOptions options = MistralAiEmbeddingOptions.builder() - .withModel(DEFAULT_EMBEDDING_MODEL) - .withEncodingFormat(DEFAULT_ENCODING_FORMAT) - .build(); - - public MistralAiEmbeddingProperties() { - super.setBaseUrl(MistralAiCommonProperties.DEFAULT_BASE_URL); - } - - public MistralAiEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(MistralAiEmbeddingOptions options) { - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java deleted file mode 100644 index 0da844cc2..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -/** - * Parent properties for Mistral AI. - * - * @author Ricken Bazolo - * @since 0.8.1 - */ -public class MistralAiParentProperties { - - private String apiKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java deleted file mode 100644 index 3933eba3e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure; - -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for Moonshot chat client. - * - * @author Geng Rong - * @author Alexandros Pappas - */ -@ConfigurationProperties(MoonshotChatProperties.CONFIG_PREFIX) -public class MoonshotChatProperties extends MoonshotParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.moonshot.chat"; - - public static final String DEFAULT_CHAT_MODEL = MoonshotApi.ChatModel.MOONSHOT_V1_8K.getValue(); - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable Moonshot chat client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private MoonshotChatOptions options = MoonshotChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public MoonshotChatOptions getOptions() { - return this.options; - } - - public void setOptions(MoonshotChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java deleted file mode 100644 index 7d9461f91..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Parent properties for Moonshot. - * - * @author Geng Rong - */ -@ConfigurationProperties(MoonshotCommonProperties.CONFIG_PREFIX) -public class MoonshotCommonProperties extends MoonshotParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.moonshot"; - - public static final String DEFAULT_BASE_URL = "https://api.moonshot.cn"; - - public MoonshotCommonProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java deleted file mode 100644 index 23a07ced3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure; - -/** - * Parent properties for Moonshot. - * - * @author Geng Rong - */ -public class MoonshotParentProperties { - - private String apiKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java deleted file mode 100644 index ebed22636..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import org.springframework.ai.oci.cohere.OCICohereChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for OCI Cohere chat model. - * - * @author Anders Swanson - */ -@ConfigurationProperties(OCICohereChatModelProperties.CONFIG_PREFIX) -public class OCICohereChatModelProperties { - - public static final String CONFIG_PREFIX = "spring.ai.oci.genai.cohere.chat"; - - private static final String DEFAULT_SERVING_MODE = ServingMode.ON_DEMAND.getMode(); - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - private boolean enabled; - - @NestedConfigurationProperty - private OCICohereChatOptions options = OCICohereChatOptions.builder() - .servingMode(DEFAULT_SERVING_MODE) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public OCICohereChatOptions getOptions() { - return this.options; - } - - public void setOptions(OCICohereChatOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java deleted file mode 100644 index 69f33d910..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import java.nio.file.Paths; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.util.StringUtils; - -/** - * Configuration properties for OCI connection. - * - * @author Anders Swanson - */ -@ConfigurationProperties(OCIConnectionProperties.CONFIG_PREFIX) -public class OCIConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.oci.genai"; - - private static final String DEFAULT_PROFILE = "DEFAULT"; - - private AuthenticationType authenticationType = AuthenticationType.FILE; - - private String profile; - - private String file = Paths.get(System.getProperty("user.home"), ".oci", "config").toString(); - - private String tenantId; - - private String userId; - - private String fingerprint; - - private String privateKey; - - private String passPhrase; - - private String region = "us-chicago-1"; - - private String endpoint; - - public String getRegion() { - return this.region; - } - - public void setRegion(String region) { - this.region = region; - } - - public String getPassPhrase() { - return this.passPhrase; - } - - public void setPassPhrase(String passPhrase) { - this.passPhrase = passPhrase; - } - - public String getPrivateKey() { - return this.privateKey; - } - - public void setPrivateKey(String privateKey) { - this.privateKey = privateKey; - } - - public String getFingerprint() { - return this.fingerprint; - } - - public void setFingerprint(String fingerprint) { - this.fingerprint = fingerprint; - } - - public String getUserId() { - return this.userId; - } - - public void setUserId(String userId) { - this.userId = userId; - } - - public String getTenantId() { - return this.tenantId; - } - - public void setTenantId(String tenantId) { - this.tenantId = tenantId; - } - - public String getFile() { - return this.file; - } - - public void setFile(String file) { - this.file = file; - } - - public String getProfile() { - return StringUtils.hasText(this.profile) ? this.profile : DEFAULT_PROFILE; - } - - public void setProfile(String profile) { - this.profile = profile; - } - - public AuthenticationType getAuthenticationType() { - return this.authenticationType; - } - - public void setAuthenticationType(AuthenticationType authenticationType) { - this.authenticationType = authenticationType; - } - - public String getEndpoint() { - return this.endpoint; - } - - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - - public enum AuthenticationType { - - FILE("file"), INSTANCE_PRINCIPAL("instance-principal"), WORKLOAD_IDENTITY("workload-identity"), - SIMPLE("simple"); - - private final String authType; - - AuthenticationType(String authType) { - this.authType = authType; - } - - public String getAuthType() { - return this.authType; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java deleted file mode 100644 index 3ba1ea993..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import com.oracle.bmc.generativeaiinference.model.EmbedTextDetails; - -import org.springframework.ai.oci.OCIEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for OCI embedding model. - * - * @author Anders Swanson - */ -@ConfigurationProperties(OCIEmbeddingModelProperties.CONFIG_PREFIX) -public class OCIEmbeddingModelProperties { - - public static final String CONFIG_PREFIX = "spring.ai.oci.genai.embedding"; - - private ServingMode servingMode = ServingMode.ON_DEMAND; - - private EmbedTextDetails.Truncate truncate = EmbedTextDetails.Truncate.End; - - private String compartment; - - private String model; - - private boolean enabled; - - public OCIEmbeddingOptions getEmbeddingOptions() { - return OCIEmbeddingOptions.builder() - .compartment(this.compartment) - .model(this.model) - .servingMode(this.servingMode.getMode()) - .truncate(this.truncate) - .build(); - } - - public ServingMode getServingMode() { - return this.servingMode; - } - - public void setServingMode(ServingMode servingMode) { - this.servingMode = servingMode; - } - - public String getCompartment() { - return this.compartment; - } - - public void setCompartment(String compartment) { - this.compartment = compartment; - } - - public String getModel() { - return this.model; - } - - public void setModel(String model) { - this.model = model; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public EmbedTextDetails.Truncate getTruncate() { - return this.truncate; - } - - public void setTruncate(EmbedTextDetails.Truncate truncate) { - this.truncate = truncate; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java deleted file mode 100644 index 49b09ae13..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -/** - * OCI serving mode. - * - * @author Anders Swanson - */ -public enum ServingMode { - - ON_DEMAND("on-demand"), DEDICATED("dedicated"); - - private final String mode; - - ServingMode(String mode) { - this.mode = mode; - } - - public String getMode() { - return this.mode; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java deleted file mode 100644 index 7d7948f8e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2023-2025 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.ollama.autoconfigure; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.OllamaEmbeddingModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.management.ModelManagementOptions; -import org.springframework.ai.ollama.management.PullModelStrategy; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * {@link AutoConfiguration Auto-configuration} for Ollama Chat Client. - * - * @author Christian Tzolov - * @author Eddú Meléndez - * @author Thomas Vitale - * @since 0.8.0 - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@ConditionalOnClass(OllamaApi.class) -@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaEmbeddingProperties.class, - OllamaConnectionProperties.class, OllamaInitializationProperties.class }) -@ImportAutoConfiguration(classes = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class, - WebClientAutoConfiguration.class }) -public class OllamaAutoConfiguration { - - @Bean - @ConditionalOnMissingBean(OllamaConnectionDetails.class) - public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) { - return new PropertiesOllamaConnectionDetails(properties); - } - - @Bean - @ConditionalOnMissingBean - public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider) { - return new OllamaApi(connectionDetails.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder)); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OllamaChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties, - OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy() - : PullModelStrategy.NEVER; - - var chatModel = OllamaChatModel.builder() - .ollamaApi(ollamaApi) - .defaultOptions(properties.getOptions()) - .toolCallingManager(toolCallingManager) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .modelManagementOptions( - new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(), - initProperties.getTimeout(), initProperties.getMaxRetries())) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OllamaEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties, - OllamaInitializationProperties initProperties, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude() - ? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER; - - var embeddingModel = OllamaEmbeddingModel.builder() - .ollamaApi(ollamaApi) - .defaultOptions(properties.getOptions()) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy, - initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(), - initProperties.getMaxRetries())) - .build(); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails { - - private final OllamaConnectionProperties properties; - - PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) { - this.properties = properties; - } - - @Override - public String getBaseUrl() { - return this.properties.getBaseUrl(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java deleted file mode 100644 index 22569554a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Ollama Chat autoconfiguration properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(OllamaChatProperties.CONFIG_PREFIX) -public class OllamaChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.ollama.chat"; - - /** - * Enable Ollama chat model. - */ - private boolean enabled = true; - - /** - * Client lever Ollama options. Use this property to configure generative temperature, - * topK and topP and alike parameters. The null values are ignored defaulting to the - * generative's defaults. - */ - @NestedConfigurationProperty - private OllamaOptions options = OllamaOptions.builder().model(OllamaModel.MISTRAL.id()).build(); - - public String getModel() { - return this.options.getModel(); - } - - public void setModel(String model) { - this.options.setModel(model); - } - - public OllamaOptions getOptions() { - return this.options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java deleted file mode 100644 index eef0f2af3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; - -/** - * Connection details for an Ollama service. - * - * @author Eddú Meléndez - */ -public interface OllamaConnectionDetails extends ConnectionDetails { - - String getBaseUrl(); - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java deleted file mode 100644 index 58e7e1c62..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Ollama connection autoconfiguration properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(OllamaConnectionProperties.CONFIG_PREFIX) -public class OllamaConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.ollama"; - - /** - * Base URL where Ollama API server is running. - */ - private String baseUrl = "http://localhost:11434"; - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java deleted file mode 100644 index 287c52999..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Ollama Embedding autoconfiguration properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(OllamaEmbeddingProperties.CONFIG_PREFIX) -public class OllamaEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.ollama.embedding"; - - /** - * Enable Ollama embedding model. - */ - private boolean enabled = true; - - /** - * Client lever Ollama options. Use this property to configure generative temperature, - * topK and topP and alike parameters. The null values are ignored defaulting to the - * generative's defaults. - */ - @NestedConfigurationProperty - private OllamaOptions options = OllamaOptions.builder().model(OllamaModel.MXBAI_EMBED_LARGE.id()).build(); - - public String getModel() { - return this.options.getModel(); - } - - public void setModel(String model) { - this.options.setModel(model); - } - - public OllamaOptions getOptions() { - return this.options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java deleted file mode 100644 index 31f92fc3d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import java.time.Duration; -import java.util.List; - -import org.springframework.ai.ollama.management.PullModelStrategy; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Ollama initialization configuration properties. - * - * @author Thomas Vitale - * @since 1.0.0 - */ -@ConfigurationProperties(OllamaInitializationProperties.CONFIG_PREFIX) -public class OllamaInitializationProperties { - - public static final String CONFIG_PREFIX = "spring.ai.ollama.init"; - - /** - * Chat models initialization settings. - */ - private final ModelTypeInit chat = new ModelTypeInit(); - - /** - * Embedding models initialization settings. - */ - private final ModelTypeInit embedding = new ModelTypeInit(); - - /** - * Whether to pull models at startup-time and how. - */ - private PullModelStrategy pullModelStrategy = PullModelStrategy.NEVER; - - /** - * How long to wait for a model to be pulled. - */ - private Duration timeout = Duration.ofMinutes(5); - - /** - * Maximum number of retries for the model pull operation. - */ - private int maxRetries = 0; - - public PullModelStrategy getPullModelStrategy() { - return this.pullModelStrategy; - } - - public void setPullModelStrategy(PullModelStrategy pullModelStrategy) { - this.pullModelStrategy = pullModelStrategy; - } - - public ModelTypeInit getChat() { - return this.chat; - } - - public ModelTypeInit getEmbedding() { - return this.embedding; - } - - public Duration getTimeout() { - return this.timeout; - } - - public void setTimeout(Duration timeout) { - this.timeout = timeout; - } - - public int getMaxRetries() { - return this.maxRetries; - } - - public void setMaxRetries(int maxRetries) { - this.maxRetries = maxRetries; - } - - public static class ModelTypeInit { - - /** - * Include this type of models in the initialization task. - */ - private boolean include = true; - - /** - * Additional models to initialize besides the ones configured via default - * properties. - */ - private List additionalModels = List.of(); - - public boolean isInclude() { - return this.include; - } - - public void setInclude(boolean include) { - this.include = include; - } - - public List getAdditionalModels() { - return this.additionalModels; - } - - public void setAdditionalModels(List additionalModels) { - this.additionalModels = additionalModels; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java deleted file mode 100644 index 8508e5d8e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.openai.OpenAiAudioSpeechOptions; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for OpenAI audio speech. - * - * Default values for required options are model = tts_1, response format = mp3, voice = - * alloy, and speed = 1. - * - * @author Ahmed Yousri - * @author Stefan Vassilev - */ -@ConfigurationProperties(OpenAiAudioSpeechProperties.CONFIG_PREFIX) -public class OpenAiAudioSpeechProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.audio.speech"; - - public static final String DEFAULT_SPEECH_MODEL = OpenAiAudioApi.TtsModel.TTS_1.getValue(); - - private static final Float SPEED = 1.0f; - - private static final OpenAiAudioApi.SpeechRequest.Voice VOICE = OpenAiAudioApi.SpeechRequest.Voice.ALLOY; - - private static final OpenAiAudioApi.SpeechRequest.AudioResponseFormat DEFAULT_RESPONSE_FORMAT = OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3; - - /** - * Enable OpenAI audio speech model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder() - .model(DEFAULT_SPEECH_MODEL) - .responseFormat(DEFAULT_RESPONSE_FORMAT) - .voice(VOICE) - .speed(SPEED) - .build(); - - public OpenAiAudioSpeechOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiAudioSpeechOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java deleted file mode 100644 index eb4a967f0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.openai.OpenAiAudioTranscriptionOptions; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -@ConfigurationProperties(OpenAiAudioTranscriptionProperties.CONFIG_PREFIX) -public class OpenAiAudioTranscriptionProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.audio.transcription"; - - public static final String DEFAULT_TRANSCRIPTION_MODEL = OpenAiAudioApi.WhisperModel.WHISPER_1.getValue(); - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - private static final OpenAiAudioApi.TranscriptResponseFormat DEFAULT_RESPONSE_FORMAT = OpenAiAudioApi.TranscriptResponseFormat.TEXT; - - /** - * Enable OpenAI audio transcription model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private OpenAiAudioTranscriptionOptions options = OpenAiAudioTranscriptionOptions.builder() - .model(DEFAULT_TRANSCRIPTION_MODEL) - .temperature(DEFAULT_TEMPERATURE.floatValue()) - .responseFormat(DEFAULT_RESPONSE_FORMAT) - .build(); - - public OpenAiAudioTranscriptionOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiAudioTranscriptionOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java deleted file mode 100644 index 1d6ddc51a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2023-2025 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.openai.autoconfigure; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.micrometer.observation.ObservationRegistry; -import org.jetbrains.annotations.NotNull; - -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationConvention; -import org.springframework.ai.model.SimpleApiKey; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.OpenAiModerationModel; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.ai.openai.api.OpenAiModerationApi; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * {@link AutoConfiguration Auto-configuration} for OpenAI. - * - * @author Christian Tzolov - * @author Stefan Vassilev - * @author Thomas Vitale - * @author Ilayaperumal Gopinathan - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@ConditionalOnClass(OpenAiApi.class) -@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class, - OpenAiEmbeddingProperties.class, OpenAiImageProperties.class, OpenAiAudioTranscriptionProperties.class, - OpenAiAudioSpeechProperties.class, OpenAiModerationProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -public class OpenAiAutoConfiguration { - - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( - OpenAiParentProperties commonProperties, OpenAiParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() - : commonProperties.getApiKey(); - String projectId = StringUtils.hasText(modelProperties.getProjectId()) ? modelProperties.getProjectId() - : commonProperties.getProjectId(); - String organizationId = StringUtils.hasText(modelProperties.getOrganizationId()) - ? modelProperties.getOrganizationId() : commonProperties.getOrganizationId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(projectId)) { - connectionHeaders.put("OpenAI-Project", List.of(projectId)); - } - if (StringUtils.hasText(organizationId)) { - connectionHeaders.put("OpenAI-Organization", List.of(organizationId)); - } - - Assert.hasText(baseUrl, - "OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai." - + modelType + ".base-url property."); - Assert.hasText(apiKey, - "OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai." - + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, CollectionUtils.toMultiValueMap(connectionHeaders)); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiChatModel openAiChatModel(OpenAiConnectionProperties commonProperties, - OpenAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ToolCallingManager toolCallingManager, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var openAiApi = openAiApi(chatProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "chat"); - - var chatModel = OpenAiChatModel.builder() - .openAiApi(openAiApi) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .retryTemplate(retryTemplate) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiConnectionProperties commonProperties, - OpenAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var openAiApi = openAiApi(embeddingProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "embedding"); - - var embeddingModel = new OpenAiEmbeddingModel(openAiApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties, - RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, - ResponseErrorHandler responseErrorHandler, String modelType) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, - modelType); - - return OpenAiApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .completionsPath(chatProperties.getCompletionsPath()) - .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH) - .restClientBuilder(restClientBuilder) - .webClientBuilder(webClientBuilder) - .responseErrorHandler(responseErrorHandler) - .build(); - } - - private OpenAiApi openAiApi(OpenAiEmbeddingProperties embeddingProperties, - OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, - modelType); - - return OpenAiApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .completionsPath(OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH) - .embeddingsPath(embeddingProperties.getEmbeddingsPath()) - .restClientBuilder(restClientBuilder) - .webClientBuilder(webClientBuilder) - .responseErrorHandler(responseErrorHandler) - .build(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProperties, - OpenAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var openAiImageApi = OpenAiImageApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(imageModel::setObservationConvention); - - return imageModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnectionProperties commonProperties, - OpenAiAudioTranscriptionProperties transcriptionProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, transcriptionProperties, - "transcription"); - - var openAiAudioApi = OpenAiAudioApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - - return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate); - - } - - @Bean - @ConditionalOnMissingBean - public OpenAiModerationModel openAiModerationClient(OpenAiConnectionProperties commonProperties, - OpenAiModerationProperties moderationProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, moderationProperties, - "moderation"); - - var openAiModerationApi = OpenAiModerationApi.builder() - .baseUrl(resolved.baseUrl) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - return new OpenAiModerationModel(openAiModerationApi, retryTemplate) - .withDefaultOptions(moderationProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiAudioSpeechProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiAudioSpeechModel openAiAudioSpeechClient(OpenAiConnectionProperties commonProperties, - OpenAiAudioSpeechProperties speechProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechProperties, - "speach"); - - var openAiAudioApi = OpenAiAudioApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - - return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap headers) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java deleted file mode 100644 index 3cef1db88..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -@ConfigurationProperties(OpenAiChatProperties.CONFIG_PREFIX) -public class OpenAiChatProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.chat"; - - public static final String DEFAULT_CHAT_MODEL = "gpt-4o-mini"; - - public static final String DEFAULT_COMPLETIONS_PATH = "/v1/chat/completions"; - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable OpenAI chat model. - */ - private boolean enabled = true; - - private String completionsPath = DEFAULT_COMPLETIONS_PATH; - - @NestedConfigurationProperty - private OpenAiChatOptions options = OpenAiChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public OpenAiChatOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getCompletionsPath() { - return this.completionsPath; - } - - public void setCompletionsPath(String completionsPath) { - this.completionsPath = completionsPath; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java deleted file mode 100644 index 41f00e303..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(OpenAiConnectionProperties.CONFIG_PREFIX) -public class OpenAiConnectionProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai"; - - public static final String DEFAULT_BASE_URL = "https://api.openai.com"; - - public OpenAiConnectionProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java deleted file mode 100644 index c70e82c66..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.openai.OpenAiEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -@ConfigurationProperties(OpenAiEmbeddingProperties.CONFIG_PREFIX) -public class OpenAiEmbeddingProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.embedding"; - - public static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002"; - - public static final String DEFAULT_EMBEDDINGS_PATH = "/v1/embeddings"; - - /** - * Enable OpenAI embedding model. - */ - private boolean enabled = true; - - private MetadataMode metadataMode = MetadataMode.EMBED; - - private String embeddingsPath = DEFAULT_EMBEDDINGS_PATH; - - @NestedConfigurationProperty - private OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build(); - - public OpenAiEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiEmbeddingOptions options) { - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getEmbeddingsPath() { - return this.embeddingsPath; - } - - public void setEmbeddingsPath(String embeddingsPath) { - this.embeddingsPath = embeddingsPath; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java deleted file mode 100644 index ac7b11c80..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.openai.OpenAiImageOptions; -import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * OpenAI Image autoconfiguration properties. - * - * @author Thomas Vitale - * @since 0.8.0 - */ -@ConfigurationProperties(OpenAiImageProperties.CONFIG_PREFIX) -public class OpenAiImageProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.image"; - - public static final String DEFAULT_IMAGE_MODEL = OpenAiImageApi.ImageModel.DALL_E_3.getValue(); - - /** - * Enable OpenAI image model. - */ - private boolean enabled = true; - - /** - * Options for OpenAI Image API. - */ - @NestedConfigurationProperty - private OpenAiImageOptions options = OpenAiImageOptions.builder().model(DEFAULT_IMAGE_MODEL).build(); - - public OpenAiImageOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiImageOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java deleted file mode 100644 index a268f93bd..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.springframework.ai.openai.OpenAiModerationOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * OpenAI Moderation autoconfiguration properties. - * - * @author Ahmed Yousri - * @since 0.9.0 - */ -@ConfigurationProperties(OpenAiModerationProperties.CONFIG_PREFIX) -public class OpenAiModerationProperties extends OpenAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.openai.moderation"; - - /** - * Options for OpenAI Moderation API. - */ - @NestedConfigurationProperty - private OpenAiModerationOptions options = OpenAiModerationOptions.builder().build(); - - public OpenAiModerationOptions getOptions() { - return this.options; - } - - public void setOptions(OpenAiModerationOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java deleted file mode 100644 index 9b3f29a0e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -/** - * Internal parent properties for the OpenAI properties. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -class OpenAiParentProperties { - - private String apiKey; - - private String baseUrl; - - private String projectId; - - private String organizationId; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getProjectId() { - return this.projectId; - } - - public void setProjectId(String projectId) { - this.projectId = projectId; - } - - public String getOrganizationId() { - return this.organizationId; - } - - public void setOrganizationId(String organizationId) { - this.organizationId = organizationId; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java deleted file mode 100644 index ae0092ee8..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.model.postgresml.autoconfigure; - -import org.springframework.ai.postgresml.PostgresMlEmbeddingModel; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.jdbc.core.JdbcTemplate; - -/** - * Auto-configuration class for PostgresMlEmbeddingModel. - * - * @author Utkarsh Srivastava - * @author Christian Tzolov - */ -@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class) -@ConditionalOnClass(PostgresMlEmbeddingModel.class) -@EnableConfigurationProperties(PostgresMlEmbeddingProperties.class) -public class PostgresMlAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = PostgresMlEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public PostgresMlEmbeddingModel postgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, - PostgresMlEmbeddingProperties embeddingProperties) { - - return new PostgresMlEmbeddingModel(jdbcTemplate, embeddingProperties.getOptions(), - embeddingProperties.isCreateExtension()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java deleted file mode 100644 index 5cd3ca0df..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.model.postgresml.autoconfigure; - -import java.util.Map; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.postgresml.PostgresMlEmbeddingModel; -import org.springframework.ai.postgresml.PostgresMlEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.util.Assert; - -/** - * Configuration properties for Postgres ML. - * - * @author Utkarsh Srivastava - * @author Christian Tzolov - */ -@ConfigurationProperties(PostgresMlEmbeddingProperties.CONFIG_PREFIX) -public class PostgresMlEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.postgresml.embedding"; - - /** - * Enable Postgres ML embedding model. - */ - private boolean enabled = true; - - /** - * Create the extensions required for embedding - */ - private boolean createExtension; - - @NestedConfigurationProperty - private PostgresMlEmbeddingOptions options = PostgresMlEmbeddingOptions.builder() - .transformer(PostgresMlEmbeddingModel.DEFAULT_TRANSFORMER_MODEL) - .vectorType(PostgresMlEmbeddingModel.VectorType.PG_ARRAY) - .kwargs(Map.of()) - .metadataMode(MetadataMode.EMBED) - .build(); - - public PostgresMlEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(PostgresMlEmbeddingOptions options) { - Assert.notNull(options, "options must not be null."); - Assert.notNull(options.getTransformer(), "transformer must not be null."); - Assert.notNull(options.getVectorType(), "vectorType must not be null."); - Assert.notNull(options.getKwargs(), "kwargs must not be null."); - Assert.notNull(options.getMetadataMode(), "metadataMode must not be null."); - - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isCreateExtension() { - return this.createExtension; - } - - public void setCreateExtension(boolean createExtension) { - this.createExtension = createExtension; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java deleted file mode 100644 index a48870d9d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.ai.qianfan.api.QianFanImageApi; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; - -/** - * {@link AutoConfiguration Auto-configuration} for QianFan Chat, Embedding, and Image - * Models. - * - * @author Geng Rong - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) -@ConditionalOnClass(QianFanApi.class) -@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class, - QianFanEmbeddingProperties.class, QianFanImageProperties.class }) -public class QianFanAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties, - QianFanChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var qianFanApi = qianFanApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), chatProperties.getSecretKey(), - commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), - responseErrorHandler); - - var chatModel = new QianFanChatModel(qianFanApi, chatProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties, - QianFanEmbeddingProperties embeddingProperties, - ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(), - commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder), - responseErrorHandler); - - var embeddingModel = new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties, - QianFanImageProperties imageProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() - : commonProperties.getApiKey(); - - String secretKey = StringUtils.hasText(imageProperties.getSecretKey()) ? imageProperties.getSecretKey() - : commonProperties.getSecretKey(); - - String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - - Assert.hasText(apiKey, "QianFan API key must be set. Use the property: spring.ai.qianfan.api-key"); - Assert.hasText(secretKey, "QianFan secret key must be set. Use the property: spring.ai.qianfan.secret-key"); - Assert.hasText(baseUrl, "QianFan base URL must be set. Use the property: spring.ai.qianfan.base-url"); - - var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey, - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(imageModel::setObservationConvention); - - return imageModel; - } - - private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, - String secretKey, String commonSecretKey, RestClient.Builder restClientBuilder, - ResponseErrorHandler responseErrorHandler) { - - String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl; - Assert.hasText(resolvedBaseUrl, "QianFan base URL must be set"); - - String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey; - Assert.hasText(resolvedApiKey, "QianFan API key must be set"); - - String resolvedSecretKey = StringUtils.hasText(secretKey) ? secretKey : commonSecretKey; - Assert.hasText(resolvedSecretKey, "QianFan Secret key must be set"); - - return new QianFanApi(resolvedBaseUrl, resolvedApiKey, resolvedSecretKey, restClientBuilder, - responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java deleted file mode 100644 index 9ca6b16cb..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import org.springframework.ai.qianfan.QianFanChatOptions; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for QianFan chat model. - * - * @author Geng Rong - */ -@ConfigurationProperties(QianFanChatProperties.CONFIG_PREFIX) -public class QianFanChatProperties extends QianFanParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.qianfan.chat"; - - public static final String DEFAULT_CHAT_MODEL = QianFanApi.ChatModel.ERNIE_Speed_8K.value; - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable QianFan chat client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private QianFanChatOptions options = QianFanChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public QianFanChatOptions getOptions() { - return this.options; - } - - public void setOptions(QianFanChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java deleted file mode 100644 index e26662ef6..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import org.springframework.ai.qianfan.api.QianFanConstants; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(QianFanConnectionProperties.CONFIG_PREFIX) -public class QianFanConnectionProperties extends QianFanParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.qianfan"; - - public static final String DEFAULT_BASE_URL = QianFanConstants.DEFAULT_BASE_URL; - - public QianFanConnectionProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java deleted file mode 100644 index d71bd12b3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.qianfan.QianFanEmbeddingOptions; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for QianFan embedding model. - * - * @author Geng Rong - */ -@ConfigurationProperties(QianFanEmbeddingProperties.CONFIG_PREFIX) -public class QianFanEmbeddingProperties extends QianFanParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.qianfan.embedding"; - - /** - * Enable QianFan embedding client. - */ - private boolean enabled = true; - - private MetadataMode metadataMode = MetadataMode.EMBED; - - @NestedConfigurationProperty - private QianFanEmbeddingOptions options = QianFanEmbeddingOptions.builder() - .model(QianFanApi.DEFAULT_EMBEDDING_MODEL) - .build(); - - public QianFanEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(QianFanEmbeddingOptions options) { - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java deleted file mode 100644 index 3341b760b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import org.springframework.ai.qianfan.QianFanImageOptions; -import org.springframework.ai.qianfan.api.QianFanImageApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * QianFan Image autoconfiguration properties. - * - * @author Geng Rong - */ -@ConfigurationProperties(QianFanImageProperties.CONFIG_PREFIX) -public class QianFanImageProperties extends QianFanParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.qianfan.image"; - - public static final String DEFAULT_IMAGE_MODEL = QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue(); - - /** - * Enable QianFan image model. - */ - private boolean enabled = true; - - /** - * Options for QianFan Image API. - */ - @NestedConfigurationProperty - private QianFanImageOptions options = QianFanImageOptions.builder().model(DEFAULT_IMAGE_MODEL).build(); - - public QianFanImageOptions getOptions() { - return this.options; - } - - public void setOptions(QianFanImageOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java deleted file mode 100644 index 424cbadc1..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -/** - * @author Geng Rong - */ -class QianFanParentProperties { - - private String apiKey; - - private String secretKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getSecretKey() { - return this.secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java deleted file mode 100644 index d1ec0261d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -import org.springframework.ai.stabilityai.api.StabilityAiApi; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(StabilityAiConnectionProperties.CONFIG_PREFIX) -public class StabilityAiConnectionProperties extends StabilityAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.stabilityai"; - - public static final String DEFAULT_BASE_URL = StabilityAiApi.DEFAULT_BASE_URL; - - public StabilityAiConnectionProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java deleted file mode 100644 index 7c143a941..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -import org.springframework.ai.stabilityai.StabilityAiImageModel; -import org.springframework.ai.stabilityai.api.StabilityAiApi; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.client.RestClient; - -/** - * {@link AutoConfiguration Auto-configuration} for StabilityAI Image Model. - * - * @author Mark Pollack - * @author Christian Tzolov - * @since 0.8.0 - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class }) -@ConditionalOnClass(StabilityAiApi.class) -@EnableConfigurationProperties({ StabilityAiConnectionProperties.class, StabilityAiImageProperties.class }) -public class StabilityAiImageAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public StabilityAiApi stabilityAiApi(StabilityAiConnectionProperties commonProperties, - StabilityAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider) { - - String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() - : commonProperties.getApiKey(); - - String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - - Assert.hasText(apiKey, "StabilityAI API key must be set"); - Assert.hasText(baseUrl, "StabilityAI base URL must be set"); - - return new StabilityAiApi(apiKey, imageProperties.getOptions().getModel(), baseUrl, - restClientBuilderProvider.getIfAvailable(RestClient::builder)); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = StabilityAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi, - StabilityAiImageProperties stabilityAiImageProperties) { - return new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java deleted file mode 100644 index dc78af948..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for Stability AI image model. - * - * @author Mark Pollack - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(StabilityAiImageProperties.CONFIG_PREFIX) -public class StabilityAiImageProperties extends StabilityAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.stabilityai.image"; - - /** - * Enable Stability image model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private StabilityAiImageOptions options = StabilityAiImageOptions.builder().build(); // stable-diffusion-v1-6 - - // is - // default - // model - - public StabilityAiImageOptions getOptions() { - return this.options; - } - - public void setOptions(StabilityAiImageOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java deleted file mode 100644 index f75cc2b09..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -/** - * Internal parent properties for the StabilityAI properties. - * - * @author Mark Pollack - * @since 0.8.0 - */ -class StabilityAiParentProperties { - - private String apiKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java deleted file mode 100644 index b653b67d7..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2023-2025 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.tool.autoconfigure; - -import java.util.ArrayList; -import java.util.List; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.tool.ToolCallbackProvider; -import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor; -import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor; -import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver; -import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver; -import org.springframework.ai.tool.resolution.StaticToolCallbackResolver; -import org.springframework.ai.tool.resolution.ToolCallbackResolver; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.support.GenericApplicationContext; - -/** - * Auto-configuration for common tool calling features of {@link ChatModel}. - * - * @author Thomas Vitale - * @author Christian Tzolov - * @since 1.0.0 - */ -@AutoConfiguration -@ConditionalOnClass(ChatModel.class) -public class ToolCallingAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext, - List functionCallbacks, List tcbProviders) { - - List allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks); - tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll); - - var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks); - - var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder() - .applicationContext(applicationContext) - .build(); - - return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver)); - } - - @Bean - @ConditionalOnMissingBean - ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() { - return new DefaultToolExecutionExceptionProcessor(false); - } - - @Bean - @ConditionalOnMissingBean - ToolCallingManager toolCallingManager(ToolCallbackResolver toolCallbackResolver, - ToolExecutionExceptionProcessor toolExecutionExceptionProcessor, - ObjectProvider observationRegistry) { - return ToolCallingManager.builder() - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .toolCallbackResolver(toolCallbackResolver) - .toolExecutionExceptionProcessor(toolExecutionExceptionProcessor) - .build(); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java deleted file mode 100644 index a4535f8ac..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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.model.transformers.autoconfigure; - -import java.io.File; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer; - -import org.springframework.ai.document.Document; -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.transformers.TransformersEmbeddingModel; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for the Transformer Embedding model. - * - * @author Christian Tzolov - */ -@ConfigurationProperties(TransformersEmbeddingModelProperties.CONFIG_PREFIX) -public class TransformersEmbeddingModelProperties { - - public static final String CONFIG_PREFIX = "spring.ai.embedding.transformer"; - - public static final String DEFAULT_CACHE_DIRECTORY = new File(System.getProperty("java.io.tmpdir"), - "spring-ai-onnx-generative") - .getAbsolutePath(); - - @NestedConfigurationProperty - private final Tokenizer tokenizer = new Tokenizer(); - - /** - * Controls caching of remote, large resources to local file system. - */ - @NestedConfigurationProperty - private final Cache cache = new Cache(); - - @NestedConfigurationProperty - private final Onnx onnx = new Onnx(); - - /** - * Enable the Transformer Embedding model. - */ - private boolean enabled = true; - - /** - * Specifies what parts of the {@link Document}'s content and metadata will be used - * for computing the embeddings. Applicable for the - * {@link TransformersEmbeddingModel#embed(Document)} method only. Has no effect on - * the {@link TransformersEmbeddingModel#embed(String)} or - * {@link TransformersEmbeddingModel#embed(List)}. Defaults to - * {@link MetadataMode#NONE}. - */ - private MetadataMode metadataMode = MetadataMode.NONE; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public Cache getCache() { - return this.cache; - } - - public Onnx getOnnx() { - return this.onnx; - } - - public Tokenizer getTokenizer() { - return this.tokenizer; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - /** - * Configurations for the {@link HuggingFaceTokenizer} used to convert sentences into - * tokens. - */ - public static class Tokenizer { - - /** - * URI of a pre-trained HuggingFaceTokenizer created by the ONNX engine (e.g. - * tokenizer.json). - */ - private String uri = TransformersEmbeddingModel.DEFAULT_ONNX_TOKENIZER_URI; - - /** - * HuggingFaceTokenizer options such as 'addSpecialTokens', 'modelMaxLength', - * 'truncation', 'padding', 'maxLength', 'stride' and 'padToMultipleOf'. Leave - * empty to fall back to the defaults. - */ - @NestedConfigurationProperty - private Map options = new HashMap<>(); - - public String getUri() { - return this.uri; - } - - public void setUri(String uri) { - this.uri = uri; - } - - public Map getOptions() { - return this.options; - } - - public void setOptions(Map options) { - this.options = options; - } - - } - - public static class Cache { - - /** - * Enable the Resource caching. - */ - private boolean enabled = true; - - /** - * Resource cache directory. Used to cache remote resources, such as the ONNX - * models, to the local file system. Applicable only for cache.enabled == true. - * Defaults to {java.io.tmpdir}/spring-ai-onnx-generative. - */ - private String directory = DEFAULT_CACHE_DIRECTORY; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getDirectory() { - return this.directory; - } - - public void setDirectory(String directory) { - this.directory = directory; - } - - } - - public static class Onnx { - - /** - * Existing, pre-trained ONNX generative. Commonly exported from - * https://sbert.net/docs/pretrained_models.html. Defaults to - * sentence-transformers/all-MiniLM-L6-v2. - */ - private String modelUri = TransformersEmbeddingModel.DEFAULT_ONNX_MODEL_URI; - - /** - * Defaults to: 'last_hidden_state'. - */ - private String modelOutputName = TransformersEmbeddingModel.DEFAULT_MODEL_OUTPUT_NAME; - - /** - * Run on a GPU or with another provider (optional). - * https://onnxruntime.ai/docs/get-started/with-java.html#run-on-a-gpu-or-with-another-provider-optional - * - * The GPU device ID to execute on. Only applicable if >= 0. Ignored otherwise. - */ - private int gpuDeviceId = -1; - - public String getModelUri() { - return this.modelUri; - } - - public void setModelUri(String modelUri) { - this.modelUri = modelUri; - } - - public int getGpuDeviceId() { - return this.gpuDeviceId; - } - - public void setGpuDeviceId(int gpuDeviceId) { - this.gpuDeviceId = gpuDeviceId; - } - - public String getModelOutputName() { - return this.modelOutputName; - } - - public void setModelOutputName(String modelOutputName) { - this.modelOutputName = modelOutputName; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java deleted file mode 100644 index ab57b118f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.embedding; - -import java.io.IOException; - -import com.google.cloud.vertexai.VertexAI; -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails; -import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel; -import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingModel; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * Auto-configuration for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @author Mark Pollack - * @since 1.0.0 - */ -@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class }) -@ConditionalOnClass({ VertexAI.class, VertexAiTextEmbeddingModel.class }) -@EnableConfigurationProperties({ VertexAiEmbeddingConnectionProperties.class, VertexAiTextEmbeddingProperties.class, - VertexAiMultimodalEmbeddingProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class }) -public class VertexAiEmbeddingAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public VertexAiEmbeddingConnectionDetails connectionDetails( - VertexAiEmbeddingConnectionProperties connectionProperties) { - - Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!"); - Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!"); - - var connectionBuilder = VertexAiEmbeddingConnectionDetails.builder() - .projectId(connectionProperties.getProjectId()) - .location(connectionProperties.getLocation()); - - if (StringUtils.hasText(connectionProperties.getApiEndpoint())) { - connectionBuilder.apiEndpoint(connectionProperties.getApiEndpoint()); - } - - return connectionBuilder.build(); - - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiTextEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public VertexAiTextEmbeddingModel textEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, - VertexAiTextEmbeddingProperties textEmbeddingProperties, RetryTemplate retryTemplate, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var embeddingModel = new VertexAiTextEmbeddingModel(connectionDetails, textEmbeddingProperties.getOptions(), - retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiMultimodalEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public VertexAiMultimodalEmbeddingModel multimodalEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, - VertexAiMultimodalEmbeddingProperties multimodalEmbeddingProperties) throws IOException { - - return new VertexAiMultimodalEmbeddingModel(connectionDetails, multimodalEmbeddingProperties.getOptions()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java deleted file mode 100644 index a55cb3774..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.embedding; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.io.Resource; - -/** - * Configuration properties for Vertex AI Embedding. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@ConfigurationProperties(VertexAiEmbeddingConnectionProperties.CONFIG_PREFIX) -public class VertexAiEmbeddingConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding"; - - /** - * Vertex AI Gemini project ID. - */ - private String projectId; - - /** - * Vertex AI Gemini location. - */ - private String location; - - /** - * URI to Vertex AI Gemini credentials (optional) - */ - private Resource credentialsUri; - - /** - * Vertex AI Gemini API endpoint. - */ - private String apiEndpoint; - - public String getProjectId() { - return this.projectId; - } - - public void setProjectId(String projectId) { - this.projectId = projectId; - } - - public String getLocation() { - return this.location; - } - - public void setLocation(String location) { - this.location = location; - } - - public Resource getCredentialsUri() { - return this.credentialsUri; - } - - public void setCredentialsUri(Resource credentialsUri) { - this.credentialsUri = credentialsUri; - } - - public String getApiEndpoint() { - return this.apiEndpoint; - } - - public void setApiEndpoint(String apiEndpoint) { - this.apiEndpoint = apiEndpoint; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java deleted file mode 100644 index e1bb8e2c3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.embedding; - -import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@ConfigurationProperties(VertexAiMultimodalEmbeddingProperties.CONFIG_PREFIX) -public class VertexAiMultimodalEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding.multimodal"; - - private boolean enabled = true; - - /** - * Vertex AI Text Embedding API options. - */ - private VertexAiMultimodalEmbeddingOptions options = VertexAiMultimodalEmbeddingOptions.builder() - .model(VertexAiMultimodalEmbeddingOptions.DEFAULT_MODEL_NAME) - .build(); - - public VertexAiMultimodalEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(VertexAiMultimodalEmbeddingOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java deleted file mode 100644 index 737a4a6bc..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.embedding; - -import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @since 1.0.0 - */ -@ConfigurationProperties(VertexAiTextEmbeddingProperties.CONFIG_PREFIX) -public class VertexAiTextEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding.text"; - - private boolean enabled = true; - - /** - * Vertex AI Text Embedding API options. - */ - private VertexAiTextEmbeddingOptions options = VertexAiTextEmbeddingOptions.builder() - .taskType(VertexAiTextEmbeddingOptions.TaskType.RETRIEVAL_DOCUMENT) - .model(VertexAiTextEmbeddingOptions.DEFAULT_MODEL_NAME) - .build(); - - public VertexAiTextEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(VertexAiTextEmbeddingOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java deleted file mode 100644 index 4a059efa2..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini; - -import java.io.IOException; - -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.vertexai.VertexAI; -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * Auto-configuration for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @author Soby Chacko - * @author Mark Pollack - * @since 1.0.0 - */ -@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@ConditionalOnClass({ VertexAI.class, VertexAiGeminiChatModel.class }) -@EnableConfigurationProperties({ VertexAiGeminiChatProperties.class, VertexAiGeminiConnectionProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -public class VertexAiGeminiAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public VertexAI vertexAi(VertexAiGeminiConnectionProperties connectionProperties) throws IOException { - - Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!"); - Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!"); - Assert.notNull(connectionProperties.getTransport(), "Vertex AI transport must be set!"); - - var vertexAIBuilder = new VertexAI.Builder().setProjectId(connectionProperties.getProjectId()) - .setLocation(connectionProperties.getLocation()) - .setTransport(com.google.cloud.vertexai.Transport.valueOf(connectionProperties.getTransport().name())); - - if (StringUtils.hasText(connectionProperties.getApiEndpoint())) { - vertexAIBuilder.setApiEndpoint(connectionProperties.getApiEndpoint()); - } - if (!CollectionUtils.isEmpty(connectionProperties.getScopes())) { - vertexAIBuilder.setScopes(connectionProperties.getScopes()); - } - - if (connectionProperties.getCredentialsUri() != null) { - GoogleCredentials credentials = GoogleCredentials - .fromStream(connectionProperties.getCredentialsUri().getInputStream()); - - vertexAIBuilder.setCredentials(credentials); - } - return vertexAIBuilder.build(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiGeminiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public VertexAiGeminiChatModel vertexAiGeminiChat(VertexAI vertexAi, VertexAiGeminiChatProperties chatProperties, - ToolCallingManager toolCallingManager, ApplicationContext context, RetryTemplate retryTemplate, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - VertexAiGeminiChatModel chatModel = VertexAiGeminiChatModel.builder() - .vertexAI(vertexAi) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .retryTemplate(retryTemplate) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - /** - * Because of the OPEN_API_SCHEMA type, the FunctionCallbackResolver instance must - * different from the other JSON schema types. - */ - // private FunctionCallbackResolver springAiFunctionManager(ApplicationContext - // context) { - // DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - // manager.setSchemaType(SchemaType.OPEN_API_SCHEMA); - // manager.setApplicationContext(context); - // return manager; - // } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java deleted file mode 100644 index 1770e5ed1..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini; - -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * Configuration properties for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(VertexAiGeminiChatProperties.CONFIG_PREFIX) -public class VertexAiGeminiChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.gemini.chat"; - - public static final String DEFAULT_MODEL = VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue(); - - /** - * Vertex AI Gemini API generative options. - */ - private VertexAiGeminiChatOptions options = VertexAiGeminiChatOptions.builder() - .temperature(0.7) - .candidateCount(1) - .model(DEFAULT_MODEL) - .build(); - - public VertexAiGeminiChatOptions getOptions() { - return this.options; - } - - public void setOptions(VertexAiGeminiChatOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java deleted file mode 100644 index 7adf07052..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini; - -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.io.Resource; - -/** - * Configuration properties for Vertex AI Gemini Chat. - * - * @author Christian Tzolov - * @since 0.8.0 - */ -@ConfigurationProperties(VertexAiGeminiConnectionProperties.CONFIG_PREFIX) -public class VertexAiGeminiConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.gemini"; - - /** - * Vertex AI Gemini project ID. - */ - private String projectId; - - /** - * Vertex AI Gemini location. - */ - private String location; - - /** - * URI to Vertex AI Gemini credentials (optional) - */ - private Resource credentialsUri; - - /** - * Vertex AI Gemini API endpoint. - */ - private String apiEndpoint; - - /** - * - */ - private List scopes = List.of(); - - private Transport transport = Transport.GRPC; - - public String getProjectId() { - return this.projectId; - } - - public void setProjectId(String projectId) { - this.projectId = projectId; - } - - public String getLocation() { - return this.location; - } - - public void setLocation(String location) { - this.location = location; - } - - public Resource getCredentialsUri() { - return this.credentialsUri; - } - - public void setCredentialsUri(Resource credentialsUri) { - this.credentialsUri = credentialsUri; - } - - public String getApiEndpoint() { - return this.apiEndpoint; - } - - public void setApiEndpoint(String apiEndpoint) { - this.apiEndpoint = apiEndpoint; - } - - public List getScopes() { - return this.scopes; - } - - public void setScopes(List scopes) { - this.scopes = scopes; - } - - public Transport getTransport() { - return this.transport; - } - - public void setTransport(Transport transport) { - this.transport = transport; - } - - public enum Transport { - - /** When used, the clients will send REST requests to the backing service. */ - REST, - /** - * When used, the clients will send gRPC to the backing service. This is usually - * more efficient and is the default transport. - */ - GRPC - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java deleted file mode 100644 index cff01d7fc..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.model.watsonxai.autoconfigure; - -import java.util.List; - -import org.springframework.ai.watsonx.WatsonxAiChatOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Chat properties for Watsonx.AI Chat. - * - * @author Christian Tzolov - * @author Alexandros Pappas - * @since 1.0.0 - */ -@ConfigurationProperties(WatsonxAiChatProperties.CONFIG_PREFIX) -public class WatsonxAiChatProperties { - - public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai.chat"; - - /** - * Enable Watsonx.AI chat model. - */ - private boolean enabled = true; - - /** - * Watsonx AI generative options. - */ - @NestedConfigurationProperty - private WatsonxAiChatOptions options = WatsonxAiChatOptions.builder() - .model("google/flan-ul2") - .temperature(0.7) - .topP(1.0) - .topK(50) - .decodingMethod("greedy") - .maxNewTokens(20) - .minNewTokens(0) - .repetitionPenalty(1.0) - .stopSequences(List.of()) - .build(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public WatsonxAiChatOptions getOptions() { - return this.options; - } - - public void setOptions(WatsonxAiChatOptions options) { - this.options = options; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java deleted file mode 100644 index a49ab048c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.model.watsonxai.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -/** - * WatsonX.ai connection autoconfiguration properties. - * - * @author Pablo Sanchidrian Herrera - * @author John Jario Moreno Rojas - * @since 1.0.0 - */ -@ConfigurationProperties(WatsonxAiConnectionProperties.CONFIG_PREFIX) -public class WatsonxAiConnectionProperties { - - public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai"; - - private String baseUrl = "https://us-south.ml.cloud.ibm.com/"; - - private String streamEndpoint = "ml/v1/text/generation_stream?version=2023-05-29"; - - private String textEndpoint = "ml/v1/text/generation?version=2023-05-29"; - - private String embeddingEndpoint = "ml/v1/text/embeddings?version=2023-05-29"; - - private String projectId; - - private String IAMToken; - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getStreamEndpoint() { - return this.streamEndpoint; - } - - public void setStreamEndpoint(String streamEndpoint) { - this.streamEndpoint = streamEndpoint; - } - - public String getTextEndpoint() { - return this.textEndpoint; - } - - public void setTextEndpoint(String textEndpoint) { - this.textEndpoint = textEndpoint; - } - - public String getEmbeddingEndpoint() { - return this.embeddingEndpoint; - } - - public void setEmbeddingEndpoint(String embeddingEndpoint) { - this.embeddingEndpoint = embeddingEndpoint; - } - - public String getProjectId() { - return this.projectId; - } - - public void setProjectId(String projectId) { - this.projectId = projectId; - } - - public String getIAMToken() { - return this.IAMToken; - } - - public void setIAMToken(String IAMToken) { - this.IAMToken = IAMToken; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java deleted file mode 100644 index 1691951f0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.model.watsonxai.autoconfigure; - -import org.springframework.ai.watsonx.WatsonxAiEmbeddingOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Watsonx.ai Embedding autoconfiguration properties. - * - * @author Pablo Sanchidrian Herrera - * @since 1.0.0 - */ -@ConfigurationProperties(WatsonxAiEmbeddingProperties.CONFIG_PREFIX) -public class WatsonxAiEmbeddingProperties { - - public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai.embedding"; - - /** - * Enable Watsonx.ai embedding model. - */ - private boolean enabled = true; - - /** - * Client lever Watsonx.ai embedding options. Use this property to configure the - * model. The null values are ignored defaulting to the defaults. - */ - @NestedConfigurationProperty - private WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create() - .withModel(WatsonxAiEmbeddingOptions.DEFAULT_MODEL); - - public String getModel() { - return this.options.getModel(); - } - - public void setModel(String model) { - this.options.setModel(model); - } - - public WatsonxAiEmbeddingOptions getOptions() { - return this.options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java deleted file mode 100644 index db27df995..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import java.util.List; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; - -/** - * {@link AutoConfiguration Auto-configuration} for ZhiPuAI. - * - * @author Geng Rong - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) -@ConditionalOnClass(ZhiPuAiApi.class) -@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class, - ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class }) -public class ZhiPuAiAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, - ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); - - var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver, - toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties commonProperties, - ZhiPuAiEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var zhiPuAiApi = zhiPuAiApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); - - var embeddingModel = new ZhiPuAiEmbeddingModel(zhiPuAiApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey, - RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) { - - String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl; - Assert.hasText(resolvedBaseUrl, "ZhiPuAI base URL must be set"); - - String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey; - Assert.hasText(resolvedApiKey, "ZhiPuAI API key must be set"); - - return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, - ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() - : commonProperties.getApiKey(); - - String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - - Assert.hasText(apiKey, "ZhiPuAI API key must be set"); - Assert.hasText(baseUrl, "ZhiPuAI base URL must be set"); - - var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler); - - return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java deleted file mode 100644 index e5f44f475..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for ZhiPuAI chat model. - * - * @author Geng Rong - */ -@ConfigurationProperties(ZhiPuAiChatProperties.CONFIG_PREFIX) -public class ZhiPuAiChatProperties extends ZhiPuAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.zhipuai.chat"; - - public static final String DEFAULT_CHAT_MODEL = ZhiPuAiApi.ChatModel.GLM_4_Air.value; - - private static final Double DEFAULT_TEMPERATURE = 0.7; - - /** - * Enable ZhiPuAI chat model. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder() - .model(DEFAULT_CHAT_MODEL) - .temperature(DEFAULT_TEMPERATURE) - .build(); - - public ZhiPuAiChatOptions getOptions() { - return this.options; - } - - public void setOptions(ZhiPuAiChatOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java deleted file mode 100644 index 3e3aee144..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(ZhiPuAiConnectionProperties.CONFIG_PREFIX) -public class ZhiPuAiConnectionProperties extends ZhiPuAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.zhipuai"; - - public static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas"; - - public ZhiPuAiConnectionProperties() { - super.setBaseUrl(DEFAULT_BASE_URL); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java deleted file mode 100644 index 300b0a23d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for ZhiPuAI embedding model. - * - * @author Geng Rong - */ -@ConfigurationProperties(ZhiPuAiEmbeddingProperties.CONFIG_PREFIX) -public class ZhiPuAiEmbeddingProperties extends ZhiPuAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.zhipuai.embedding"; - - public static final String DEFAULT_EMBEDDING_MODEL = ZhiPuAiApi.EmbeddingModel.Embedding_2.value; - - /** - * Enable ZhiPuAI embedding model. - */ - private boolean enabled = true; - - private MetadataMode metadataMode = MetadataMode.EMBED; - - @NestedConfigurationProperty - private ZhiPuAiEmbeddingOptions options = ZhiPuAiEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build(); - - public ZhiPuAiEmbeddingOptions getOptions() { - return this.options; - } - - public void setOptions(ZhiPuAiEmbeddingOptions options) { - this.options = options; - } - - public MetadataMode getMetadataMode() { - return this.metadataMode; - } - - public void setMetadataMode(MetadataMode metadataMode) { - this.metadataMode = metadataMode; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java deleted file mode 100644 index bfcceee5e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Configuration properties for ZhiPuAI chat model. - * - * @author Geng Rong - */ -@ConfigurationProperties(ZhiPuAiImageProperties.CONFIG_PREFIX) -public class ZhiPuAiImageProperties extends ZhiPuAiParentProperties { - - public static final String CONFIG_PREFIX = "spring.ai.zhipuai.image"; - - /** - * Enable ZhiPuAI image model. - */ - private boolean enabled = true; - - /** - * Options for ZhiPuAI Image API. - */ - @NestedConfigurationProperty - private ZhiPuAiImageOptions options = ZhiPuAiImageOptions.builder().build(); - - public ZhiPuAiImageOptions getOptions() { - return this.options; - } - - public void setOptions(ZhiPuAiImageOptions options) { - this.options = options; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java deleted file mode 100644 index 5a8e3f193..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -/** - * @author Geng Rong - */ -class ZhiPuAiParentProperties { - - private String apiKey; - - private String baseUrl; - - public String getApiKey() { - return this.apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - - public String getBaseUrl() { - return this.baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java deleted file mode 100644 index f44ec9613..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.retry.autoconfigure; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.retry.NonTransientAiException; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.ai.retry.TransientAiException; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.NonNull; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; -import org.springframework.retry.RetryListener; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StreamUtils; -import org.springframework.web.client.ResponseErrorHandler; - -/** - * {@link AutoConfiguration Auto-configuration} for AI Retry. - * - * @author Christian Tzolov - */ -@AutoConfiguration -@ConditionalOnClass(RetryUtils.class) -@EnableConfigurationProperties({ SpringAiRetryProperties.class }) -public class SpringAiRetryAutoConfiguration { - - private static final Logger logger = LoggerFactory.getLogger(SpringAiRetryAutoConfiguration.class); - - @Bean - @ConditionalOnMissingBean - public RetryTemplate retryTemplate(SpringAiRetryProperties properties) { - return RetryTemplate.builder() - .maxAttempts(properties.getMaxAttempts()) - .retryOn(TransientAiException.class) - .exponentialBackoff(properties.getBackoff().getInitialInterval(), properties.getBackoff().getMultiplier(), - properties.getBackoff().getMaxInterval()) - .withListener(new RetryListener() { - - @Override - public void onError(RetryContext context, - RetryCallback callback, Throwable throwable) { - logger.warn("Retry error. Retry count:" + context.getRetryCount(), throwable); - } - }) - .build(); - } - - @Bean - @ConditionalOnMissingBean - public ResponseErrorHandler responseErrorHandler(SpringAiRetryProperties properties) { - - return new ResponseErrorHandler() { - - @Override - public boolean hasError(@NonNull ClientHttpResponse response) throws IOException { - return response.getStatusCode().isError(); - } - - @Override - public void handleError(@NonNull ClientHttpResponse response) throws IOException { - if (response.getStatusCode().isError()) { - String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); - String message = String.format("%s - %s", response.getStatusCode().value(), error); - - // Explicitly configured transient codes - if (properties.getOnHttpCodes().contains(response.getStatusCode().value())) { - throw new TransientAiException(message); - } - - // onClientErrors - If true, do not throw a NonTransientAiException, - // and do not attempt retry for 4xx client error codes, false by - // default. - if (!properties.isOnClientErrors() && response.getStatusCode().is4xxClientError()) { - throw new NonTransientAiException(message); - } - - // Explicitly configured non-transient codes - if (!CollectionUtils.isEmpty(properties.getExcludeOnHttpCodes()) - && properties.getExcludeOnHttpCodes().contains(response.getStatusCode().value())) { - throw new NonTransientAiException(message); - } - throw new TransientAiException(message); - } - } - }; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java deleted file mode 100644 index 385103a94..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.retry.autoconfigure; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -/** - * Properties for AI Retry. - * - * @author Christian Tzolov - */ -@ConfigurationProperties(SpringAiRetryProperties.CONFIG_PREFIX) -public class SpringAiRetryProperties { - - public static final String CONFIG_PREFIX = "spring.ai.retry"; - - /** - * Maximum number of retry attempts. - */ - private int maxAttempts = 10; - - /** - * Exponential Backoff properties. - */ - @NestedConfigurationProperty - private Backoff backoff = new Backoff(); - - /** - * If false, throw a NonTransientAiException, and do not attempt retry for 4xx client - * error codes. False by default. If true, throw a TransientAiException, and attempt - * retry for 4xx client. - */ - private boolean onClientErrors = false; - - /** - * List of HTTP status codes that should not trigger a retry (e.g. throw - * NonTransientAiException). - */ - private List excludeOnHttpCodes = new ArrayList<>(); - - /** - * List of HTTP status codes that should trigger a retry. - */ - private List onHttpCodes = new ArrayList<>(); - - public int getMaxAttempts() { - return this.maxAttempts; - } - - public void setMaxAttempts(int maxAttempts) { - this.maxAttempts = maxAttempts; - } - - public Backoff getBackoff() { - return this.backoff; - } - - public List getExcludeOnHttpCodes() { - return this.excludeOnHttpCodes; - } - - public void setExcludeOnHttpCodes(List onHttpCodes) { - this.excludeOnHttpCodes = onHttpCodes; - } - - public boolean isOnClientErrors() { - return this.onClientErrors; - } - - public void setOnClientErrors(boolean onClientErrors) { - this.onClientErrors = onClientErrors; - } - - public List getOnHttpCodes() { - return this.onHttpCodes; - } - - public void setOnHttpCodes(List onHttpCodes) { - this.onHttpCodes = onHttpCodes; - } - - /** - * Exponential Backoff properties. - */ - public static class Backoff { - - /** - * Initial sleep duration. - */ - private Duration initialInterval = Duration.ofMillis(2000); - - /** - * Backoff interval multiplier. - */ - private int multiplier = 5; - - /** - * Maximum backoff duration. - */ - private Duration maxInterval = Duration.ofMillis(3 * 60000); - - public Duration getInitialInterval() { - return this.initialInterval; - } - - public void setInitialInterval(Duration initialInterval) { - this.initialInterval = initialInterval; - } - - public int getMultiplier() { - return this.multiplier; - } - - public void setMultiplier(int multiplier) { - this.multiplier = multiplier; - } - - public Duration getMaxInterval() { - return this.maxInterval; - } - - public void setMaxInterval(Duration maxInterval) { - this.maxInterval = maxInterval; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index 3a94062ad..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "groups": [ - { - "name": "spring.ai.azure.openai.chat.options.enhancements", - "type": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration", - "sourceType": "org.springframework.ai.azure.openai.AzureOpenAiChatOptions", - "sourceMethod": "getEnhancements()" - }, - { - "name": "spring.ai.mistralai.chat.options.tool-choice", - "type": "org.springframework.ai.mistralai.api.MistralAiApi$ChatCompletionRequest$ToolChoice", - "sourceType": "org.springframework.ai.mistralai.MistralAiChatOptions" - }, - { - "name": "spring.ai.openai.chat.output-audio", - "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters", - "sourceType": "org.springframework.ai.openai.OpenAiChatOptions" - } - ], - "properties": [ - { - "name": "spring.ai.azure.openai.chat.options.enhancements.grounding", - "type": "com.azure.ai.openai.models.AzureChatGroundingEnhancementConfiguration", - "sourceType": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration" - }, - { - "name": "spring.ai.azure.openai.chat.options.enhancements.ocr", - "type": "com.azure.ai.openai.models.AzureChatOCREnhancementConfiguration", - "sourceType": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration" - }, - { - "name": "spring.ai.openai.chat.output-audio.voice", - "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters$Voice", - "sourceType": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters" - }, - { - "name": "spring.ai.openai.chat.output-audio.format", - "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters$AudioResponseFormat", - "sourceType": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters" - } - ], - "hints": [] -} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports deleted file mode 100644 index 7cb623cc6..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ /dev/null @@ -1,44 +0,0 @@ -# -# 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. -# -org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration -org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiAutoConfiguration -org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration -org.springframework.ai.model.transformers.autoconfigure.TransformersEmbeddingModelAutoConfiguration -org.springframework.ai.model.huggingface.autoconfigure.HuggingfaceChatAutoConfiguration -gemini.org.springframework.ai.model.vertexai.autoconfigure.VertexAiGeminiAutoConfiguration -cohere.org.springframework.ai.model.bedrock.autoconfigure.BedrockCohereEmbeddingAutoConfiguration -titan.org.springframework.ai.model.bedrock.autoconfigure.BedrockTitanEmbeddingAutoConfiguration -converse.org.springframework.ai.model.bedrock.autoconfigure.BedrockConverseProxyChatAutoConfiguration -org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationAutoConfiguration -org.springframework.ai.model.embedding.observation.autoconfigure.EmbeddingObservationAutoConfiguration -org.springframework.ai.model.image.observation.autoconfigure.ImageObservationAutoConfiguration -org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration -org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration -org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration -org.springframework.ai.model.postgresml.autoconfigure.PostgresMlAutoConfiguration -org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration -org.springframework.ai.model.watsonxai.autoconfigure.WatsonxAiAutoConfiguration -org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration -org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration -org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration -org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration -org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration -org.springframework.ai.model.qianfan.autoconfigure.QianFanAutoConfiguration -org.springframework.ai.model.minimax.autoconfigure.MiniMaxAutoConfiguration -embedding.org.springframework.ai.model.vertexai.autoconfigure.VertexAiEmbeddingAutoConfiguration -cassandra.org.springframework.ai.model.chat.memory.autoconfigure.CassandraChatMemoryAutoConfiguration -org.springframework.ai.autoconfigure.vectorstore.observation.VectorStoreObservationAutoConfiguration -org.springframework.ai.autoconfigure.vectorstore.couchbase.CouchbaseSearchVectorStoreAutoConfiguration diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java deleted file mode 100644 index d16010981..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.autoconfigure.chat.observation; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ChatObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ChatObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ChatObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationFilter.class)); - } - - @Test - void promptHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void completionFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationFilter.class)); - } - - @Test - void completionHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java deleted file mode 100644 index 42037c9e0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.autoconfigure.embedding.observation; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link EmbeddingObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class EmbeddingObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddingObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(EmbeddingModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(EmbeddingModelMeterObservationHandler.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java deleted file mode 100644 index deb0a22ad..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.autoconfigure.image.observation; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ImageObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ImageObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ImageObservationAutoConfiguration.class)); - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationFilter.class)); - } - - @Test - void promptFilterEnabled() { - this.contextRunner.withPropertyValues("spring.ai.image.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationFilter.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java deleted file mode 100644 index a46ff4022..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.anthropic.AnthropicChatOptions; -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*") -public class AnthropicAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(AnthropicAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); - - @Test - void call() { - this.contextRunner.run(context -> { - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void callWith8KResponseContext() { - this.contextRunner - .withPropertyValues("spring.ai.anthropic.beta-version=" + AnthropicApi.BETA_MAX_TOKENS, - "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getValue()) - .run(context -> { - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - var optoins = AnthropicChatOptions.builder().maxTokens(8192).build(); - var response = chatModel.call(new Prompt("Tell me a joke", optoins)); - assertThat(response.getResult().getOutput().getText()).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void stream() { - this.contextRunner.run(context -> { - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - - String response = responseFlux.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java deleted file mode 100644 index ceb714227..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link AnthropicChatProperties}, {@link AnthropicConnectionProperties}. - */ -public class AnthropicPropertiesTests { - - @Test - public void connectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.anthropic.base-url=TEST_BASE_URL", - "spring.ai.anthropic.api-key=abc123", - "spring.ai.anthropic.version=6666", - "spring.ai.anthropic.beta-version=7777", - "spring.ai.anthropic.chat.options.model=MODEL_XYZ", - "spring.ai.anthropic.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(AnthropicChatProperties.class); - var connectionProperties = context.getBean(AnthropicConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getVersion()).isEqualTo("6666"); - assertThat(connectionProperties.getBetaVersion()).isEqualTo("7777"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - // enabled is true by default - assertThat(chatProperties.isEnabled()).isTrue(); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.anthropic.api-key=API_KEY", - "spring.ai.anthropic.base-url=TEST_BASE_URL", - - "spring.ai.anthropic.chat.options.model=MODEL_XYZ", - "spring.ai.anthropic.chat.options.max-tokens=123", - "spring.ai.anthropic.chat.options.metadata.user-id=MyUserId", - "spring.ai.anthropic.chat.options.stop_sequences=boza,koza", - - "spring.ai.anthropic.chat.options.temperature=0.55", - "spring.ai.anthropic.chat.options.top-p=0.56", - "spring.ai.anthropic.chat.options.top-k=100" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(AnthropicChatProperties.class); - var connectionProperties = context.getBean(AnthropicConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getStopSequences()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100); - - assertThat(chatProperties.getOptions().getMetadata().userId()).isEqualTo("MyUserId"); - }); - } - - @Test - public void chatCompletionDisabled() { - - // It is enabled by default - new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); - }); - - // Explicitly enable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); - }); - - // Explicitly disable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isEmpty(); - assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java deleted file mode 100644 index fb4e53e86..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.anthropic.AnthropicChatOptions; -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration; -import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Request; -import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Response; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*") -class FunctionCallWithFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - - this.contextRunner - .withPropertyValues( - "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue()) - .run(context -> { - - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - - var userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - AnthropicChatOptions.builder().toolNames("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel.call(new Prompt(List.of(userMessage), - AnthropicChatOptions.builder().toolNames("weatherFunction3").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - - this.contextRunner - .withPropertyValues( - "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue()) - .run(context -> { - - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - - var userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location. Return temperature in 36°F or 36°C format.") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunction3() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java deleted file mode 100644 index 27b8d2dab..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.model.anthropic.autoconfigure.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.anthropic.AnthropicChatModel; -import org.springframework.ai.anthropic.AnthropicChatOptions; -import org.springframework.ai.anthropic.api.AnthropicApi; -import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*") -public class FunctionCallWithPromptFunctionIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) - .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues( - "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue()) - .run(context -> { - - AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius."); - - var promptOptions = AnthropicChatOptions.builder() - .toolCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location. Return temperature in 36°F or 36°C format.") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java deleted file mode 100644 index 4cd87697c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import java.lang.reflect.Field; -import java.net.URI; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - -import com.azure.ai.openai.OpenAIClient; -import com.azure.ai.openai.OpenAIClientBuilder; -import com.azure.ai.openai.implementation.OpenAIClientImpl; -import com.azure.core.http.HttpHeader; -import com.azure.core.http.HttpHeaderName; -import com.azure.core.http.HttpMethod; -import com.azure.core.http.HttpPipeline; -import com.azure.core.http.HttpRequest; -import com.azure.core.http.HttpResponse; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAIClientBuilderCustomizer; - -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Piotr Olaszewski - * @author Soby Chacko - * @author Manuel Andreo Garcia - * @since 0.8.0 - */ -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+") -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+") -class AzureOpenAiAutoConfigurationIT { - - private static String CHAT_MODEL_NAME = "gpt-4o"; - - private static String EMBEDDING_MODEL_NAME = "text-embedding-ada-002"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), - "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"), - - "spring.ai.azure.openai.chat.options.deployment-name=" + CHAT_MODEL_NAME, - "spring.ai.azure.openai.chat.options.temperature=0.8", - "spring.ai.azure.openai.chat.options.maxTokens=123", - - "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME, - "spring.ai.azure.openai.audio.transcription.options.deployment-name=" + System.getenv("AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME") - // @formatter:on - ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); - - private final Message systemMessage = new SystemPromptTemplate(""" - You are a helpful AI assistant. Your name is {name}. - You are an AI assistant that helps people find information. - Your name is {name} - You should reply to the user's request with your name and also in the style of a {voice}. - """).createMessage(Map.of("name", "Bob", "voice", "pirate")); - - private final UserMessage userMessage = new UserMessage( - "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did."); - - @Test - void chatCompletion() { - this.contextRunner.run(context -> { - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage))); - assertThat(response.getResult().getOutput().getText()).contains("Blackbeard"); - }); - } - - @Test - void httpRequestContainsUserAgentAndCustomHeaders() { - this.contextRunner - .withPropertyValues("spring.ai.azure.openai.custom-headers.foo=bar", - "spring.ai.azure.openai.custom-headers.fizz=buzz") - .run(context -> { - OpenAIClientBuilder openAIClientBuilder = context.getBean(OpenAIClientBuilder.class); - OpenAIClient openAIClient = openAIClientBuilder.buildClient(); - Field serviceClientField = ReflectionUtils.findField(OpenAIClient.class, "serviceClient"); - assertThat(serviceClientField).isNotNull(); - ReflectionUtils.makeAccessible(serviceClientField); - OpenAIClientImpl oaci = (OpenAIClientImpl) ReflectionUtils.getField(serviceClientField, openAIClient); - assertThat(oaci).isNotNull(); - HttpPipeline httpPipeline = oaci.getHttpPipeline(); - HttpResponse httpResponse = httpPipeline - .send(new HttpRequest(HttpMethod.POST, new URI(System.getenv("AZURE_OPENAI_ENDPOINT")).toURL())) - .block(); - assertThat(httpResponse).isNotNull(); - HttpHeader httpHeader = httpResponse.getRequest().getHeaders().get(HttpHeaderName.USER_AGENT); - assertThat(httpHeader.getValue().startsWith("spring-ai azsdk-java-azure-ai-openai/")).isTrue(); - HttpHeader customHeader1 = httpResponse.getRequest().getHeaders().get("foo"); - assertThat(customHeader1.getValue()).isEqualTo("bar"); - HttpHeader customHeader2 = httpResponse.getRequest().getHeaders().get("fizz"); - assertThat(customHeader2.getValue()).isEqualTo("buzz"); - }); - } - - @Test - void chatCompletionStreaming() { - this.contextRunner.run(context -> { - - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - Flux response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage))); - - List responses = response.collectList().block(); - assertThat(responses.size()).isGreaterThan(10); - - String stitchedResponseContent = responses.stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(stitchedResponseContent).contains("Blackbeard"); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); - } - - @Test - @EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME", matches = ".+") - void transcribe() { - this.contextRunner.run(context -> { - AzureOpenAiAudioTranscriptionModel transcriptionModel = context - .getBean(AzureOpenAiAudioTranscriptionModel.class); - Resource audioFile = new ClassPathResource("/speech/jfk.flac"); - String response = transcriptionModel.call(audioFile); - assertThat(response).isEqualTo( - "And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country."); - }); - } - - @Test - void chatActivation() { - - // Disable the chat auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty()); - - // The chat auto-configuration is enabled by default. - this.contextRunner.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty()); - - // Explicitly enable the chat auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=true") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty()); - } - - @Test - void embeddingActivation() { - - // Disable the embedding auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty()); - - // The embedding auto-configuration is enabled by default. - this.contextRunner - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty()); - - // Explicitly enable the embedding auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=true") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty()); - } - - @Test - void audioTranscriptionActivation() { - - // Disable the transcription auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=false") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty()); - - // The transcription auto-configuration is enabled by default. - this.contextRunner - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty()); - - // Explicitly enable the transcription auto-configuration. - this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=true") - .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty()); - } - - @Test - void openAIClientBuilderCustomizer() { - AtomicBoolean firstCustomizationApplied = new AtomicBoolean(false); - AtomicBoolean secondCustomizationApplied = new AtomicBoolean(false); - this.contextRunner - .withBean("first", AzureOpenAIClientBuilderCustomizer.class, - () -> clientBuilder -> firstCustomizationApplied.set(true)) - .withBean("second", AzureOpenAIClientBuilderCustomizer.class, - () -> clientBuilder -> secondCustomizationApplied.set(true)) - .run(context -> { - context.getBean(OpenAIClientBuilder.class); - assertThat(firstCustomizationApplied.get()).isTrue(); - assertThat(secondCustomizationApplied.get()).isTrue(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java deleted file mode 100644 index df965f34e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiConnectionProperties; -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 0.8.0 - */ -public class AzureOpenAiAutoConfigurationPropertyTests { - - @Test - public void embeddingPropertiesTest() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.azure.openai.api-key=TEST_API_KEY", - "spring.ai.azure.openai.endpoint=TEST_ENDPOINT", - "spring.ai.azure.openai.embedding.options.deployment-name=MODEL_XYZ") - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(AzureOpenAiEmbeddingProperties.class); - var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("TEST_API_KEY"); - assertThat(connectionProperties.getEndpoint()).isEqualTo("TEST_ENDPOINT"); - - assertThat(chatProperties.getOptions().getDeploymentName()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void chatPropertiesTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.api-key=API_KEY", - "spring.ai.azure.openai.endpoint=ENDPOINT", - - "spring.ai.azure.openai.chat.options.deployment-name=MODEL_XYZ", - "spring.ai.azure.openai.chat.options.frequencyPenalty=-1.5", - "spring.ai.azure.openai.chat.options.logitBias.myTokenId=-5", - "spring.ai.azure.openai.chat.options.maxTokens=123", - "spring.ai.azure.openai.chat.options.n=10", - "spring.ai.azure.openai.chat.options.presencePenalty=0", - "spring.ai.azure.openai.chat.options.stop=boza,koza", - "spring.ai.azure.openai.chat.options.temperature=0.55", - "spring.ai.azure.openai.chat.options.topP=0.56", - "spring.ai.azure.openai.chat.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(AzureOpenAiChatProperties.class); - var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(AzureOpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getEndpoint()).isEqualTo("ENDPOINT"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getDeploymentName()).isEqualTo("text-embedding-ada-002"); - - assertThat(chatProperties.getOptions().getDeploymentName()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); - assertThat(chatProperties.getOptions().getLogitBias().get("myTokenId")).isEqualTo(-5); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getN()).isEqualTo(10); - assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - - assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java deleted file mode 100644 index 99c4ed05c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure; - -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 reactor.core.publisher.Flux; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 1.0.0 - */ -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class AzureOpenAiDirectOpenAiAutoConfigurationIT { - - private static String CHAT_MODEL_NAME = "gpt-4o"; - - private static String EMBEDDING_MODEL_NAME = "text-embedding-ada-002"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"), - - "spring.ai.azure.openai.chat.options.deployment-name=" + CHAT_MODEL_NAME, - "spring.ai.azure.openai.chat.options.temperature=0.8", - "spring.ai.azure.openai.chat.options.maxTokens=123", - "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME - // @formatter:on - ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); - - private final Message systemMessage = new SystemPromptTemplate(""" - You are a helpful AI assistant. Your name is {name}. - You are an AI assistant that helps people find information. - Your name is {name} - You should reply to the user's request with your name and also in the style of a {voice}. - """).createMessage(Map.of("name", "Bob", "voice", "pirate")); - - private final UserMessage userMessage = new UserMessage( - "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did."); - - @Test - public void chatCompletion() { - this.contextRunner.run(context -> { - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage))); - assertThat(response.getResult().getOutput().getText()).contains("Blackbeard"); - }); - } - - @Test - public void chatCompletionStreaming() { - this.contextRunner.run(context -> { - - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - Flux response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage))); - - List responses = response.collectList().block(); - assertThat(responses.size()).isGreaterThan(10); - - String stitchedResponseContent = responses.stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(stitchedResponseContent).contains("Blackbeard"); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java deleted file mode 100644 index 96655413b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure.tool; - -import org.springframework.util.StringUtils; - -public final class DeploymentNameUtil { - - private DeploymentNameUtil() { - - } - - public static String getDeploymentName() { - String deploymentName = System.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"); - if (StringUtils.hasText(deploymentName)) { - return deploymentName; - } - else { - return "gpt-4o"; - } - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java deleted file mode 100644 index 5f560d174..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2023-2025 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.azure.openai.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+") -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+") -class FunctionCallWithFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), - "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) - // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues( - "spring.ai.azure.openai.chat.options..deployment-name=" + DeploymentNameUtil.getDeploymentName()) - .run(context -> { - - ChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Paris and in Tokyo? Use Multi-turn function calling."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - AzureOpenAiChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel.call(new Prompt(List.of(userMessage), - AzureOpenAiChatOptions.builder().function("weatherFunction3").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner - .withPropertyValues( - "spring.ai.azure.openai.chat.options..deployment-name=" + DeploymentNameUtil.getDeploymentName()) - .run(context -> { - - ChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Paris and in Tokyo? Use Multi-turn function calling."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunction3() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java deleted file mode 100644 index b809da5dd..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+") -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+") -public class FunctionCallWithFunctionWrapperIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionWrapperIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), - "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) - // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues( - "spring.ai.azure.openai.chat.options.deployment-name=" + DeploymentNameUtil.getDeploymentName()) - .run(context -> { - - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Paris and in Tokyo?"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - AzureOpenAiChatOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsAnyOf("30", "10", "15"); - - }); - } - - @Configuration - static class Config { - - @Bean - public ToolCallback weatherFunctionInfo() { - - return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) - .description("Get the current weather in a given location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java deleted file mode 100644 index 90e5d7a9b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration; -import org.springframework.ai.azure.openai.AzureOpenAiChatModel; -import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+") -@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+") -public class FunctionCallWithPromptFunctionIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"), - "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT")) - // @formatter:onn - .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues( - "spring.ai.azure.openai.chat.options.deployment-name=" + DeploymentNameUtil.getDeploymentName()) - .run(context -> { - - AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris and in Tokyo? Use Multi-turn function calling."); - - var promptOptions = AzureOpenAiChatOptions.builder() - .functionCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java deleted file mode 100644 index 20c1899ef..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.model.azure.openai.autoconfigure.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java deleted file mode 100644 index af51a1e93..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure; - -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.auth.credentials.AwsCredentials; -import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; -import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.regions.providers.AwsRegionProvider; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Wei Jiang - * @author Mark Pollack - * @since 1.0.0 - */ -@RequiresAwsCredentials -public class BedrockAwsConnectionConfigurationIT { - - @Test - public void autoConfigureAWSCredentialAndRegionProvider() { - BedrockTestUtils.getContextRunner() - .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class)) - .run(context -> { - var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class); - var awsRegionProvider = context.getBean(AwsRegionProvider.class); - - assertThat(awsCredentialsProvider).isNotNull(); - assertThat(awsRegionProvider).isNotNull(); - - var credentials = awsCredentialsProvider.resolveCredentials(); - assertThat(credentials).isNotNull(); - assertThat(credentials.accessKeyId()).isEqualTo(System.getenv("AWS_ACCESS_KEY_ID")); - assertThat(credentials.secretAccessKey()).isEqualTo(System.getenv("AWS_SECRET_ACCESS_KEY")); - - assertThat(awsRegionProvider.getRegion()).isEqualTo(Region.US_EAST_1); - }); - } - - @Test - public void autoConfigureWithCustomAWSCredentialAndRegionProvider() { - BedrockTestUtils.getContextRunner() - .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class, - CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration.class)) - .run(context -> { - var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class); - var awsRegionProvider = context.getBean(AwsRegionProvider.class); - - assertThat(awsCredentialsProvider).isNotNull(); - assertThat(awsRegionProvider).isNotNull(); - - var credentials = awsCredentialsProvider.resolveCredentials(); - assertThat(credentials).isNotNull(); - assertThat(credentials.accessKeyId()).isEqualTo("CUSTOM_ACCESS_KEY"); - assertThat(credentials.secretAccessKey()).isEqualTo("CUSTOM_SECRET_ACCESS_KEY"); - - assertThat(awsRegionProvider.getRegion()).isEqualTo(Region.AWS_GLOBAL); - }); - } - - @EnableConfigurationProperties({ BedrockAwsConnectionProperties.class }) - @Import(BedrockAwsConnectionConfiguration.class) - static class TestAutoConfiguration { - - } - - @AutoConfiguration - static class CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public AwsCredentialsProvider credentialsProvider() { - return new AwsCredentialsProvider() { - - @Override - public AwsCredentials resolveCredentials() { - return new AwsCredentials() { - - @Override - public String accessKeyId() { - return "CUSTOM_ACCESS_KEY"; - } - - @Override - public String secretAccessKey() { - return "CUSTOM_SECRET_ACCESS_KEY"; - } - - }; - } - - }; - } - - @Bean - @ConditionalOnMissingBean - public AwsRegionProvider regionProvider() { - return new AwsRegionProvider() { - - @Override - public Region getRegion() { - return Region.AWS_GLOBAL; - } - - }; - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java deleted file mode 100644 index 88b12b6c3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure; - -import com.fasterxml.jackson.databind.ObjectMapper; -import software.amazon.awssdk.regions.Region; - -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -public final class BedrockTestUtils { - - private BedrockTestUtils() { - } // Prevent instantiation - - public static ApplicationContextRunner getContextRunner() { - return new ApplicationContextRunner() - .withPropertyValues("spring.ai.bedrock.aws.access-key=" + System.getenv("AWS_ACCESS_KEY_ID"), - "spring.ai.bedrock.aws.secret-key=" + System.getenv("AWS_SECRET_ACCESS_KEY"), - "spring.ai.bedrock.aws.session-token=" + System.getenv("AWS_SESSION_TOKEN"), - "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id()) - .withUserConfiguration(Config.class); - } - - public static ApplicationContextRunner getContextRunnerWithUserConfiguration() { - return new ApplicationContextRunner().withUserConfiguration(Config.class); - } - - @Configuration - static class Config { - - @Bean - public ObjectMapper objectMapper() { - return new ObjectMapper(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java deleted file mode 100644 index 00f5e633b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -@Target({ ElementType.TYPE, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*") -@EnabledIfEnvironmentVariable(named = "AWS_SESSION_TOKEN", matches = ".*") -public @interface RequiresAwsCredentials { - - // You can add custom properties here if needed - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java deleted file mode 100644 index f612425cf..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.cohere; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.regions.Region; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; -import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest; -import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest.InputType; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Mark Pollack - * @since 1.0.0 - */ -@RequiresAwsCredentials -public class BedrockCohereEmbeddingAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true", - "spring.ai.bedrock.cohere.embedding.model=" + CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id(), - "spring.ai.bedrock.cohere.embedding.options.inputType=SEARCH_DOCUMENT", - "spring.ai.bedrock.cohere.embedding.options.truncate=NONE") - .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)); - - @Test - public void singleEmbedding() { - this.contextRunner.run(context -> { - BedrockCohereEmbeddingModel embeddingModel = context.getBean(BedrockCohereEmbeddingModel.class); - assertThat(embeddingModel).isNotNull(); - EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World")); - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); - } - - @Test - public void batchEmbedding() { - this.contextRunner.run(context -> { - - BedrockCohereEmbeddingModel embeddingModel = context.getBean(BedrockCohereEmbeddingModel.class); - - assertThat(embeddingModel).isNotNull(); - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - - }); - } - - @Test - public void propertiesTest() { - - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true", - "spring.ai.bedrock.aws.access-key=ACCESS_KEY", "spring.ai.bedrock.aws.secret-key=SECRET_KEY", - "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(), - "spring.ai.bedrock.cohere.embedding.model=MODEL_XYZ", - "spring.ai.bedrock.cohere.embedding.options.inputType=CLASSIFICATION", - "spring.ai.bedrock.cohere.embedding.options.truncate=START") - .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) - .run(context -> { - var properties = context.getBean(BedrockCohereEmbeddingProperties.class); - var awsProperties = context.getBean(BedrockAwsConnectionProperties.class); - - assertThat(properties.isEnabled()).isTrue(); - assertThat(awsProperties.getRegion()).isEqualTo(Region.US_EAST_1.id()); - assertThat(properties.getModel()).isEqualTo("MODEL_XYZ"); - - assertThat(properties.getOptions().getInputType()).isEqualTo(InputType.CLASSIFICATION); - assertThat(properties.getOptions().getTruncate()).isEqualTo(CohereEmbeddingRequest.Truncate.START); - - assertThat(awsProperties.getAccessKey()).isEqualTo("ACCESS_KEY"); - assertThat(awsProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - }); - } - - @Test - public void embeddingDisabled() { - - // It is disabled by default - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty(); - assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isEmpty(); - }); - - // Explicitly enable the embedding auto-configuration. - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isNotEmpty(); - }); - - // Explicitly disable the embedding auto-configuration. - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty(); - assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java deleted file mode 100644 index f89cda503..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.converse; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; -import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@RequiresAwsCredentials -public class BedrockConverseProxyChatAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(BedrockConverseProxyChatAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withPropertyValues( - "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0", - "spring.ai.bedrock.converse.chat.options.temperature=0.5") - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)); - - @Test - void call() { - this.contextRunner.run(context -> { - BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void stream() { - this.contextRunner.run(context -> { - BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - - String response = responseFlux.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java deleted file mode 100644 index b71900197..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.converse; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * - * Unit Tests for {@link BedrockConverseProxyChatProperties}. - */ -public class BedrockConverseProxyChatPropertiesTests { - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.bedrock.converse.chat.options.model=MODEL_XYZ", - - "spring.ai.bedrock.converse.chat.options.max-tokens=123", - "spring.ai.bedrock.converse.chat.options.metadata.user-id=MyUserId", - "spring.ai.bedrock.converse.chat.options.stop_sequences=boza,koza", - - "spring.ai.bedrock.converse.chat.options.temperature=0.55", - "spring.ai.bedrock.converse.chat.options.top-p=0.56", - "spring.ai.bedrock.converse.chat.options.top-k=100" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(BedrockConverseProxyChatProperties.class); - - assertThat(chatProperties.isEnabled()).isTrue(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getStopSequences()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100); - - }); - } - - @Test - public void chatCompletionDisabled() { - - // It is enabled by default - new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty()); - - // Explicitly enable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty()); - - // Explicitly disable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isEmpty()); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java deleted file mode 100644 index 95c595370..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.converse.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; -import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; -import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@RequiresAwsCredentials -public class FunctionCallWithPromptFunctionIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class); - - private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues( - "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0") - .run(context -> { - - BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius."); - - var promptOptions = ToolCallingChatOptions.builder() - .toolCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location. Return temperature in 36°F or 36°C format.") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java deleted file mode 100644 index 29081ec2f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.converse.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java deleted file mode 100644 index 107bb2a6d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.model.bedrock.autoconfigure.titan; - -import java.util.Base64; -import java.util.List; - -import org.junit.jupiter.api.Test; -import software.amazon.awssdk.regions.Region; - -import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; -import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils; -import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials; -import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; -import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType; -import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.io.DefaultResourceLoader; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Mark Pollack - * @since 1.0.0 - */ -@RequiresAwsCredentials -public class BedrockTitanEmbeddingAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true", - "spring.ai.bedrock.aws.access-key=" + System.getenv("AWS_ACCESS_KEY_ID"), - "spring.ai.bedrock.aws.secret-key=" + System.getenv("AWS_SECRET_ACCESS_KEY"), - "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(), - "spring.ai.bedrock.titan.embedding.model=" + TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id()) - .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)); - - @Test - public void singleTextEmbedding() { - this.contextRunner.withPropertyValues("spring.ai.bedrock.titan.embedding.inputType=TEXT").run(context -> { - BedrockTitanEmbeddingModel embeddingModel = context.getBean(BedrockTitanEmbeddingModel.class); - assertThat(embeddingModel).isNotNull(); - EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World")); - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); - } - - @Test - public void singleImageEmbedding() { - this.contextRunner.withPropertyValues("spring.ai.bedrock.titan.embedding.inputType=IMAGE").run(context -> { - BedrockTitanEmbeddingModel embeddingModel = context.getBean(BedrockTitanEmbeddingModel.class); - assertThat(embeddingModel).isNotNull(); - - byte[] image = new DefaultResourceLoader().getResource("classpath:/spring_framework.png") - .getContentAsByteArray(); - - var base64Image = Base64.getEncoder().encodeToString(image); - - EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of(base64Image)); - - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); - } - - @Test - public void propertiesTest() { - - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true", - "spring.ai.bedrock.aws.access-key=ACCESS_KEY", "spring.ai.bedrock.aws.secret-key=SECRET_KEY", - "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(), - "spring.ai.bedrock.titan.embedding.model=MODEL_XYZ", - "spring.ai.bedrock.titan.embedding.inputType=TEXT") - .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) - .run(context -> { - var properties = context.getBean(BedrockTitanEmbeddingProperties.class); - var awsProperties = context.getBean(BedrockAwsConnectionProperties.class); - - assertThat(properties.isEnabled()).isTrue(); - assertThat(awsProperties.getRegion()).isEqualTo(Region.US_EAST_1.id()); - assertThat(properties.getModel()).isEqualTo("MODEL_XYZ"); - - assertThat(properties.getInputType()).isEqualTo(InputType.TEXT); - - assertThat(awsProperties.getAccessKey()).isEqualTo("ACCESS_KEY"); - assertThat(awsProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - }); - } - - @Test - public void embeddingDisabled() { - - // It is disabled by default - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty(); - assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isEmpty(); - }); - - // Explicitly enable the embedding auto-configuration. - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isNotEmpty(); - }); - - // Explicitly disable the embedding auto-configuration. - BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty(); - assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java deleted file mode 100644 index 5906eb3b9..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.model.chat.client.autoconfigure; - -import java.util.List; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.ChatClientCustomizer; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - */ -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class ChatClientAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(ChatClientAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), - "spring.ai.openai.chat.options.model=gpt-4o") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class, ChatClientAutoConfiguration.class)); - - @Test - void implicitlyEnabled() { - this.contextRunner.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty()); - } - - @Test - void explicitlyEnabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=true") - .run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty()); - } - - @Test - void explicitlyDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=false") - .run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty()); - } - - @Test - void generate() { - this.contextRunner.run(context -> { - ChatClient.Builder builder = context.getBean(ChatClient.Builder.class); - - assertThat(builder).isNotNull(); - - ChatClient chatClient = builder.build(); - - String response = chatClient.prompt().user("Hello").call().content(); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void testChatClientCustomizers() { - this.contextRunner.withUserConfiguration(Config.class).run(context -> { - - ChatClient.Builder builder = context.getBean(ChatClient.Builder.class); - - ChatClient chatClient = builder.build(); - - assertThat(chatClient).isNotNull(); - - ActorsFilms actorsFilms = chatClient.prompt() - .user(u -> u.param("actor", "Tom Hanks")) - .call() - .entity(ActorsFilms.class); - - logger.info("" + actorsFilms); - assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks"); - assertThat(actorsFilms.movies()).hasSize(5); - }); - } - - record ActorsFilms(String actor, List movies) { - - } - - @Configuration - static class Config { - - @Bean - public ChatClientCustomizer chatClientCustomizer() { - return b -> b.defaultSystem("You are a movie expert.") - .defaultUser("Generate the filmography of 5 movies for {actor}."); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java deleted file mode 100644 index 7fd0ee9f8..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.model.chat.client.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.chat.client.observation.ChatClientInputContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ChatClientAutoConfiguration} observability support. - * - * @author Christian Tzolov - */ -class ChatClientObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class)); - - @Test - void inputContentFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatClientInputContentObservationFilter.class)); - } - - @Test - void inputContentFilterEnabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.client.observations.include-input=true") - .run(context -> assertThat(context).hasSingleBean(ChatClientInputContentObservationFilter.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java deleted file mode 100644 index a41904290..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.model.chat.memory.autoconfigure.cassandra; - -import java.time.Duration; -import java.util.List; - -import com.datastax.driver.core.utils.UUIDs; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.CassandraContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; - -import org.springframework.ai.chat.memory.cassandra.CassandraChatMemory; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.MessageType; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Mick Semb Wever - * @author Jihoon Kim - * @since 1.0.0 - */ -@Testcontainers -class CassandraChatMemoryAutoConfigurationIT { - - static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra"); - - @Container - static CassandraContainer cassandraContainer = new CassandraContainer(DEFAULT_IMAGE_NAME.withTag("5.0")); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(CassandraChatMemoryAutoConfiguration.class, CassandraAutoConfiguration.class)) - .withPropertyValues("spring.ai.chat.memory.cassandra.keyspace=test_autoconfigure"); - - @Test - void addAndGet() { - this.contextRunner.withPropertyValues("spring.cassandra.contactPoints=" + getContactPointHost()) - .withPropertyValues("spring.cassandra.port=" + getContactPointPort()) - .withPropertyValues("spring.cassandra.localDatacenter=" + cassandraContainer.getLocalDatacenter()) - .withPropertyValues("spring.ai.chat.memory.cassandra.time-to-live=" + getTimeToLive()) - .run(context -> { - CassandraChatMemory memory = context.getBean(CassandraChatMemory.class); - - String sessionId = UUIDs.timeBased().toString(); - assertThat(memory.get(sessionId, Integer.MAX_VALUE)).isEmpty(); - - memory.add(sessionId, new UserMessage("test question")); - - assertThat(memory.get(sessionId, Integer.MAX_VALUE)).hasSize(1); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getMessageType()) - .isEqualTo(MessageType.USER); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getText()).isEqualTo("test question"); - - memory.clear(sessionId); - assertThat(memory.get(sessionId, Integer.MAX_VALUE)).isEmpty(); - - memory.add(sessionId, List.of(new UserMessage("test question"), new AssistantMessage("test answer"))); - - assertThat(memory.get(sessionId, Integer.MAX_VALUE)).hasSize(2); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(1).getMessageType()) - .isEqualTo(MessageType.USER); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(1).getText()).isEqualTo("test question"); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getMessageType()) - .isEqualTo(MessageType.ASSISTANT); - assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getText()).isEqualTo("test answer"); - - CassandraChatMemoryProperties properties = context.getBean(CassandraChatMemoryProperties.class); - assertThat(properties.getTimeToLive()).isEqualTo(getTimeToLive()); - }); - } - - @Test - void compareTimeToLive_ISO8601Format() { - this.contextRunner.withPropertyValues("spring.cassandra.contactPoints=" + getContactPointHost()) - .withPropertyValues("spring.cassandra.port=" + getContactPointPort()) - .withPropertyValues("spring.cassandra.localDatacenter=" + cassandraContainer.getLocalDatacenter()) - .withPropertyValues("spring.ai.chat.memory.cassandra.time-to-live=" + getTimeToLiveString()) - .run(context -> { - CassandraChatMemoryProperties properties = context.getBean(CassandraChatMemoryProperties.class); - assertThat(properties.getTimeToLive()).isEqualTo(Duration.parse(getTimeToLiveString())); - }); - } - - private String getContactPointHost() { - return cassandraContainer.getContactPoint().getHostString(); - } - - private String getContactPointPort() { - return String.valueOf(cassandraContainer.getContactPoint().getPort()); - } - - private Duration getTimeToLive() { - return Duration.ofSeconds(12000); - } - - private String getTimeToLiveString() { - return "PT1M"; - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java deleted file mode 100644 index dab9eb806..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.chat.memory.autoconfigure.cassandra; - -import java.time.Duration; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Mick Semb Wever - * @author Jihoon Kim - * @since 1.0.0 - */ -class CassandraChatMemoryPropertiesTest { - - @Test - void defaultValues() { - var props = new CassandraChatMemoryProperties(); - assertThat(props.getKeyspace()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_KEYSPACE_NAME); - assertThat(props.getTable()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_TABLE_NAME); - assertThat(props.getAssistantColumn()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_ASSISTANT_COLUMN_NAME); - assertThat(props.getUserColumn()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_USER_COLUMN_NAME); - assertThat(props.getTimeToLive()).isNull(); - assertThat(props.isInitializeSchema()).isTrue(); - } - - @Test - void customValues() { - var props = new CassandraChatMemoryProperties(); - props.setKeyspace("my_keyspace"); - props.setTable("my_table"); - props.setAssistantColumn("my_assistant_column"); - props.setUserColumn("my_user_column"); - props.setTimeToLive(Duration.ofDays(1)); - props.setInitializeSchema(false); - - assertThat(props.getKeyspace()).isEqualTo("my_keyspace"); - assertThat(props.getTable()).isEqualTo("my_table"); - assertThat(props.getAssistantColumn()).isEqualTo("my_assistant_column"); - assertThat(props.getUserColumn()).isEqualTo("my_user_column"); - assertThat(props.getTimeToLive()).isEqualTo(Duration.ofDays(1)); - assertThat(props.isInitializeSchema()).isFalse(); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java deleted file mode 100644 index 3ac3f12ca..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.model.chat.observation.autoconfigure; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; -import io.micrometer.tracing.otel.bridge.OtelTracer; -import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter; -import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler; -import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter; -import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ChatObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ChatObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ChatObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(ChatModelMeterObservationHandler.class)); - } - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationFilter.class)); - } - - @Test - void promptHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void promptHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-prompt=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class)); - } - - @Test - void completionFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationFilter.class)); - } - - @Test - void completionHandlerDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerEnabled() { - this.contextRunner - .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null) - .withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).hasSingleBean(ChatModelCompletionObservationHandler.class)); - } - - @Test - void completionHandlerDisabled() { - this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-completion=true") - .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java deleted file mode 100644 index 10c9feb7c..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.model.embedding.observation.autoconfigure; - -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link EmbeddingObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class EmbeddingObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EmbeddingObservationAutoConfiguration.class)); - - @Test - void meterObservationHandlerEnabled() { - this.contextRunner.withBean(CompositeMeterRegistry.class) - .run(context -> assertThat(context).hasSingleBean(EmbeddingModelMeterObservationHandler.class)); - } - - @Test - void meterObservationHandlerDisabled() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(EmbeddingModelMeterObservationHandler.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java deleted file mode 100644 index db991fe08..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.model.huggingface.autoconfigure; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.huggingface.HuggingfaceChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "HUGGINGFACE_API_KEY", matches = ".+") -@EnabledIfEnvironmentVariable(named = "HUGGINGFACE_CHAT_URL", matches = ".+") -public class HuggingfaceChatAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(HuggingfaceChatAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.huggingface.chat.api-key=" + System.getenv("HUGGINGFACE_API_KEY"), - "spring.ai.huggingface.chat.url=" + System.getenv("HUGGINGFACE_CHAT_URL")) - // @formatter:on - .withConfiguration(AutoConfigurations.of(HuggingfaceChatAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - HuggingfaceChatModel chatModel = context.getBean(HuggingfaceChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Disabled("Until streaming support is added") - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - HuggingfaceChatModel chatModel = context.getBean(HuggingfaceChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - - String response = responseFlux.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java deleted file mode 100644 index 23417b67b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.model.image.observation.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ImageObservationAutoConfiguration}. - * - * @author Thomas Vitale - */ -class ImageObservationAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ImageObservationAutoConfiguration.class)); - - @Test - void promptFilterDefault() { - this.contextRunner - .run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationFilter.class)); - } - - @Test - void promptFilterEnabled() { - this.contextRunner.withPropertyValues("spring.ai.image.observations.include-prompt=true") - .run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationFilter.class)); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java deleted file mode 100644 index f86e76185..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import java.util.List; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*") -public class FunctionCallbackInPromptIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - var promptOptions = MiniMaxChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamingFunctionCallTest() { - - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - var promptOptions = MiniMaxChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions)); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java deleted file mode 100644 index 6d511b709..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import java.util.List; -import java.util.function.Function; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*") -class FunctionCallbackWithPlainFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - // FIXME: multiple function calls may stop prematurely due to model performance - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ChatResponse response = chatModel.call( - new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - // Test weatherFunctionTwo - response = chatModel.call(new Prompt(List.of(userMessage), - MiniMaxChatOptions.builder().function("weatherFunctionTwo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ToolCallingChatOptions toolOptions = ToolCallingChatOptions.builder().toolNames("weatherFunction").build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), toolOptions)); - - logger.info("Response: {}", response); - }); - } - - // FIXME: multiple function calls may stop prematurely due to model performance - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - Flux response = chatModel.stream( - new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("weatherFunction").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - // Test weatherFunctionTwo - response = chatModel.stream(new Prompt(List.of(userMessage), - MiniMaxChatOptions.builder().function("weatherFunctionTwo").build())); - - content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunctionTwo() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java deleted file mode 100644 index 2b82ba7d4..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxEmbeddingModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*") -public class MiniMaxAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(MiniMaxAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java deleted file mode 100644 index 5bf102959..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import java.util.List; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*") -public class MiniMaxFunctionCallbackIT { - - private final Logger logger = LoggerFactory.getLogger(MiniMaxFunctionCallbackIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> { - - MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("WeatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - }); - } - - @Configuration - static class Config { - - @Bean - public FunctionCallback weatherFunctionInfo() { - - return FunctionCallback.builder() - .function("WeatherInfo", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java deleted file mode 100644 index b0a75f743..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.minimax.MiniMaxChatModel; -import org.springframework.ai.minimax.MiniMaxEmbeddingModel; -import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link MiniMaxConnectionProperties}, {@link MiniMaxChatProperties} and - * {@link MiniMaxEmbeddingProperties}. - * - * @author Geng Rong - */ -public class MiniMaxPropertiesTests { - - @Test - public void chatProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.api-key=abc123", - "spring.ai.minimax.chat.options.model=MODEL_XYZ", - "spring.ai.minimax.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MiniMaxChatProperties.class); - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isNull(); - assertThat(chatProperties.getBaseUrl()).isNull(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void chatOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.api-key=abc123", - "spring.ai.minimax.chat.base-url=TEST_BASE_URL2", - "spring.ai.minimax.chat.api-key=456", - "spring.ai.minimax.chat.options.model=MODEL_XYZ", - "spring.ai.minimax.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MiniMaxChatProperties.class); - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isEqualTo("456"); - assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void embeddingProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.api-key=abc123", - "spring.ai.minimax.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isNull(); - assertThat(embeddingProperties.getBaseUrl()).isNull(); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void embeddingOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.api-key=abc123", - "spring.ai.minimax.embedding.base-url=TEST_BASE_URL2", - "spring.ai.minimax.embedding.api-key=456", - "spring.ai.minimax.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isEqualTo("456"); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.api-key=API_KEY", - "spring.ai.minimax.base-url=TEST_BASE_URL", - - "spring.ai.minimax.chat.options.model=MODEL_XYZ", - "spring.ai.minimax.chat.options.frequencyPenalty=-1.5", - "spring.ai.minimax.chat.options.logitBias.myTokenId=-5", - "spring.ai.minimax.chat.options.maxTokens=123", - "spring.ai.minimax.chat.options.n=10", - "spring.ai.minimax.chat.options.presencePenalty=0", - "spring.ai.minimax.chat.options.responseFormat.type=json", - "spring.ai.minimax.chat.options.seed=66", - "spring.ai.minimax.chat.options.stop=boza,koza", - "spring.ai.minimax.chat.options.temperature=0.55", - "spring.ai.minimax.chat.options.topP=0.56", - - // "spring.ai.minimax.chat.options.toolChoice.functionName=toolChoiceFunctionName", - "spring.ai.minimax.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(MiniMaxApi.ChatCompletionRequest.ToolChoiceBuilder.function("toolChoiceFunctionName")), - - "spring.ai.minimax.chat.options.tools[0].function.name=myFunction1", - "spring.ai.minimax.chat.options.tools[0].function.description=function description", - "spring.ai.minimax.chat.options.tools[0].function.jsonSchema=" + """ - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state e.g. San Francisco, CA" - }, - "lat": { - "type": "number", - "description": "The city latitude" - }, - "lon": { - "type": "number", - "description": "The city longitude" - }, - "unit": { - "type": "string", - "enum": ["c", "f"] - } - }, - "required": ["location", "lat", "lon", "unit"] - } - """ - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MiniMaxChatProperties.class); - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("embo-01"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getN()).isEqualTo(10); - assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0); - assertThat(chatProperties.getOptions().getResponseFormat()) - .isEqualTo(new MiniMaxApi.ChatCompletionRequest.ResponseFormat("json")); - assertThat(chatProperties.getOptions().getSeed()).isEqualTo(66); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - - JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}", - chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT); - - assertThat(chatProperties.getOptions().getTools()).hasSize(1); - var tool = chatProperties.getOptions().getTools().get(0); - assertThat(tool.getType()).isEqualTo(MiniMaxApi.FunctionTool.Type.FUNCTION); - var function = tool.getFunction(); - assertThat(function.getName()).isEqualTo("myFunction1"); - assertThat(function.getDescription()).isEqualTo("function description"); - assertThat(function.getParameters()).isNotEmpty(); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.minimax.api-key=API_KEY", - "spring.ai.minimax.base-url=TEST_BASE_URL", - - "spring.ai.minimax.embedding.options.model=MODEL_XYZ", - "spring.ai.minimax.embedding.options.encodingFormat=MyEncodingFormat" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(MiniMaxConnectionProperties.class); - var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - void embeddingActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java deleted file mode 100644 index 3c95cf669..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.minimax.autoconfigure; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Geng Rong - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Get the weather in location") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, - @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java deleted file mode 100644 index b7bb85226..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.mistralai.MistralAiChatModel; -import org.springframework.ai.mistralai.MistralAiEmbeddingModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 0.8.1 - */ -@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") -public class MistralAiAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(MistralAiAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java deleted file mode 100644 index dd4eae9ab..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link MistralAiCommonProperties}, {@link MistralAiEmbeddingProperties}. - */ -public class MistralAiPropertiesTests { - - @Test - public void embeddingProperties() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", "spring.ai.mistralai.api-key=abc123", - "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); - var connectionProperties = context.getBean(MistralAiCommonProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isNull(); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo(MistralAiCommonProperties.DEFAULT_BASE_URL); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", - "spring.ai.mistralai.chat.options.tools[0].function.name=myFunction1", - "spring.ai.mistralai.chat.options.tools[0].function.description=function description", - "spring.ai.mistralai.chat.options.tools[0].function.jsonSchema=" + """ - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state e.g. San Francisco, CA" - }, - "lat": { - "type": "number", - "description": "The city latitude" - }, - "lon": { - "type": "number", - "description": "The city longitude" - }, - "unit": { - "type": "string", - "enum": ["c", "f"] - } - }, - "required": ["location", "lat", "lon", "unit"] - } - """, - - "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2", - "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) - .run(context -> { - - var chatProperties = context.getBean(MistralAiChatProperties.class); - - var tool = chatProperties.getOptions().getTools().get(0); - assertThat(tool.getType()).isEqualTo(MistralAiApi.FunctionTool.Type.FUNCTION); - var function = tool.getFunction(); - assertThat(function.getName()).isEqualTo("myFunction1"); - assertThat(function.getDescription()).isEqualTo("function description"); - assertThat(function.getParameters()).isNotEmpty(); - }); - } - - @Test - public void embeddingOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", - "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2", - "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); - var connectionProperties = context.getBean(MistralAiCommonProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isEqualTo("456"); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.api-key=API_KEY", "spring.ai.mistralai.base-url=TEST_BASE_URL", - - "spring.ai.mistralai.embedding.options.model=MODEL_XYZ", - "spring.ai.mistralai.embedding.options.encodingFormat=MyEncodingFormat") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(MistralAiCommonProperties.class); - var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java deleted file mode 100644 index bd7ed874a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure.tool; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.mistralai.MistralAiChatModel; -import org.springframework.ai.mistralai.MistralAiChatOptions; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") -class PaymentStatusBeanIT { - - // Assuming we have the following data - public static final Map DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002", - new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004", - new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08")); - - private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - - this.contextRunner - .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue()) - .run(context -> { - - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - - ChatResponse response = chatModel - .call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")), - MistralAiChatOptions.builder() - .function("retrievePaymentStatus") - .function("retrievePaymentDate") - .build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001"); - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid"); - }); - } - - record StatusDate(String status, String date) { - - } - - @Configuration - static class Config { - - @Bean - @Description("Get payment status of a transaction") - public Function retrievePaymentStatus() { - return transaction -> new Status(DATA.get(transaction.transactionId).status()); - } - - @Bean - @Description("Get payment date of a transaction") - public Function retrievePaymentDate() { - return transaction -> new Date(DATA.get(transaction.transactionId).date()); - } - - public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) { - - } - - public record Status(@JsonProperty(required = true, value = "status") String status) { - - } - - public record Date(@JsonProperty(required = true, value = "date") String date) { - - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java deleted file mode 100644 index 9c4b1c71a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure.tool; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Same test as {@link PaymentStatusBeanIT.java} but using {@link OpenAiChatModel} for - * Mistral AI Function Calling implementation. - * - * @author Christian Tzolov - */ -@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") -class PaymentStatusBeanOpenAiIT { - - // Assuming we have the following data - public static final Map DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002", - new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004", - new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08")); - - private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"), - "spring.ai.openai.chat.base-url=https://api.mistral.ai") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - - this.contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue()) - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - ChatResponse response = chatModel - .call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")), - OpenAiChatOptions.builder() - .function("retrievePaymentStatus") - .function("retrievePaymentDate") - .build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001"); - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid"); - }); - } - - record StatusDate(String status, String date) { - - } - - @Configuration - static class Config { - - @Bean - @Description("Get payment status of a transaction") - public Function retrievePaymentStatus() { - return transaction -> new Status(DATA.get(transaction.transactionId).status()); - } - - @Bean - @Description("Get payment date of a transaction") - public Function retrievePaymentDate() { - return transaction -> new Date(DATA.get(transaction.transactionId).date()); - } - - public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) { - - } - - public record Status(@JsonProperty(required = true, value = "status") String status) { - - } - - public record Date(@JsonProperty(required = true, value = "date") String date) { - - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java deleted file mode 100644 index 01a278a3d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure.tool; - -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.mistralai.MistralAiChatModel; -import org.springframework.ai.mistralai.MistralAiChatOptions; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") -public class PaymentStatusPromptIT { - - // Assuming we have the following payment data. - public static final Map DATA = Map.of(new Transaction("T1001"), - new StatusDate("Paid", "2021-10-05"), new Transaction("T1002"), new StatusDate("Unpaid", "2021-10-06"), - new Transaction("T1003"), new StatusDate("Paid", "2021-10-07"), new Transaction("T1004"), - new StatusDate("Paid", "2021-10-05"), new Transaction("T1005"), new StatusDate("Pending", "2021-10-08")); - - private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue()) - .run(context -> { - - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - - UserMessage userMessage = new UserMessage("What's the status of my transaction with id T1001?"); - - var promptOptions = MistralAiChatOptions.builder() - .functionCallbacks(List.of(FunctionToolCallback - .builder("retrievePaymentStatus", - (Transaction transaction) -> new Status(DATA.get(transaction).status())) - .description("Get payment status of a transaction") - .inputType(Transaction.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001"); - assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid"); - }); - } - - public record Transaction(@JsonProperty(required = true, value = "transaction_id") String id) { - - } - - public record Status(@JsonProperty(required = true, value = "status") String status) { - - } - - record StatusDate(String status, String date) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java deleted file mode 100644 index 758ab779f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.model.mistralai.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration; -import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request; -import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.mistralai.MistralAiChatModel; -import org.springframework.ai.mistralai.MistralAiChatOptions; -import org.springframework.ai.mistralai.api.MistralAiApi; -import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Alexandros Pappas - * @since 0.8.1 - */ -@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*") -public class WeatherServicePromptIT { - - private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.mistralai.api-key=" + System.getenv("MISTRAL_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)); - - @Test - void promptFunctionCall() { - this.contextRunner - .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue()) - .run(context -> { - - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - - UserMessage userMessage = new UserMessage("What's the weather like in Paris? Use Celsius."); - // UserMessage userMessage = new UserMessage("What's the weather like in - // San Francisco, Tokyo, and - // Paris?"); - - var promptOptions = MistralAiChatOptions.builder() - .toolChoice(ToolChoice.AUTO) - .functionCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService()) - .description("Get the current weather in requested location") - .inputType(MyWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsAnyOf("15", "15.0"); - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner - .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue()) - .run(context -> { - - MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); - - UserMessage userMessage = new UserMessage("What's the weather like in Paris? Use Celsius."); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder() - .toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService()) - .description("Get the current weather in requested location") - .inputType(MyWeatherService.Request.class) - .build())) - - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).containsAnyOf("15", "15.0"); - }); - } - - public static class MyWeatherService implements Function { - - @Override - public Response apply(Request request) { - if (request.location().contains("Paris")) { - return new Response(15, request.unit()); - } - else if (request.location().contains("Tokyo")) { - return new Response(10, request.unit()); - } - else if (request.location().contains("San Francisco")) { - return new Response(30, request.unit()); - } - throw new IllegalArgumentException("Invalid request: " + request); - } - - // @formatter:off - public enum Unit { C, F } - - @JsonInclude(Include.NON_NULL) - public record Request( - @JsonProperty(required = true, value = "location") String location, - @JsonProperty(required = true, value = "unit") Unit unit) { } - // @formatter:on - - public record Response(double temperature, Unit unit) { - - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java deleted file mode 100644 index c97df8b12..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -public class MoonshotPropertiesTests { - - @Test - public void chatProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.moonshot.base-url=TEST_BASE_URL", - "spring.ai.moonshot.api-key=abc123", - "spring.ai.moonshot.chat.options.model=MODEL_XYZ", - "spring.ai.moonshot.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MoonshotChatProperties.class); - var connectionProperties = context.getBean(MoonshotCommonProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isNull(); - assertThat(chatProperties.getBaseUrl()).isNull(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void chatOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.moonshot.base-url=TEST_BASE_URL", - "spring.ai.moonshot.api-key=abc123", - "spring.ai.moonshot.chat.base-url=TEST_BASE_URL2", - "spring.ai.moonshot.chat.api-key=456", - "spring.ai.moonshot.chat.options.model=MODEL_XYZ", - "spring.ai.moonshot.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MoonshotChatProperties.class); - var connectionProperties = context.getBean(MoonshotCommonProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isEqualTo("456"); - assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.moonshot.api-key=API_KEY", - "spring.ai.moonshot.base-url=TEST_BASE_URL", - - "spring.ai.moonshot.chat.options.model=MODEL_XYZ", - "spring.ai.moonshot.chat.options.frequencyPenalty=-1.5", - "spring.ai.moonshot.chat.options.logitBias.myTokenId=-5", - "spring.ai.moonshot.chat.options.maxTokens=123", - "spring.ai.moonshot.chat.options.n=10", - "spring.ai.moonshot.chat.options.presencePenalty=0", - "spring.ai.moonshot.chat.options.responseFormat.type=json", - "spring.ai.moonshot.chat.options.seed=66", - "spring.ai.moonshot.chat.options.stop=boza,koza", - "spring.ai.moonshot.chat.options.temperature=0.55", - "spring.ai.moonshot.chat.options.topP=0.56", - "spring.ai.moonshot.chat.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(MoonshotChatProperties.class); - var connectionProperties = context.getBean(MoonshotCommonProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getN()).isEqualTo(10); - assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - - assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL", - "spring.ai.moonshot.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MoonshotChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL", - "spring.ai.moonshot.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java deleted file mode 100644 index 0dc2271f3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure.tool; - -import java.util.List; -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.model.moonshot.autoconfigure.MoonshotAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - * @author Alexandros Pappas - */ -@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*") -public class FunctionCallbackInPromptIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - var promptOptions = MoonshotChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamingFunctionCallTest() { - - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - var promptOptions = MoonshotChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions)); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java deleted file mode 100644 index 72b3681c8..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; -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.model.moonshot.autoconfigure.MoonshotAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - * @author Alexandros Pappas - */ -@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*") -class FunctionCallbackWithPlainFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - MoonshotChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - // Test weatherFunctionTwo - response = chatModel.call(new Prompt(List.of(userMessage), - MoonshotChatOptions.builder().function("weatherFunctionTwo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - ToolCallingChatOptions toolOptions = ToolCallingChatOptions.builder().toolNames("weatherFunction").build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), toolOptions)); - - logger.info("Response: {}", response); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), - MoonshotChatOptions.builder().function("weatherFunction").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - // Test weatherFunctionTwo - response = chatModel.stream(new Prompt(List.of(userMessage), - MoonshotChatOptions.builder().function("weatherFunctionTwo").build())); - - content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunctionTwo() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java deleted file mode 100644 index 1fbc6858b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Geng Rong - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java deleted file mode 100644 index 86a85a9e1..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.model.moonshot.autoconfigure.tool; - -import java.util.List; -import java.util.Objects; -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.model.moonshot.autoconfigure.MoonshotAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - * @author Alexandros Pappas - */ -@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*") -public class MoonshotFunctionCallbackIT { - - private final Logger logger = LoggerFactory.getLogger(MoonshotFunctionCallbackIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.run(context -> { - - MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius"); - - Flux response = chatModel.stream( - new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .filter(Objects::nonNull) - .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"); - - }); - } - - @Configuration - static class Config { - - @Bean - public FunctionCallback weatherFunctionInfo() { - - return FunctionCallback.builder() - .function("WeatherInfo", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java deleted file mode 100644 index b8935152e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyPair; -import java.security.KeyPairGenerator; - -import com.oracle.bmc.http.client.pki.Pem; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.ai.oci.cohere.OCICohereChatModel; -import org.springframework.ai.oci.cohere.OCICohereChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -class OCIGenAIAutoConfigurationTest { - - @Test - void setProperties(@TempDir Path tempDir) throws Exception { - Path tmp = tempDir.resolve("my-key.pem"); - createPrivateKey(tmp); - ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.oci.genai.authenticationType=simple", - "spring.ai.oci.genai.userId=my-user", - "spring.ai.oci.genai.tenantId=my-tenant", - "spring.ai.oci.genai.fingerprint=xyz", - "spring.ai.oci.genai.privateKey=" + tmp.toAbsolutePath(), - "spring.ai.oci.genai.region=us-ashburn-1", - "spring.ai.oci.genai.cohere.chat.options.compartment=my-compartment", - "spring.ai.oci.genai.cohere.chat.options.servingMode=dedicated", - "spring.ai.oci.genai.cohere.chat.options.model=my-model", - "spring.ai.oci.genai.cohere.chat.options.maxTokens=1000", - "spring.ai.oci.genai.cohere.chat.options.temperature=0.5", - "spring.ai.oci.genai.cohere.chat.options.topP=0.8", - "spring.ai.oci.genai.cohere.chat.options.maxTokens=1000", - "spring.ai.oci.genai.cohere.chat.options.frequencyPenalty=0.1", - "spring.ai.oci.genai.cohere.chat.options.presencePenalty=0.2" - // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); - - contextRunner.run(context -> { - OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class); - assertThat(chatModel).isNotNull(); - OCICohereChatOptions options = (OCICohereChatOptions) chatModel.getDefaultOptions(); - assertThat(options.getCompartment()).isEqualTo("my-compartment"); - assertThat(options.getModel()).isEqualTo("my-model"); - assertThat(options.getServingMode()).isEqualTo("dedicated"); - assertThat(options.getMaxTokens()).isEqualTo(1000); - assertThat(options.getTemperature()).isEqualTo(0.5); - assertThat(options.getTopP()).isEqualTo(0.8); - assertThat(options.getFrequencyPenalty()).isEqualTo(0.1); - assertThat(options.getPresencePenalty()).isEqualTo(0.2); - - OCIConnectionProperties props = context.getBean(OCIConnectionProperties.class); - assertThat(props.getAuthenticationType()).isEqualTo(OCIConnectionProperties.AuthenticationType.SIMPLE); - assertThat(props.getUserId()).isEqualTo("my-user"); - assertThat(props.getTenantId()).isEqualTo("my-tenant"); - assertThat(props.getFingerprint()).isEqualTo("xyz"); - assertThat(props.getPrivateKey()).isEqualTo(tmp.toAbsolutePath().toString()); - assertThat(props.getRegion()).isEqualTo("us-ashburn-1"); - - }); - } - - private void createPrivateKey(Path tmp) throws Exception { - KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); - gen.initialize(2048); - KeyPair keyPair = gen.generateKeyPair(); - byte[] encoded = Pem.encoder().encode(keyPair.getPrivate()); - Files.write(tmp, encoded); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java deleted file mode 100644 index 610ef9c97..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.model.oci.genai.autoconfigure; - -import java.nio.file.Paths; -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import org.springframework.ai.embedding.EmbeddingRequest; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.oci.OCIEmbeddingModel; -import org.springframework.ai.oci.cohere.OCICohereChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = OCIGenAiAutoConfigurationIT.COMPARTMENT_ID_KEY, matches = ".+") -public class OCIGenAiAutoConfigurationIT { - - public static final String COMPARTMENT_ID_KEY = "OCI_COMPARTMENT_ID"; - - public static final String OCI_CHAT_MODEL_ID_KEY = "OCI_CHAT_MODEL_ID"; - - private final String CONFIG_FILE = Paths.get(System.getProperty("user.home"), ".oci", "config").toString(); - - private final String COMPARTMENT_ID = System.getenv(COMPARTMENT_ID_KEY); - - private final String CHAT_MODEL_ID = System.getenv(OCI_CHAT_MODEL_ID_KEY); - - private final ApplicationContextRunner embeddingContextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.oci.genai.authenticationType=file", - "spring.ai.oci.genai.file=" + this.CONFIG_FILE, - "spring.ai.oci.genai.embedding.compartment=" + this.COMPARTMENT_ID, - "spring.ai.oci.genai.embedding.servingMode=on-demand", - "spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0" - // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); - - private final ApplicationContextRunner cohereChatContextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.oci.genai.authenticationType=file", - "spring.ai.oci.genai.file=" + this.CONFIG_FILE, - "spring.ai.oci.genai.cohere.chat.options.compartment=" + this.COMPARTMENT_ID, - "spring.ai.oci.genai.cohere.chat.options.servingMode=on-demand", - "spring.ai.oci.genai.cohere.chat.options.model=" + this.CHAT_MODEL_ID - // @formatter:on - ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class)); - - @Test - void embeddings() { - this.embeddingContextRunner.run(context -> { - OCIEmbeddingModel embeddingModel = context.getBean(OCIEmbeddingModel.class); - assertThat(embeddingModel).isNotNull(); - EmbeddingResponse response = embeddingModel - .call(new EmbeddingRequest(List.of("There are 50 states in the USA", "Canada has 10 provinces"), null)); - assertThat(response).isNotNull(); - assertThat(response.getResults()).hasSize(2); - }); - } - - @Test - @EnabledIfEnvironmentVariable(named = OCIGenAiAutoConfigurationIT.OCI_CHAT_MODEL_ID_KEY, matches = ".+") - void cohereChat() { - this.cohereChatContextRunner.run(context -> { - OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class); - assertThat(chatModel).isNotNull(); - String response = chatModel.call("How many states are in the United States of America?"); - assertThat(response).isNotBlank(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java deleted file mode 100644 index b57f2ccab..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import java.time.Duration; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.ollama.OllamaContainer; - -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.management.ModelManagementOptions; -import org.springframework.ai.ollama.management.OllamaModelManager; -import org.springframework.ai.ollama.management.PullModelStrategy; -import org.springframework.util.Assert; - -@Testcontainers -@EnabledIfEnvironmentVariable(named = "OLLAMA_AUTOCONF_TESTS_ENABLED", matches = "true") -public abstract class BaseOllamaIT { - - static { - System.out.println("OLLAMA_AUTOCONF_TESTS_ENABLED=" + System.getenv("OLLAMA_AUTOCONF_TESTS_ENABLED")); - System.out.println("System property=" + System.getProperty("OLLAMA_AUTOCONF_TESTS_ENABLED")); - } - private static final String OLLAMA_LOCAL_URL = "http://localhost:11434"; - - private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(10); - - private static final int DEFAULT_MAX_RETRIES = 2; - - // Environment variable to control whether to create a new container or use existing - // Ollama instance - private static final boolean SKIP_CONTAINER_CREATION = Boolean - .parseBoolean(System.getenv().getOrDefault("OLLAMA_WITH_REUSE", "false")); - - private static OllamaContainer ollamaContainer; - - private static final ThreadLocal ollamaApi = new ThreadLocal<>(); - - /** - * Initialize the Ollama API with the specified model. When OLLAMA_WITH_REUSE=true - * (default), uses TestContainers withReuse feature. When OLLAMA_WITH_REUSE=false, - * connects to local Ollama instance. - * @param model the Ollama model to initialize (must not be null or empty) - * @return configured OllamaApi instance - * @throws IllegalArgumentException if model is null or empty - */ - protected static OllamaApi initializeOllama(final String model) { - Assert.hasText(model, "Model name must be provided"); - - if (!SKIP_CONTAINER_CREATION) { - ollamaContainer = new OllamaContainer(OllamaImage.DEFAULT_IMAGE).withReuse(true); - ollamaContainer.start(); - } - - final OllamaApi api = buildOllamaApiWithModel(model); - ollamaApi.set(api); - return api; - } - - /** - * Get the initialized OllamaApi instance. - * @return the OllamaApi instance - * @throws IllegalStateException if called before initialization - */ - protected static OllamaApi getOllamaApi() { - OllamaApi api = ollamaApi.get(); - Assert.state(api != null, "OllamaApi not initialized. Call initializeOllama first."); - return api; - } - - @AfterAll - public static void tearDown() { - if (ollamaContainer != null) { - ollamaContainer.stop(); - } - } - - public static OllamaApi buildOllamaApiWithModel(final String model) { - final String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint(); - final OllamaApi api = new OllamaApi(baseUrl); - ensureModelIsPresent(api, model); - return api; - } - - public String getBaseUrl() { - String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint(); - return baseUrl; - } - - private static void ensureModelIsPresent(final OllamaApi ollamaApi, final String model) { - final var modelManagementOptions = ModelManagementOptions.builder() - .maxRetries(DEFAULT_MAX_RETRIES) - .timeout(DEFAULT_TIMEOUT) - .build(); - final var ollamaModelManager = new OllamaModelManager(ollamaApi, modelManagementOptions); - ollamaModelManager.pullModel(model, PullModelStrategy.WHEN_MISSING); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java deleted file mode 100644 index 1623d3ed3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import java.io.IOException; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.management.OllamaModelManager; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Eddú Meléndez - * @author Thomas Vitale - * @since 0.8.0 - */ -public class OllamaChatAutoConfigurationIT extends BaseOllamaIT { - - private static final String MODEL_NAME = OllamaModel.LLAMA3_2.getName(); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.baseUrl=" + getBaseUrl(), - "spring.ai.ollama.chat.options.model=" + MODEL_NAME, - "spring.ai.ollama.chat.options.temperature=0.5", - "spring.ai.ollama.chat.options.topK=10") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); - - private final UserMessage userMessage = new UserMessage("What's the capital of Denmark?"); - - @BeforeAll - public static void beforeAll() throws IOException, InterruptedException { - initializeOllama(MODEL_NAME); - } - - @Test - public void chatCompletion() { - this.contextRunner.run(context -> { - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - ChatResponse response = chatModel.call(new Prompt(this.userMessage)); - assertThat(response.getResult().getOutput().getText()).contains("Copenhagen"); - }); - } - - @Test - public void chatCompletionStreaming() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - Flux response = chatModel.stream(new Prompt(this.userMessage)); - - List responses = response.collectList().block(); - assertThat(responses.size()).isGreaterThan(1); - - String stitchedResponseContent = responses.stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - - assertThat(stitchedResponseContent).contains("Copenhagen"); - }); - } - - @Test - public void chatCompletionWithPull() { - this.contextRunner.withPropertyValues("spring.ai.ollama.init.pull-model-strategy=when_missing") - .withPropertyValues("spring.ai.ollama.chat.options.model=tinyllama") - .run(context -> { - var model = "tinyllama"; - OllamaApi ollamaApi = context.getBean(OllamaApi.class); - var modelManager = new OllamaModelManager(ollamaApi); - assertThat(modelManager.isModelAvailable(model)).isTrue(); - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - ChatResponse response = chatModel.call(new Prompt(this.userMessage)); - assertThat(response.getResult().getOutput().getText()).contains("Copenhagen"); - modelManager.deleteModel(model); - }); - } - - @Test - void chatActivation() { - this.contextRunner.withPropertyValues("spring.ai.ollama.chat.enabled=false").run(context -> { - assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaChatModel.class)).isEmpty(); - }); - - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.ollama.chat.enabled=true").run(context -> { - assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java deleted file mode 100644 index 077073ef9..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 0.8.0 - */ -public class OllamaChatAutoConfigurationTests { - - @Test - public void propertiesTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.base-url=TEST_BASE_URL", - "spring.ai.ollama.chat.options.model=MODEL_XYZ", - "spring.ai.ollama.chat.options.temperature=0.55", - "spring.ai.ollama.chat.options.topP=0.56", - "spring.ai.ollama.chat.options.topK=123") - // @formatter:on - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OllamaChatProperties.class); - var connectionProperties = context.getBean(OllamaConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getModel()).isEqualTo("MODEL_XYZ"); - - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - - assertThat(chatProperties.getOptions().getTopK()).isEqualTo(123); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java deleted file mode 100644 index 1a38c32ae..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import java.io.IOException; -import java.util.List; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.ollama.OllamaEmbeddingModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.management.OllamaModelManager; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @author Thomas Vitale - * @since 1.0.0 - */ -public class OllamaEmbeddingAutoConfigurationIT extends BaseOllamaIT { - - private static final String MODEL_NAME = OllamaModel.NOMIC_EMBED_TEXT.getName(); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.ollama.embedding.options.model=" + MODEL_NAME, - "spring.ai.ollama.base-url=" + getBaseUrl()) - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)); - - @BeforeAll - public static void beforeAll() throws IOException, InterruptedException { - initializeOllama(MODEL_NAME); - } - - @Test - public void singleTextEmbedding() { - this.contextRunner.run(context -> { - OllamaEmbeddingModel embeddingModel = context.getBean(OllamaEmbeddingModel.class); - assertThat(embeddingModel).isNotNull(); - EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World")); - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingModel.dimensions()).isEqualTo(768); - }); - } - - @Test - public void embeddingWithPull() { - this.contextRunner.withPropertyValues("spring.ai.ollama.init.pull-model-strategy=when_missing") - .withPropertyValues("spring.ai.ollama.embedding.options.model=all-minilm") - .run(context -> { - var model = "all-minilm"; - OllamaApi ollamaApi = context.getBean(OllamaApi.class); - var modelManager = new OllamaModelManager(ollamaApi); - assertThat(modelManager.isModelAvailable(model)).isTrue(); - - OllamaEmbeddingModel embeddingModel = context.getBean(OllamaEmbeddingModel.class); - EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World")); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - modelManager.deleteModel(model); - }); - } - - @Test - void embeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.ollama.embedding.enabled=false").run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); - }); - - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.ollama.embedding.enabled=true").run(context -> { - assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java deleted file mode 100644 index b29d88f8b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 0.8.0 - */ -public class OllamaEmbeddingAutoConfigurationTests { - - @Test - public void propertiesTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.base-url=TEST_BASE_URL", - "spring.ai.ollama.embedding.options.model=MODEL_XYZ", - "spring.ai.ollama.embedding.options.temperature=0.13", - "spring.ai.ollama.embedding.options.topK=13" - // @formatter:on - ) - .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(OllamaEmbeddingProperties.class); - var connectionProperties = context.getBean(OllamaConnectionProperties.class); - - assertThat(embeddingProperties.getModel()).isEqualTo("MODEL_XYZ"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(embeddingProperties.getOptions().toMap()).containsKeys("temperature"); - assertThat(embeddingProperties.getOptions().toMap().get("temperature")).isEqualTo(0.13); - assertThat(embeddingProperties.getOptions().getTopK()).isEqualTo(13); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java deleted file mode 100644 index 5bb7547d0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.model.ollama.autoconfigure; - -public final class OllamaImage { - - public static final String DEFAULT_IMAGE = "ollama/ollama:0.5.7"; - - private OllamaImage() { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java deleted file mode 100644 index 875a90742..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.model.ollama.autoconfigure.tool; - -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -public class FunctionCallbackInPromptIT extends BaseOllamaIT { - - private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class); - - private static final String MODEL_NAME = "qwen2.5:3b"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.baseUrl=" + getBaseUrl(), - "spring.ai.ollama.chat.options.model=" + MODEL_NAME, - "spring.ai.ollama.chat.options.temperature=0.5", - "spring.ai.ollama.chat.options.topK=10") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); - - @BeforeAll - public static void beforeAll() { - initializeOllama(MODEL_NAME); - } - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - var promptOptions = OllamaOptions.builder() - .functionCallbacks(List.of(FunctionToolCallback - .builder("CurrentWeatherService", new MockWeatherService()) - .description( - "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamingFunctionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - var promptOptions = OllamaOptions.builder() - .functionCallbacks(List.of(FunctionToolCallback - .builder("CurrentWeatherService", new MockWeatherService()) - .description( - "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions)); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java deleted file mode 100644 index 6995eeed4..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.ollama.autoconfigure.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 10; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, - @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java deleted file mode 100644 index 48d32380d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2023-2025 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.ollama.autoconfigure.tool; - -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -public class OllamaFunctionCallbackIT extends BaseOllamaIT { - - private static final Logger logger = LoggerFactory.getLogger(OllamaFunctionCallbackIT.class); - - private static final String MODEL_NAME = "qwen2.5:3b"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.baseUrl=" + getBaseUrl(), - "spring.ai.ollama.chat.options.model=" + MODEL_NAME, - "spring.ai.ollama.chat.options.temperature=0.5", - "spring.ai.ollama.chat.options.topK=10") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @BeforeAll - public static void beforeAll() { - initializeOllama(MODEL_NAME); - } - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: " + response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - logger.info("Response: " + content); - - assertThat(content).contains("30", "10", "15"); - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder().toolNames("WeatherInfo").build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: " + response.getResult().getOutput().getText()); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Configuration - static class Config { - - @Bean - public ToolCallback weatherFunctionInfo() { - - return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) - .description( - "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java deleted file mode 100644 index 947192863..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2023-2025 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.ollama.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; - -import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import org.springframework.ai.tool.ToolCallbacks; -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration tests for function-based tool calling in Ollama. - * - * @author Thomas Vitale - */ -public class OllamaFunctionToolBeanIT extends BaseOllamaIT { - - private static final Logger logger = LoggerFactory.getLogger(OllamaFunctionToolBeanIT.class); - - private static final String MODEL_NAME = "qwen2.5:3b"; - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.ollama.baseUrl=" + getBaseUrl(), - "spring.ai.ollama.chat.options.model=" + MODEL_NAME, - "spring.ai.ollama.chat.options.temperature=0.5", - "spring.ai.ollama.chat.options.topK=10") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @BeforeAll - public static void beforeAll() { - initializeOllama(MODEL_NAME); - } - - @Test - void toolCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - MyTools myTools = context.getBean(MyTools.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OllamaOptions.builder().toolCallbacks(ToolCallbacks.from(myTools)).build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - - } - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), OllamaOptions.builder().toolNames("weatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("weatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.run(context -> { - - OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations."); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder().toolNames("weatherInfo").build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: {}", response.getResult().getOutput().getText()); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - static class MyTools { - - @Tool(description = "Find the weather conditions, and temperatures for a location, like a city or state.") - public String weatherByLocation(String locationName) { - int temperature = 0; - if (locationName.equals("San Francisco")) { - temperature = 30; - } - else if (locationName.equals("Tokyo")) { - temperature = 10; - } - else if (locationName.equals("Paris")) { - temperature = 15; - } - return "The temperature in " + locationName + " is " + temperature + " degrees Celsius."; - } - - } - - @Configuration - static class Config { - - @Bean - @Description("Find the weather conditions, forecasts, and temperatures for a location, like a city or state.") - public Function weatherInfo() { - return new MockWeatherService(); - } - - @Bean - public MyTools myTools() { - return new MyTools(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java deleted file mode 100644 index e384541c0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.metadata.Usage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class OpenAiAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(OpenAiAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)); - - @Test - void chatCall() { - this.contextRunner.run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void chatCallAudioResponse() { - this.contextRunner - .withPropertyValues( - "spring.ai.openai.chat.options.model=" + OpenAiApi.ChatModel.GPT_4_O_AUDIO_PREVIEW.getValue(), - "spring.ai.openai.chat.options.output-modalities=text,audio", - "spring.ai.openai.chat.options.output-audio.voice=ALLOY", - "spring.ai.openai.chat.options.output-audio.format=WAV") - .run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - ChatResponse response = chatModel - .call(new Prompt(new UserMessage("Tell me joke about Spring Framework"))); - assertThat(response).isNotNull(); - logger.info("Response: " + response); - // AudioPlayer.play(response.getResult().getOutput().getMedia().get(0).getDataAsByteArray()); - }); - } - - @Test - void transcribe() { - this.contextRunner.run(context -> { - OpenAiAudioTranscriptionModel transcriptionModel = context.getBean(OpenAiAudioTranscriptionModel.class); - Resource audioFile = new ClassPathResource("/speech/jfk.flac"); - String response = transcriptionModel.call(audioFile); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void speech() { - this.contextRunner.run(context -> { - OpenAiAudioSpeechModel speechModel = context.getBean(OpenAiAudioSpeechModel.class); - byte[] response = speechModel.call("H"); - assertThat(response).isNotNull(); - assertThat(verifyMp3FrameHeader(response)) - .withFailMessage("Expected MP3 frame header to be present in the response, but it was not found.") - .isTrue(); - assertThat(response.length).isNotEqualTo(0); - - logger.debug("Response: " + Arrays.toString(response)); - }); - } - - public boolean verifyMp3FrameHeader(byte[] audioResponse) { - // Check if the response is null or too short to contain a frame header - if (audioResponse == null || audioResponse.length < 2) { - return false; - } - // Check for the MP3 frame header - // 0xFFE0 is the sync word for an MP3 frame (11 bits set to 1 followed by 3 bits - // set to 0) - return (audioResponse[0] & 0xFF) == 0xFF && (audioResponse[1] & 0xE0) == 0xE0; - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void streamingWithTokenUsage() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.stream-usage=true").run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - - Usage[] streamingTokenUsage = new Usage[1]; - String response = responseFlux.collectList().block().stream().map(chatResponse -> { - streamingTokenUsage[0] = chatResponse.getMetadata().getUsage(); - return (chatResponse.getResult() != null) ? chatResponse.getResult().getOutput().getText() : ""; - }).collect(Collectors.joining()); - - assertThat(streamingTokenUsage[0].getPromptTokens()).isGreaterThan(0); - assertThat(streamingTokenUsage[0].getCompletionTokens()).isGreaterThan(0); - assertThat(streamingTokenUsage[0].getTotalTokens()).isGreaterThan(0); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - OpenAiEmbeddingModel embeddingModel = context.getBean(OpenAiEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1536); - }); - } - - @Test - void generateImage() { - this.contextRunner.withPropertyValues("spring.ai.openai.image.options.size=1024x1024").run(context -> { - OpenAiImageModel imageModel = context.getBean(OpenAiImageModel.class); - ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl()); - }); - } - - @Test - void generateImageWithModel() { - // The 256x256 size is supported by dall-e-2, but not by dall-e-3. - this.contextRunner - .withPropertyValues("spring.ai.openai.image.options.model=dall-e-2", - "spring.ai.openai.image.options.size=256x256") - .run(context -> { - OpenAiImageModel imageModel = context.getBean(OpenAiImageModel.class); - ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl()); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java deleted file mode 100644 index bbff01e58..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java +++ /dev/null @@ -1,687 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link OpenAiConnectionProperties}, {@link OpenAiChatProperties} and - * {@link OpenAiEmbeddingProperties}. - * - * @author Christian Tzolov - * @author Thomas Vitale - * @since 0.8.0 - */ -public class OpenAiPropertiesTests { - - @Test - public void chatProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.chat.options.model=MODEL_XYZ", - "spring.ai.openai.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isNull(); - assertThat(chatProperties.getBaseUrl()).isNull(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void transcriptionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ", - "spring.ai.openai.audio.transcription.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(transcriptionProperties.getApiKey()).isNull(); - assertThat(transcriptionProperties.getBaseUrl()).isNull(); - - assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f); - }); - } - - @Test - public void chatOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.chat.base-url=TEST_BASE_URL2", - "spring.ai.openai.chat.api-key=456", - "spring.ai.openai.chat.options.model=MODEL_XYZ", - "spring.ai.openai.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isEqualTo("456"); - assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void transcriptionOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.audio.transcription.base-url=TEST_BASE_URL2", - "spring.ai.openai.audio.transcription.api-key=456", - "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ", - "spring.ai.openai.audio.transcription.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(transcriptionProperties.getApiKey()).isEqualTo("456"); - assertThat(transcriptionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f); - }); - } - - @Test - public void speechProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.audio.speech.options.model=TTS_1", - "spring.ai.openai.audio.speech.options.voice=alloy", - "spring.ai.openai.audio.speech.options.response-format=mp3", - "spring.ai.openai.audio.speech.options.speed=0.75") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(speechProperties.getApiKey()).isNull(); - assertThat(speechProperties.getBaseUrl()).isNull(); - - assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_1"); - assertThat(speechProperties.getOptions().getVoice()) - .isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ALLOY); - assertThat(speechProperties.getOptions().getResponseFormat()) - .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3); - assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.75f); - }); - } - - @Test - public void speechPropertiesTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.audio.speech.options.model=TTS_1", - "spring.ai.openai.audio.speech.options.voice=alloy", - "spring.ai.openai.audio.speech.options.response-format=mp3", - "spring.ai.openai.audio.speech.options.speed=0.75") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_1"); - assertThat(speechProperties.getOptions().getVoice()) - .isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ALLOY); - assertThat(speechProperties.getOptions().getResponseFormat()) - .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3); - assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.75f); - }); - } - - @Test - public void speechOverrideConnectionPropertiesTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.audio.speech.base-url=TEST_BASE_URL2", - "spring.ai.openai.audio.speech.api-key=456", - "spring.ai.openai.audio.speech.options.model=TTS_2", - "spring.ai.openai.audio.speech.options.voice=echo", - "spring.ai.openai.audio.speech.options.response-format=opus", - "spring.ai.openai.audio.speech.options.speed=0.5") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(speechProperties.getApiKey()).isEqualTo("456"); - assertThat(speechProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_2"); - assertThat(speechProperties.getOptions().getVoice()).isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ECHO); - assertThat(speechProperties.getOptions().getResponseFormat()) - .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.OPUS); - assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.5f); - }); - } - - @Test - public void embeddingProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isNull(); - assertThat(embeddingProperties.getBaseUrl()).isNull(); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void embeddingOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.embedding.base-url=TEST_BASE_URL2", - "spring.ai.openai.embedding.api-key=456", - "spring.ai.openai.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isEqualTo("456"); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void imageProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.image.options.model=MODEL_XYZ", - "spring.ai.openai.image.options.n=3") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(OpenAiImageProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isNull(); - assertThat(imageProperties.getBaseUrl()).isNull(); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - }); - } - - @Test - public void imageOverrideConnectionProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.api-key=abc123", - "spring.ai.openai.image.base-url=TEST_BASE_URL2", - "spring.ai.openai.image.api-key=456", - "spring.ai.openai.image.options.model=MODEL_XYZ", - "spring.ai.openai.image.options.n=3") - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(OpenAiImageProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isEqualTo("456"); - assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.chat.options.model=MODEL_XYZ", - "spring.ai.openai.chat.options.frequencyPenalty=-1.5", - "spring.ai.openai.chat.options.logitBias.myTokenId=-5", - "spring.ai.openai.chat.options.maxTokens=123", - "spring.ai.openai.chat.options.n=10", - "spring.ai.openai.chat.options.presencePenalty=0", - "spring.ai.openai.chat.options.seed=66", - "spring.ai.openai.chat.options.stop=boza,koza", - "spring.ai.openai.chat.options.temperature=0.55", - "spring.ai.openai.chat.options.topP=0.56", - - // "spring.ai.openai.chat.options.toolChoice.functionName=toolChoiceFunctionName", - "spring.ai.openai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ToolChoiceBuilder.FUNCTION("toolChoiceFunctionName")), - - "spring.ai.openai.chat.options.tools[0].function.name=myFunction1", - "spring.ai.openai.chat.options.tools[0].function.description=function description", - "spring.ai.openai.chat.options.tools[0].function.jsonSchema=" + """ - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state e.g. San Francisco, CA" - }, - "lat": { - "type": "number", - "description": "The city latitude" - }, - "lon": { - "type": "number", - "description": "The city longitude" - }, - "unit": { - "type": "string", - "enum": ["c", "f"] - } - }, - "required": ["location", "lat", "lon", "unit"] - } - """, - "spring.ai.openai.chat.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); - assertThat(chatProperties.getOptions().getLogitBias().get("myTokenId")).isEqualTo(-5); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getN()).isEqualTo(10); - assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0); - assertThat(chatProperties.getOptions().getSeed()).isEqualTo(66); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - - JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}", - "" + chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT); - - assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ"); - - assertThat(chatProperties.getOptions().getTools()).hasSize(1); - var tool = chatProperties.getOptions().getTools().get(0); - assertThat(tool.getType()).isEqualTo(OpenAiApi.FunctionTool.Type.FUNCTION); - var function = tool.getFunction(); - assertThat(function.getName()).isEqualTo("myFunction1"); - assertThat(function.getDescription()).isEqualTo("function description"); - assertThat(function.getParameters()).isNotEmpty(); - }); - } - - @Test - public void transcriptionOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ", - "spring.ai.openai.audio.transcription.options.language=en", - "spring.ai.openai.audio.transcription.options.prompt=Er, yes, I think so", - "spring.ai.openai.audio.transcription.options.responseFormat=JSON", - "spring.ai.openai.audio.transcription.options.temperature=0.55" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002"); - - assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(transcriptionProperties.getOptions().getLanguage()).isEqualTo("en"); - assertThat(transcriptionProperties.getOptions().getPrompt()).isEqualTo("Er, yes, I think so"); - assertThat(transcriptionProperties.getOptions().getResponseFormat()) - .isEqualTo(OpenAiAudioApi.TranscriptResponseFormat.JSON); - assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.embedding.options.model=MODEL_XYZ", - "spring.ai.openai.embedding.options.encodingFormat=MyEncodingFormat", - "spring.ai.openai.embedding.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat"); - assertThat(embeddingProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - public void imageOptionsTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.image.options.n=3", - "spring.ai.openai.image.options.model=MODEL_XYZ", - "spring.ai.openai.image.options.quality=hd", - "spring.ai.openai.image.options.response_format=url", - "spring.ai.openai.image.options.size=1024x1024", - "spring.ai.openai.image.options.width=1024", - "spring.ai.openai.image.options.height=1024", - "spring.ai.openai.image.options.style=vivid", - "spring.ai.openai.image.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(OpenAiImageProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getQuality()).isEqualTo("hd"); - assertThat(imageProperties.getOptions().getResponseFormat()).isEqualTo("url"); - assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024"); - assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid"); - assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - void embeddingActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); - }); - - } - - @Test - void imageActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.image.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.image.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); - }); - - } - - @Test - void audioSpeechActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.speech.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.speech.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); - }); - - } - - @Test - void audioTranscriptionActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.transcription.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.transcription.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java deleted file mode 100644 index a863c88b7..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java +++ /dev/null @@ -1,357 +0,0 @@ -/* - * 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.model.openai.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.ai.openai.api.ResponseFormat; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link OpenAiChatProperties} #options#responseFormat support. - * - * @author Christian Tzolov - */ -public class OpenAiResponseFormatPropertiesTests { - - @Test - public void responseFormatJsonSchema() { - - String responseFormatJsonSchema = """ - { - "$schema" : "https://json-schema.org/draft/2020-12/schema", - "type" : "object", - "properties" : { - "someString" : { - "type" : "string" - } - }, - "additionalProperties" : false - } - """; - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - - "spring.ai.openai.chat.options.response-format.type=JSON_SCHEMA", - "spring.ai.openai.chat.options.response-format.name=MyName", - "spring.ai.openai.chat.options.response-format.schema=" + responseFormatJsonSchema, - "spring.ai.openai.chat.options.response-format.strict=true" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(chatProperties.getOptions().getResponseFormat()) - .isEqualTo(new ResponseFormat(ResponseFormat.Type.JSON_SCHEMA, responseFormatJsonSchema)); - }); - } - - @Test - public void responseFormatJsonObject() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.chat.options.response-format.type=JSON_OBJECT") - - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - - assertThat(chatProperties.getOptions().getResponseFormat()) - .isEqualTo(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()); - }); - } - - @Test - public void emptyResponseFormat() { - - new ApplicationContextRunner().withPropertyValues("spring.ai.openai.api-key=API_KEY") - - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(OpenAiChatProperties.class); - - assertThat(chatProperties.getOptions().getResponseFormat()).isNull(); - }); - } - - @Test - public void transcriptionOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ", - "spring.ai.openai.audio.transcription.options.language=en", - "spring.ai.openai.audio.transcription.options.prompt=Er, yes, I think so", - "spring.ai.openai.audio.transcription.options.responseFormat=JSON", - "spring.ai.openai.audio.transcription.options.temperature=0.55" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002"); - - assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(transcriptionProperties.getOptions().getLanguage()).isEqualTo("en"); - assertThat(transcriptionProperties.getOptions().getPrompt()).isEqualTo("Er, yes, I think so"); - assertThat(transcriptionProperties.getOptions().getResponseFormat()) - .isEqualTo(OpenAiAudioApi.TranscriptResponseFormat.JSON); - assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.embedding.options.model=MODEL_XYZ", - "spring.ai.openai.embedding.options.encodingFormat=MyEncodingFormat", - "spring.ai.openai.embedding.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat"); - assertThat(embeddingProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - public void imageOptionsTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.openai.api-key=API_KEY", - "spring.ai.openai.base-url=TEST_BASE_URL", - - "spring.ai.openai.image.options.n=3", - "spring.ai.openai.image.options.model=MODEL_XYZ", - "spring.ai.openai.image.options.quality=hd", - "spring.ai.openai.image.options.response_format=url", - "spring.ai.openai.image.options.size=1024x1024", - "spring.ai.openai.image.options.width=1024", - "spring.ai.openai.image.options.height=1024", - "spring.ai.openai.image.options.style=vivid", - "spring.ai.openai.image.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(OpenAiImageProperties.class); - var connectionProperties = context.getBean(OpenAiConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getQuality()).isEqualTo("hd"); - assertThat(imageProperties.getOptions().getResponseFormat()).isEqualTo("url"); - assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024"); - assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid"); - assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - void embeddingActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); - }); - - } - - @Test - void imageActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.image.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.image.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); - }); - - } - - @Test - void audioSpeechActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.speech.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.speech.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); - }); - - } - - @Test - void audioTranscriptionActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.transcription.enabled=false") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", - "spring.ai.openai.audio.transcription.enabled=true") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java deleted file mode 100644 index 8e238be52..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.model.openai.autoconfigure.tool; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -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 org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.api.OpenAiApi.ChatModel; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -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(OpenAiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - ChatClient chatClient = ChatClient.builder(chatModel).build(); - - // @formatter:off - chatClient.prompt() - .user("Tell me a joke?") - .call().content(); - - String content = ChatClient.builder(chatModel).build().prompt() - .user("What's the weather like in San Francisco, Tokyo, and Paris?") - .functions(FunctionToolCallback - .builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build()) - .call().content(); - // @formatter:on - - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - }); - } - - @Test - void lambdaFunctionCallTest() { - Map state = new ConcurrentHashMap<>(); - - record LightInfo(String roomName, boolean isOn) { - } - - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // @formatter:off - String content = ChatClient.builder(chatModel).build().prompt() - .user("Turn the light on in the kitchen and in the living room!") - .functions(FunctionToolCallback - .builder("turnLight", (LightInfo lightInfo) -> { - logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName()); - state.put(lightInfo.roomName(), lightInfo.isOn()); - }) - .description("Turn light on or off in a room") - .inputType(LightInfo.class) - .build()) - .call().content(); - // @formatter:on - logger.info("Response: {}", content); - assertThat(state).containsEntry("kitchen", Boolean.TRUE); - assertThat(state).containsEntry("living room", Boolean.TRUE); - }); - } - - @Test - void functionCallTest2() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // @formatter:off - String content = ChatClient.builder(chatModel).build().prompt() - .user("What's the weather like in Amsterdam?") - .functions(FunctionToolCallback - .builder("CurrentWeatherService", input -> "18 degrees Celsius") - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build()) - .call().content(); - // @formatter:on - logger.info("Response: {}", content); - - assertThat(content).contains("18"); - }); - } - - @Test - void streamingFunctionCallTest() { - - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // @formatter:off - String content = ChatClient.builder(chatModel).build().prompt() - .user("What's the weather like in San Francisco, Tokyo, and Paris?") - .functions(FunctionToolCallback - .builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build()) - .stream().content() - .collectList().block().stream().collect(Collectors.joining()); - // @formatter:on - - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java deleted file mode 100644 index 45596ff5e..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.model.openai.autoconfigure.tool; - -import java.util.List; -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.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi.ChatModel; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class FunctionCallbackInPromptIT { - - 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(OpenAiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), - "spring.ai.openai.chat.options.temperature=0.1") - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris?"); - - var promptOptions = OpenAiChatOptions.builder() - .functionCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamingFunctionCallTest() { - - this.contextRunner - .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(), - "spring.ai.openai.chat.options.temperature=0.5") - .run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris?"); - - var promptOptions = OpenAiChatOptions.builder() - .functionCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions)); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java deleted file mode 100644 index b1ddf745d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2023-2025 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.openai.autoconfigure.tool; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.BeforeEach; -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.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.model.ToolContext; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi.ChatModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -class FunctionCallbackWithPlainFunctionBeanIT { - - private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), - "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - private static Map feedback = new ConcurrentHashMap<>(); - - @BeforeEach - void setUp() { - feedback.clear(); - } - - @Test - void functionCallingVoidInput() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage("Turn the light on in the living room"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().function("turnLivingRoomLightOn").build())); - - logger.info("Response: {}", response); - assertThat(feedback).hasSize(1); - assertThat(feedback.get("turnLivingRoomLightOn")).isEqualTo(Boolean.valueOf(true)); - }); - } - - @Test - void functionCallingSupplier() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage("Turn the light on in the living room"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().function("turnLivingRoomLightOnSupplier").build())); - - logger.info("Response: {}", response); - assertThat(feedback).hasSize(1); - assertThat(feedback.get("turnLivingRoomLightOnSupplier")).isEqualTo(Boolean.valueOf(true)); - }); - } - - @Test - void functionCallingVoidOutput() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room"); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("turnLight").build())); - - logger.info("Response: {}", response); - assertThat(feedback).hasSize(2); - assertThat(feedback.get("kitchen")).isEqualTo(Boolean.valueOf(true)); - assertThat(feedback.get("living room")).isEqualTo(Boolean.valueOf(true)); - }); - } - - @Test - void functionCallingConsumer() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().function("turnLightConsumer").build())); - - logger.info("Response: {}", response); - assertThat(feedback).hasSize(2); - assertThat(feedback.get("kitchen")).isEqualTo(Boolean.valueOf(true)); - assertThat(feedback.get("living room")).isEqualTo(Boolean.valueOf(true)); - - }); - } - - @Test - void trainScheduler() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "Please schedule a train from San Francisco to Los Angeles on 2023-12-25"); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder() - .toolNames("trainReservation") - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: {}", response.getResult().getOutput().getText()); - }); - } - - @Test - void functionCallWithDirectBiFunction() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - ChatClient chatClient = ChatClient.builder(chatModel).build(); - - String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?") - .functions("weatherFunctionWithContext") - .toolContext(Map.of("sessionId", "123")) - .call() - .content(); - logger.info(content); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder() - .function("weatherFunctionWithContext") - .toolContext(Map.of("sessionId", "123")) - .build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithBiFunctionClass() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - ChatClient chatClient = ChatClient.builder(chatModel).build(); - - String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?") - .functions("weatherFunctionWithClassBiFunction") - .toolContext(Map.of("sessionId", "123")) - .call() - .content(); - logger.info(content); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder() - .function("weatherFunctionWithClassBiFunction") - .toolContext(Map.of("sessionId", "123")) - .build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallTest() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); - - ChatResponse response = chatModel.call( - new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - // Test weatherFunctionTwo - response = chatModel.call(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().function("weatherFunctionTwo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder() - .toolNames("weatherFunction") - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: {}", response.getResult().getOutput().getText()); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'"); - - Flux response = chatModel.stream( - new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("weatherFunction").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - - // Test weatherFunctionTwo - response = chatModel.stream(new Prompt(List.of(userMessage), - OpenAiChatOptions.builder().function("weatherFunctionTwo").build())); - - content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .collect(Collectors.joining()); - logger.info("Response: {}", content); - - assertThat(content).isNotEmpty().withFailMessage("Content returned from OpenAI model is empty"); - assertThat(content).contains("30", "10", "15"); - - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public MyBiFunction weatherFunctionWithClassBiFunction() { - return new MyBiFunction(); - } - - @Bean - @Description("Get the weather in location") - public BiFunction weatherFunctionWithContext() { - return (request, context) -> new MockWeatherService().apply(request); - } - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunctionTwo() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - @Bean - @Description("Turn light on or off in a room") - public Function turnLight() { - return (LightInfo lightInfo) -> { - logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName()); - feedback.put(lightInfo.roomName(), lightInfo.isOn()); - return null; - }; - } - - @Bean - @Description("Turn light on or off in a room") - public Consumer turnLightConsumer() { - return (LightInfo lightInfo) -> { - logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName()); - feedback.put(lightInfo.roomName(), lightInfo.isOn()); - }; - } - - @Bean - @Description("Turns light on in the living room") - public Function turnLivingRoomLightOn() { - return (Void v) -> { - logger.info("Turning light on in the living room"); - feedback.put("turnLivingRoomLightOn", Boolean.TRUE); - return "Done"; - }; - } - - @Bean - @Description("Turns light on in the living room") - public Supplier turnLivingRoomLightOnSupplier() { - return () -> { - logger.info("Turning light on in the living room"); - feedback.put("turnLivingRoomLightOnSupplier", Boolean.TRUE); - return "Done"; - }; - } - - @Bean - @Description("Schedule a train reservation") - public Function, TrainSearchResponse> trainReservation() { - return (TrainSearchRequest request) -> { - logger.info("Turning light to [" + request.data().from() + "] in " + request.data().to()); - return new TrainSearchResponse<>( - new TrainSearchScheduleResponse(request.data().from(), request.data().to(), "", "123")); - }; - } - - } - - public static class MyBiFunction - implements BiFunction { - - @Override - public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) { - return new MockWeatherService().apply(request); - } - - } - - record LightInfo(String roomName, boolean isOn) { - } - - record TrainSearchSchedule(String from, String to, String date) { - } - - record TrainSearchScheduleResponse(String from, String to, String date, String trainNumber) { - } - - record TrainSearchRequest(T data) { - } - - record TrainSearchResponse(T data) { - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java deleted file mode 100644 index f3f24a06f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.openai.autoconfigure.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 10; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, - @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java deleted file mode 100644 index 682a8dacb..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.model.openai.autoconfigure.tool; - -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 org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.api.OpenAiApi.ChatModel; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class OpenAiFunctionCallback2IT { - - private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallback2IT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), - "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // @formatter:off - ChatClient chatClient = ChatClient.builder(chatModel) - .defaultFunctions("WeatherInfo") - .defaultUser(u -> u.text("What's the weather like in {cities}?")) - .build(); - - String content = chatClient.prompt() - .user(u -> u.param("cities", "San Francisco, Tokyo, Paris")) - .call().content(); - // @formatter:on - - logger.info("Response: {}", content); - - assertThat(content).contains("30", "10", "15"); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - // @formatter:off - String content = ChatClient.builder(chatModel).build().prompt() - .functions("WeatherInfo") - .user("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); - - assertThat(content).contains("30", "10", "15"); - }); - } - - @Configuration - static class Config { - - @Bean - public ToolCallback weatherFunctionInfo() { - - return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java deleted file mode 100644 index 6ab701eb2..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.model.openai.autoconfigure.tool; - -import java.util.List; -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.model.openai.autoconfigure.OpenAiAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi.ChatModel; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") -public class OpenAiFunctionCallbackIT { - - private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallbackIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"), - "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName()) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?"); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> { - - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'WeatherInfo'"); - - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - }); - } - - @Configuration - static class Config { - - @Bean - public ToolCallback weatherFunctionInfo() { - - return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java deleted file mode 100644 index 9ac0dd861..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.model.postgresml.autoconfigure; - -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.postgresml.PostgresMlEmbeddingModel; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.context.SpringBootTest; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link PostgresMlEmbeddingProperties}. - * - * @author Utkarsh Srivastava - * @author Christian Tzolov - */ -@SpringBootTest(properties = { "spring.ai.postgresml.embedding.options.metadata-mode=all", - "spring.ai.postgresml.embedding.options.kwargs.key1=value1", - "spring.ai.postgresml.embedding.options.kwargs.key2=value2", - "spring.ai.postgresml.embedding.options.transformer=abc123" }) -class PostgresMlEmbeddingPropertiesTests { - - @Autowired - private PostgresMlEmbeddingProperties postgresMlProperties; - - @Test - void postgresMlPropertiesAreCorrect() { - assertThat(this.postgresMlProperties).isNotNull(); - assertThat(this.postgresMlProperties.getOptions().getTransformer()).isEqualTo("abc123"); - assertThat(this.postgresMlProperties.getOptions().getVectorType()) - .isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_ARRAY); - assertThat(this.postgresMlProperties.getOptions().getKwargs()) - .isEqualTo(Map.of("key1", "value1", "key2", "value2")); - assertThat(this.postgresMlProperties.getOptions().getMetadataMode()).isEqualTo(MetadataMode.ALL); - } - - @SpringBootConfiguration - @EnableConfigurationProperties(PostgresMlEmbeddingProperties.class) - static class TestConfiguration { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java deleted file mode 100644 index bc2334e20..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; -import reactor.core.publisher.Flux; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"), - @EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") }) -public class QianFanAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(QianFanAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.apiKey=" + System.getenv("QIANFAN_API_KEY"), - "spring.ai.qianfan.secretKey=" + System.getenv("QIANFAN_SECRET_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - QianFanChatModel client = context.getBean(QianFanChatModel.class); - String response = client.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - QianFanChatModel client = context.getBean(QianFanChatModel.class); - Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); - String response = Objects.requireNonNull(responseFlux.collectList().block()) - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - QianFanEmbeddingModel embeddingClient = context.getBean(QianFanEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingClient - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingClient.dimensions()).isEqualTo(1024); - }); - } - - @Test - void generateImage() { - this.contextRunner.withPropertyValues("spring.ai.qianfan.image.options.size=1024x1024").run(context -> { - QianFanImageModel imageModel = context.getBean(QianFanImageModel.class); - ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNull(); - assertThat(imageResponse.getResult().getOutput().getB64Json()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getB64Json()); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java deleted file mode 100644 index 8d976f971..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java +++ /dev/null @@ -1,438 +0,0 @@ -/* - * 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.model.qianfan.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link QianFanConnectionProperties}, {@link QianFanChatProperties} and - * {@link QianFanEmbeddingProperties}. - * - * @author Geng Rong - */ -public class QianFanPropertiesTests { - - @Test - public void chatProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.chat.options.model=MODEL_XYZ", - "spring.ai.qianfan.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(QianFanChatProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isNull(); - assertThat(chatProperties.getBaseUrl()).isNull(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void chatOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.chat.base-url=TEST_BASE_URL2", - "spring.ai.qianfan.chat.api-key=456", - "spring.ai.qianfan.chat.secret-key=def456", - "spring.ai.qianfan.chat.options.model=MODEL_XYZ", - "spring.ai.qianfan.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(QianFanChatProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isEqualTo("456"); - assertThat(chatProperties.getSecretKey()).isEqualTo("def456"); - assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void embeddingProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isNull(); - assertThat(embeddingProperties.getBaseUrl()).isNull(); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void embeddingOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.embedding.base-url=TEST_BASE_URL2", - "spring.ai.qianfan.embedding.api-key=456", - "spring.ai.qianfan.embedding.secret-key=def456", - "spring.ai.qianfan.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isEqualTo("456"); - assertThat(embeddingProperties.getSecretKey()).isEqualTo("def456"); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.api-key=API_KEY", - "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", - - "spring.ai.qianfan.chat.options.model=MODEL_XYZ", - "spring.ai.qianfan.chat.options.frequencyPenalty=-1.5", - "spring.ai.qianfan.chat.options.logitBias.myTokenId=-5", - "spring.ai.qianfan.chat.options.maxTokens=123", - "spring.ai.qianfan.chat.options.presencePenalty=0", - "spring.ai.qianfan.chat.options.responseFormat.type=json", - "spring.ai.qianfan.chat.options.stop=boza,koza", - "spring.ai.qianfan.chat.options.temperature=0.55", - "spring.ai.qianfan.chat.options.topP=0.56" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(QianFanChatProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("bge_large_zh"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0); - assertThat(chatProperties.getOptions().getResponseFormat()) - .isEqualTo(new QianFanApi.ChatCompletionRequest.ResponseFormat("json")); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.api-key=API_KEY", - "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", - - "spring.ai.qianfan.embedding.options.model=MODEL_XYZ", - "spring.ai.qianfan.embedding.options.encodingFormat=MyEncodingFormat" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - void embeddingActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty(); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty(); - }); - - } - - @Test - public void imageProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.image.options.model=MODEL_XYZ", - "spring.ai.qianfan.image.options.n=3") - // @formatter:on - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(QianFanImageProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isNull(); - assertThat(imageProperties.getBaseUrl()).isNull(); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - }); - } - - @Test - public void imageOverrideConnectionProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.base-url=TEST_BASE_URL", - "spring.ai.qianfan.api-key=abc123", - "spring.ai.qianfan.secret-key=def123", - "spring.ai.qianfan.image.base-url=TEST_BASE_URL2", - "spring.ai.qianfan.image.api-key=456", - "spring.ai.qianfan.image.secret-key=def456", - "spring.ai.qianfan.image.options.model=MODEL_XYZ", - "spring.ai.qianfan.image.options.n=3") - // @formatter:on - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(QianFanImageProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("def123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isEqualTo("456"); - assertThat(imageProperties.getSecretKey()).isEqualTo("def456"); - assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - }); - } - - @Test - public void imageOptionsTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.qianfan.api-key=API_KEY", - "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", - - "spring.ai.qianfan.image.options.n=3", - "spring.ai.qianfan.image.options.model=MODEL_XYZ", - "spring.ai.qianfan.image.options.size=1024x1024", - "spring.ai.qianfan.image.options.width=1024", - "spring.ai.qianfan.image.options.height=1024", - "spring.ai.qianfan.image.options.style=vivid", - "spring.ai.qianfan.image.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(QianFanImageProperties.class); - var connectionProperties = context.getBean(QianFanConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY"); - - assertThat(imageProperties.getOptions().getN()).isEqualTo(3); - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024"); - assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024); - assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid"); - assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - void imageActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=false") - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanImageModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL") - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY", - "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=true") - .withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java deleted file mode 100644 index ad447cb4f..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import org.springframework.ai.image.Image; -import org.springframework.ai.image.ImageGeneration; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.stabilityai.StyleEnum; -import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "STABILITYAI_API_KEY", matches = ".*") -public class StabilityAiAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.stabilityai.image.api-key=" + System.getenv("STABILITYAI_API_KEY")) - .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - ImageModel imageModel = context.getBean(ImageModel.class); - StabilityAiImageOptions imageOptions = StabilityAiImageOptions.builder() - .stylePreset(StyleEnum.PHOTOGRAPHIC) - .build(); - - var instructions = """ - A light cream colored mini golden doodle. - """; - - ImagePrompt imagePrompt = new ImagePrompt(instructions, imageOptions); - ImageResponse imageResponse = imageModel.call(imagePrompt); - - ImageGeneration imageGeneration = imageResponse.getResult(); - Image image = imageGeneration.getOutput(); - - assertThat(image.getB64Json()).isNotEmpty(); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java deleted file mode 100644 index 74e1624f9..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.model.stabilityai.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.stabilityai.StabilityAiImageModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - * @since 0.8.0 - */ -public class StabilityAiImagePropertiesTests { - - @Test - public void chatPropertiesTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT", - "spring.ai.stabilityai.image.options.n=10", - "spring.ai.stabilityai.image.options.model=MODEL_XYZ", - "spring.ai.stabilityai.image.options.width=512", - "spring.ai.stabilityai.image.options.height=256", - "spring.ai.stabilityai.image.options.response-format=application/json", - "spring.ai.stabilityai.image.options.n=4", - "spring.ai.stabilityai.image.options.cfg-scale=7", - "spring.ai.stabilityai.image.options.clip-guidance-preset=SIMPLE", - "spring.ai.stabilityai.image.options.sampler=K_EULER", - "spring.ai.stabilityai.image.options.seed=0", - "spring.ai.stabilityai.image.options.steps=30", - "spring.ai.stabilityai.image.options.style-preset=neon-punk" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(StabilityAiImageProperties.class); - - assertThat(chatProperties.getBaseUrl()).isEqualTo("ENDPOINT"); - assertThat(chatProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - - assertThat(chatProperties.getOptions().getWidth()).isEqualTo(512); - assertThat(chatProperties.getOptions().getHeight()).isEqualTo(256); - assertThat(chatProperties.getOptions().getResponseFormat()).isEqualTo("application/json"); - assertThat(chatProperties.getOptions().getN()).isEqualTo(4); - assertThat(chatProperties.getOptions().getCfgScale()).isEqualTo(7); - assertThat(chatProperties.getOptions().getClipGuidancePreset()).isEqualTo("SIMPLE"); - assertThat(chatProperties.getOptions().getSampler()).isEqualTo("K_EULER"); - assertThat(chatProperties.getOptions().getSeed()).isEqualTo(0); - assertThat(chatProperties.getOptions().getSteps()).isEqualTo(30); - assertThat(chatProperties.getOptions().getStylePreset()).isEqualTo("neon-punk"); - }); - } - - @Test - void stabilityImageActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=false") - .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isEmpty(); - - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT") - .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isNotEmpty(); - - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=true") - .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isNotEmpty(); - - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java deleted file mode 100644 index f67bad99b..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2023-2025 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.tool.autoconfigure; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; - -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.tool.DefaultToolCallingManager; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.tool.StaticToolCallbackProvider; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.ToolCallbackProvider; -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.ai.tool.definition.ToolDefinition; -import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor; -import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.ai.tool.method.MethodToolCallback; -import org.springframework.ai.tool.method.MethodToolCallbackProvider; -import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver; -import org.springframework.ai.tool.resolution.ToolCallbackResolver; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ToolCallingAutoConfiguration}. - * - * @author Thomas Vitale - * @author Christian Tzolov - */ -class ToolCallingAutoConfigurationTests { - - @Test - void beansAreCreated() { - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class)) - .run(context -> { - var toolCallbackResolver = context.getBean(ToolCallbackResolver.class); - assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class); - - var toolExecutionExceptionProcessor = context.getBean(ToolExecutionExceptionProcessor.class); - assertThat(toolExecutionExceptionProcessor).isInstanceOf(DefaultToolExecutionExceptionProcessor.class); - - var toolCallingManager = context.getBean(ToolCallingManager.class); - assertThat(toolCallingManager).isInstanceOf(DefaultToolCallingManager.class); - }); - } - - @Test - void resolveMultipleFuncitonAndToolCallbacks() { - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class)) - .withUserConfiguration(Config.class) - .run(context -> { - var toolCallbackResolver = context.getBean(ToolCallbackResolver.class); - assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class); - - assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull(); - assertThat(toolCallbackResolver.resolve("getForecast").getName()).isEqualTo("getForecast"); - - assertThat(toolCallbackResolver.resolve("getAlert")).isNotNull(); - assertThat(toolCallbackResolver.resolve("getAlert").getName()).isEqualTo("getAlert"); - - assertThat(toolCallbackResolver.resolve("weatherFunction1")).isNotNull(); - assertThat(toolCallbackResolver.resolve("weatherFunction1").getName()).isEqualTo("weatherFunction1"); - - assertThat(toolCallbackResolver.resolve("getCurrentWeather3")).isNotNull(); - assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getName()) - .isEqualTo("getCurrentWeather3"); - - assertThat(toolCallbackResolver.resolve("getCurrentWeather4")).isNotNull(); - assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getName()) - .isEqualTo("getCurrentWeather4"); - - assertThat(toolCallbackResolver.resolve("getCurrentWeather5")).isNotNull(); - assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getName()) - .isEqualTo("getCurrentWeather5"); - }); - } - - static class WeatherService { - - @Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.") - public String getForecast(String location) { - return "30"; - } - - @Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.") - public String getForecast2(String location) { - return "30"; - } - - public String getAlert(String usState) { - return "Alert"; - } - - } - - @Configuration - static class Config { - - // Note: Currently we do not have ToolCallbackResolver implementation that can - // resolve the ToolCallback from the Tool annotation. - // Therefore we need to provide the ToolCallback instances explicitly using the - // ToolCallbacks.from(...) utility method. - @Bean - public ToolCallbackProvider toolCallbacks() { - return MethodToolCallbackProvider.builder().toolObjects(new WeatherService()).build(); - } - - public record Request(String location) { - } - - public record Response(String temperature) { - } - - @Bean - @Description("Get the weather in location. Return temperature in 36°F or 36°C format.") - public Function weatherFunction1() { - return request -> new Response("30"); - } - - @Bean - public FunctionCallback functionCallbacks3() { - return FunctionCallback.builder() - .function("getCurrentWeather3", (Request request) -> "15.0°C") - .description("Gets the weather in location") - .inputType(Request.class) - .build(); - } - - @Bean - public FunctionCallback functionCallbacks4() { - return FunctionCallback.builder() - .function("getCurrentWeather4", (Request request) -> "15.0°C") - .description("Gets the weather in location") - .inputType(Request.class) - .build(); - - } - - @Bean - public ToolCallback toolCallbacks5() { - return FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C") - .description("Gets the weather in location") - .inputType(Request.class) - .build(); - - } - - @Bean - public ToolCallbackProvider blabla() { - return new StaticToolCallbackProvider( - FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C") - .description("Gets the weather in location") - .inputType(Request.class) - .build()); - - } - - @Bean - public ToolCallback toolCallbacks6() { - var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getAlert", String.class); - return MethodToolCallback.builder() - .toolDefinition(ToolDefinition.builder(toolMethod).build()) - .toolMethod(toolMethod) - .toolObject(new WeatherService()) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java deleted file mode 100644 index 50b60c35d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.model.transformers.autoconfigure; - -import java.io.File; -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.transformers.TransformersEmbeddingModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - */ -public class TransformersEmbeddingModelAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(TransformersEmbeddingModelAutoConfiguration.class)); - - @TempDir - File tempDir; - - @Test - public void embedding() { - this.contextRunner.run(context -> { - var properties = context.getBean(TransformersEmbeddingModelProperties.class); - assertThat(properties.getCache().isEnabled()).isTrue(); - assertThat(properties.getCache().getDirectory()).isEqualTo( - new File(System.getProperty("java.io.tmpdir"), "spring-ai-onnx-generative").getAbsolutePath()); - - EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class); - assertThat(embeddingModel).isInstanceOf(TransformersEmbeddingModel.class); - - List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI")); - - assertThat(embeddings.size()).isEqualTo(2); // batch size - assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); // dimensions - // size - }); - } - - @Test - public void remoteOnnxModel() { - // https://huggingface.co/intfloat/e5-small-v2 - this.contextRunner.withPropertyValues( - "spring.ai.embedding.transformer.cache.directory=" + this.tempDir.getAbsolutePath(), - "spring.ai.embedding.transformer.onnx.modelUri=https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx", - "spring.ai.embedding.transformer.tokenizer.uri=https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json") - .run(context -> { - var properties = context.getBean(TransformersEmbeddingModelProperties.class); - assertThat(properties.getOnnx().getModelUri()) - .isEqualTo("https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx"); - assertThat(properties.getTokenizer().getUri()) - .isEqualTo("https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json"); - - assertThat(properties.getCache().isEnabled()).isTrue(); - assertThat(properties.getCache().getDirectory()).isEqualTo(this.tempDir.getAbsolutePath()); - assertThat(this.tempDir.listFiles()).hasSize(2); - - EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class); - assertThat(embeddingModel).isInstanceOf(TransformersEmbeddingModel.class); - - assertThat(embeddingModel.dimensions()).isEqualTo(384); - - List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI")); - - assertThat(embeddings.size()).isEqualTo(2); // batch size - assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); // dimensions - // size - }); - } - - @Test - void embeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.embedding.transformer.enabled=false").run(context -> { - assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.embedding.transformer.enabled=true").run(context -> { - assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isNotEmpty(); - }); - - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java deleted file mode 100644 index c1ed7eaf3..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.embedding; - -import java.io.File; -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.DocumentEmbeddingRequest; -import org.springframework.ai.embedding.EmbeddingOptionsBuilder; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.embedding.EmbeddingResultMetadata; -import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel; -import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - */ -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -public class VertexAiTextEmbeddingModelAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.embedding.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.embedding.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiEmbeddingAutoConfiguration.class)); - - @TempDir - File tempDir; - - @Test - public void textEmbedding() { - this.contextRunner.run(context -> { - var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); - var textEmbeddingProperties = context.getBean(VertexAiTextEmbeddingProperties.class); - - assertThat(conntectionProperties).isNotNull(); - assertThat(textEmbeddingProperties.isEnabled()).isTrue(); - - VertexAiTextEmbeddingModel embeddingModel = context.getBean(VertexAiTextEmbeddingModel.class); - assertThat(embeddingModel).isInstanceOf(VertexAiTextEmbeddingModel.class); - - List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI")); - - assertThat(embeddings.size()).isEqualTo(2); // batch size - assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); - }); - } - - @Test - void textEmbeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=false").run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=true").run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); - }); - - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty(); - }); - - } - - @Test - public void multimodalEmbedding() { - this.contextRunner.run(context -> { - var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class); - var multimodalEmbeddingProperties = context.getBean(VertexAiMultimodalEmbeddingProperties.class); - - assertThat(conntectionProperties).isNotNull(); - assertThat(multimodalEmbeddingProperties.isEnabled()).isTrue(); - - VertexAiMultimodalEmbeddingModel multiModelEmbeddingModel = context - .getBean(VertexAiMultimodalEmbeddingModel.class); - - assertThat(multiModelEmbeddingModel).isNotNull(); - - var document = new Document("Hello World"); - - DocumentEmbeddingRequest embeddingRequest = new DocumentEmbeddingRequest(List.of(document), - EmbeddingOptionsBuilder.builder().build()); - - EmbeddingResponse embeddingResponse = multiModelEmbeddingModel.call(embeddingRequest); - assertThat(embeddingResponse.getResults()).hasSize(1); - assertThat(embeddingResponse.getResults().get(0)).isNotNull(); - assertThat(embeddingResponse.getResults().get(0).getMetadata().getModalityType()) - .isEqualTo(EmbeddingResultMetadata.ModalityType.TEXT); - assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1408); - - assertThat(embeddingResponse.getMetadata().getModel()).isEqualTo("multimodalembedding@001"); - assertThat(embeddingResponse.getMetadata().getUsage().getPromptTokens()).isEqualTo(0); - - assertThat(multiModelEmbeddingModel.dimensions()).isEqualTo(1408); - - }); - } - - @Test - void multimodalEmbeddingActivation() { - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=false").run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isEmpty(); - }); - - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=true").run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); - }); - - this.contextRunner.run(context -> { - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java deleted file mode 100644 index 364c46635..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini.tool; - -import java.util.List; -import java.util.Set; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -class FunctionCallWithFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model=" - // + VertexAiGeminiChatModel.ChatModel.GEMINI_PRO_1_5_PRO.getValue()) - + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue()) - .run(context -> { - - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - - var userMessage = new UserMessage(""" - What's the weather like in San Francisco, Paris and in Tokyo? - Return the temperature in Celsius. - Perform multiple funciton execution if necessary. - """); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - VertexAiGeminiChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel.call(new Prompt(List.of(userMessage), - VertexAiGeminiChatOptions.builder().function("weatherFunction3").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel - .call(new Prompt(List.of(userMessage), VertexAiGeminiChatOptions.builder().build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).doesNotContain("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - - this.contextRunner.withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model=" - // + VertexAiGeminiChatModel.ChatModel.GEMINI_PRO_1_5_PRO.getValue()) - + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue()) - .run(context -> { - - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - - var userMessage = new UserMessage(""" - What's the weather like in San Francisco, Paris and in Tokyo? - Return the temperature in Celsius. - Perform multiple funciton execution if necessary. - """); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - response = chatModel.call(new Prompt(List.of(userMessage), - VertexAiGeminiChatOptions.builder().toolNames(Set.of("weatherFunction3")).build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunction3() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java deleted file mode 100644 index 32cc29327..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -public class FunctionCallWithFunctionWrapperIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionWrapperIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model=" - + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue()) - .run(context -> { - - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - - var userMessage = new UserMessage(""" - What's the weather like in San Francisco, Paris and in Tokyo? - Return the temperature in Celsius. - """); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), - VertexAiGeminiChatOptions.builder().toolName("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Configuration - static class Config { - - @Bean - public ToolCallback weatherFunctionInfo() { - - return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService()) - .description("Get the current weather in a given location") - .inputType(MockWeatherService.Request.class) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java deleted file mode 100644 index 3d9d04a86..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini.tool; - -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.tool.function.FunctionToolCallback; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel; -import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*") -@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*") -public class FunctionCallWithPromptFunctionIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"), - "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION")) - .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner - .withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model=" - + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH_LIGHT.getValue()) - .run(context -> { - - VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class); - - // var systemMessage = new SystemMessage(""" - // Use Multi-turn function calling. - // Answer for all listed locations. - // If the information was not fetched call the function again. Repeat at - // most 3 times. - // """); - var userMessage = new UserMessage(""" - What's the weather like in San Francisco, Paris and in Tokyo? - Return the temperature in Celsius. - """); - - var promptOptions = VertexAiGeminiChatOptions.builder() - .toolCallbacks( - List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - // Verify that no function call is made. - response = chatModel - .call(new Prompt(List.of(userMessage), VertexAiGeminiChatOptions.builder().build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).doesNotContain("30", "10", "15"); - - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java deleted file mode 100644 index af856fdd0..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.model.vertexai.autoconfigure.gemini.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Christian Tzolov - */ -@JsonClassDescription("Get the weather in location") -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java deleted file mode 100644 index 86f6fca45..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.model.watsonxai.autoconfigure; - -import org.junit.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -public class WatsonxAiAutoConfigurationTests { - - @Test - public void propertiesTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.watsonx.ai.base-url=TEST_BASE_URL", - "spring.ai.watsonx.ai.stream-endpoint=ml/v1/text/generation_stream?version=2023-05-29", - "spring.ai.watsonx.ai.text-endpoint=ml/v1/text/generation?version=2023-05-29", - "spring.ai.watsonx.ai.embedding-endpoint=ml/v1/text/embeddings?version=2023-05-29", - "spring.ai.watsonx.ai.projectId=1", - "spring.ai.watsonx.ai.IAMToken=123456") - // @formatter:on - .withConfiguration( - AutoConfigurations.of(RestClientAutoConfiguration.class, WatsonxAiAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(WatsonxAiConnectionProperties.class); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getStreamEndpoint()) - .isEqualTo("ml/v1/text/generation_stream?version=2023-05-29"); - assertThat(connectionProperties.getTextEndpoint()) - .isEqualTo("ml/v1/text/generation?version=2023-05-29"); - assertThat(connectionProperties.getEmbeddingEndpoint()) - .isEqualTo("ml/v1/text/embeddings?version=2023-05-29"); - assertThat(connectionProperties.getProjectId()).isEqualTo("1"); - assertThat(connectionProperties.getIAMToken()).isEqualTo("123456"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java deleted file mode 100644 index d36e0a691..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import java.util.List; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import reactor.core.publisher.Flux; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*") -public class ZhiPuAiAutoConfigurationIT { - - private static final Log logger = LogFactory.getLog(ZhiPuAiAutoConfigurationIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)); - - @Test - void generate() { - this.contextRunner.run(context -> { - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - String response = chatModel.call("Hello"); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void generateStreaming() { - this.contextRunner.run(context -> { - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); - String response = responseFlux.collectList() - .block() - .stream() - .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) - .collect(Collectors.joining()); - - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); - } - - @Test - void embedding() { - this.contextRunner.run(context -> { - ZhiPuAiEmbeddingModel embeddingModel = context.getBean(ZhiPuAiEmbeddingModel.class); - - EmbeddingResponse embeddingResponse = embeddingModel - .embedForResponse(List.of("Hello World", "World is big and salvation is near")); - assertThat(embeddingResponse.getResults()).hasSize(2); - assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0); - assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty(); - assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1); - - assertThat(embeddingModel.dimensions()).isEqualTo(1024); - }); - } - - @Test - void generateImage() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.image.options.size=1024x1024").run(context -> { - ZhiPuAiImageModel ImageModel = context.getBean(ZhiPuAiImageModel.class); - ImageResponse imageResponse = ImageModel.call(new ImagePrompt("forest")); - assertThat(imageResponse.getResults()).hasSize(1); - assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty(); - logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl()); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java deleted file mode 100644 index ec1b9387a..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java +++ /dev/null @@ -1,432 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure; - -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link ZhiPuAiConnectionProperties}, {@link ZhiPuAiChatProperties} and - * {@link ZhiPuAiEmbeddingProperties}. - * - * @author Geng Rong - */ -public class ZhiPuAiPropertiesTests { - - @Test - public void chatProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.chat.options.model=MODEL_XYZ", - "spring.ai.zhipuai.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(ZhiPuAiChatProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isNull(); - assertThat(chatProperties.getBaseUrl()).isNull(); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void chatOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.chat.base-url=TEST_BASE_URL2", - "spring.ai.zhipuai.chat.api-key=456", - "spring.ai.zhipuai.chat.options.model=MODEL_XYZ", - "spring.ai.zhipuai.chat.options.temperature=0.55") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(ZhiPuAiChatProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(chatProperties.getApiKey()).isEqualTo("456"); - assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - }); - } - - @Test - public void embeddingProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isNull(); - assertThat(embeddingProperties.getBaseUrl()).isNull(); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void embeddingOverrideConnectionProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.embedding.base-url=TEST_BASE_URL2", - "spring.ai.zhipuai.embedding.api-key=456", - "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(embeddingProperties.getApiKey()).isEqualTo("456"); - assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void imageProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.image.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(ZhiPuAiImageProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isNull(); - assertThat(imageProperties.getBaseUrl()).isNull(); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void imageOverrideConnectionProperties() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.api-key=abc123", - "spring.ai.zhipuai.image.base-url=TEST_BASE_URL2", - "spring.ai.zhipuai.image.api-key=456", - "spring.ai.zhipuai.image.options.model=MODEL_XYZ") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(ZhiPuAiImageProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getApiKey()).isEqualTo("abc123"); - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - - assertThat(imageProperties.getApiKey()).isEqualTo("456"); - assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2"); - - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void chatOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.api-key=API_KEY", - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - - "spring.ai.zhipuai.chat.options.model=MODEL_XYZ", - "spring.ai.zhipuai.chat.options.maxTokens=123", - "spring.ai.zhipuai.chat.options.stop=boza,koza", - "spring.ai.zhipuai.chat.options.temperature=0.55", - "spring.ai.zhipuai.chat.options.topP=0.56", - "spring.ai.zhipuai.chat.options.requestId=RequestId", - "spring.ai.zhipuai.chat.options.doSample=true", - - // "spring.ai.zhipuai.chat.options.toolChoice.functionName=toolChoiceFunctionName", - "spring.ai.zhipuai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ZhiPuAiApi.ChatCompletionRequest.ToolChoiceBuilder.function("toolChoiceFunctionName")), - - "spring.ai.zhipuai.chat.options.tools[0].function.name=myFunction1", - "spring.ai.zhipuai.chat.options.tools[0].function.description=function description", - "spring.ai.zhipuai.chat.options.tools[0].function.jsonSchema=" + """ - { - "type": "object", - "properties": { - "location": { - "type": "string", - "description": "The city and state e.g. San Francisco, CA" - }, - "lat": { - "type": "number", - "description": "The city latitude" - }, - "lon": { - "type": "number", - "description": "The city longitude" - }, - "unit": { - "type": "string", - "enum": ["c", "f"] - } - }, - "required": ["location", "lat", "lon", "unit"] - } - """, - "spring.ai.zhipuai.chat.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var chatProperties = context.getBean(ZhiPuAiChatProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("Embedding-2"); - - assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123); - assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza"); - assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); - assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); - assertThat(chatProperties.getOptions().getRequestId()).isEqualTo("RequestId"); - assertThat(chatProperties.getOptions().getDoSample()).isEqualTo(Boolean.TRUE); - - JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}", - chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT); - - assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ"); - - assertThat(chatProperties.getOptions().getTools()).hasSize(1); - var tool = chatProperties.getOptions().getTools().get(0); - assertThat(tool.getType()).isEqualTo(ZhiPuAiApi.FunctionTool.Type.FUNCTION); - var function = tool.getFunction(); - assertThat(function.getName()).isEqualTo("myFunction1"); - assertThat(function.getDescription()).isEqualTo("function description"); - assertThat(function.getParameters()).isNotEmpty(); - }); - } - - @Test - public void embeddingOptionsTest() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.api-key=API_KEY", - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - - "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ", - "spring.ai.zhipuai.embedding.options.encodingFormat=MyEncodingFormat", - "spring.ai.zhipuai.embedding.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - - assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - }); - } - - @Test - public void imageOptionsTest() { - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.zhipuai.api-key=API_KEY", - "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.image.options.model=MODEL_XYZ", - "spring.ai.zhipuai.image.options.user=userXYZ" - ) - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - var imageProperties = context.getBean(ZhiPuAiImageProperties.class); - var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); - - assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY"); - assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); - assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ"); - }); - } - - @Test - void embeddingActivation() { - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.embedding.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.embedding.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty(); - }); - } - - @Test - void chatActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.chat.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.chat.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty(); - }); - - } - - @Test - void imageActivation() { - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.image.enabled=false") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty(); - }); - - new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.image.enabled=true") - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .run(context -> { - assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty(); - assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty(); - }); - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java deleted file mode 100644 index 12dc83d6d..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure.tool; - -import java.util.List; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*") -public class FunctionCallbackInPromptIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - var promptOptions = ZhiPuAiChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - // .responseConverter(response -> "" + response.temp() + - // response.unit()) - .build())) - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions)); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - }); - } - - @Test - void streamingFunctionCallTest() { - - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - var promptOptions = ZhiPuAiChatOptions.builder() - .functionCallbacks(List.of(FunctionCallback.builder() - .function("CurrentWeatherService", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - .build())) - .build(); - - Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions)); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java deleted file mode 100644 index df21b42ec..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure.tool; - -import java.util.List; -import java.util.function.Function; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.tool.ToolCallingChatOptions; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Description; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*") -class FunctionCallbackWithPlainFunctionBeanIT { - - private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ChatResponse response = chatModel.call( - new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("weatherFunction").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - // Test weatherFunctionTwo - response = chatModel.call(new Prompt(List.of(userMessage), - ZhiPuAiChatOptions.builder().function("weatherFunctionTwo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void functionCallWithPortableFunctionCallingOptions() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder() - .toolNames("weatherFunction") - .build(); - - ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions)); - - logger.info("Response: {}", response); - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - // Test weatherFunction - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - Flux response = chatModel.stream( - new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("weatherFunction").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - // Test weatherFunctionTwo - response = chatModel.stream(new Prompt(List.of(userMessage), - ZhiPuAiChatOptions.builder().function("weatherFunctionTwo").build())); - - content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - }); - } - - @Configuration - static class Config { - - @Bean - @Description("Get the weather in location") - public Function weatherFunction() { - return new MockWeatherService(); - } - - // Relies on the Request's JsonClassDescription annotation to provide the - // function description. - @Bean - public Function weatherFunctionTwo() { - MockWeatherService weatherService = new MockWeatherService(); - return (weatherService::apply); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java deleted file mode 100644 index 9d3e97312..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure.tool; - -import java.util.function.Function; - -import com.fasterxml.jackson.annotation.JsonClassDescription; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonPropertyDescription; - -/** - * Mock 3rd party weather service. - * - * @author Geng Rong - */ -public class MockWeatherService implements Function { - - @Override - public Response apply(Request request) { - - double temperature = 0; - if (request.location().contains("Paris")) { - temperature = 15; - } - else if (request.location().contains("Tokyo")) { - temperature = 10; - } - else if (request.location().contains("San Francisco")) { - temperature = 30; - } - - return new Response(temperature, 15, 20, 2, 53, 45, Unit.C); - } - - /** - * Temperature units. - */ - public enum Unit { - - /** - * Celsius. - */ - C("metric"), - /** - * Fahrenheit. - */ - F("imperial"); - - /** - * Human readable unit name. - */ - public final String unitName; - - Unit(String text) { - this.unitName = text; - } - - } - - /** - * Weather Function request. - */ - @JsonInclude(Include.NON_NULL) - @JsonClassDescription("Weather API request") - public record Request(@JsonProperty(required = true, - value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location, - @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat, - @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon, - @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) { - - } - - /** - * Weather Function response. - */ - public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity, - Unit unit) { - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java deleted file mode 100644 index b7e636592..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.model.zhipuai.autoconfigure.tool; - -import java.util.List; -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.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Geng Rong - */ -@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*") -public class ZhipuAiFunctionCallbackIT { - - private final Logger logger = LoggerFactory.getLogger(ZhipuAiFunctionCallbackIT.class); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY")) - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, - RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) - .withUserConfiguration(Config.class); - - @Test - void functionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - ChatResponse response = chatModel - .call(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build())); - - logger.info("Response: {}", response); - - assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15"); - - }); - } - - @Test - void streamFunctionCallTest() { - this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> { - - ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class); - - UserMessage userMessage = new UserMessage( - "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius."); - - Flux response = chatModel - .stream(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build())); - - String content = response.collectList() - .block() - .stream() - .map(ChatResponse::getResults) - .flatMap(List::stream) - .map(Generation::getOutput) - .map(AssistantMessage::getText) - .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"); - - }); - } - - @Configuration - static class Config { - - @Bean - public FunctionCallback weatherFunctionInfo() { - - return FunctionCallback.builder() - .function("WeatherInfo", new MockWeatherService()) - .description("Get the weather in location") - .inputType(MockWeatherService.Request.class) - // .responseConverter(response -> "" + response.temp() + response.unit()) - .build(); - } - - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java deleted file mode 100644 index 4b712f050..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.retry.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Christian Tzolov - */ -public class SpringAiRetryAutoConfigurationIT { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class)); - - @Test - void testRetryAutoConfiguration() { - this.contextRunner.run(context -> { - assertThat(context).hasSingleBean(RetryTemplate.class); - assertThat(context).hasSingleBean(ResponseErrorHandler.class); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java deleted file mode 100644 index 64cefc6c2..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.retry.autoconfigure; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit Tests for {@link SpringAiRetryProperties}. - * - * @author Christian Tzolov - */ -public class SpringAiRetryPropertiesTests { - - @Test - public void retryDefaultProperties() { - - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class)) - .run(context -> { - var retryProperties = context.getBean(SpringAiRetryProperties.class); - - assertThat(retryProperties.getMaxAttempts()).isEqualTo(10); - // do not retry on 4xx errors - assertThat(retryProperties.isOnClientErrors()).isFalse(); - assertThat(retryProperties.getExcludeOnHttpCodes()).isEmpty(); - assertThat(retryProperties.getOnHttpCodes()).isEmpty(); - assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(2000); - assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(5); - assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(3 * 60000); - }); - } - - @Test - public void retryCustomProperties() { - - new ApplicationContextRunner().withPropertyValues( - // @formatter:off - "spring.ai.retry.max-attempts=100", - "spring.ai.retry.on-client-errors=false", - "spring.ai.retry.exclude-on-http-codes=404,500", - "spring.ai.retry.on-http-codes=429", - "spring.ai.retry.backoff.initial-interval=1000", - "spring.ai.retry.backoff.multiplier=2", - "spring.ai.retry.backoff.max-interval=60000") - // @formatter:on - .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class)) - .run(context -> { - var retryProperties = context.getBean(SpringAiRetryProperties.class); - - assertThat(retryProperties.getMaxAttempts()).isEqualTo(100); - assertThat(retryProperties.isOnClientErrors()).isFalse(); - assertThat(retryProperties.getExcludeOnHttpCodes()).containsExactly(404, 500); - assertThat(retryProperties.getOnHttpCodes()).containsExactly(429); - assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(1000); - assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(2); - assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(60000); - }); - } - -} diff --git a/spring-ai-spring-boot-autoconfigure/src/test/resources/application-test.properties b/spring-ai-spring-boot-autoconfigure/src/test/resources/application-test.properties deleted file mode 100644 index 9107b9e40..000000000 --- a/spring-ai-spring-boot-autoconfigure/src/test/resources/application-test.properties +++ /dev/null @@ -1,10 +0,0 @@ -# Test MCP STDIO client configuration -spring.ai.mcp.client.stdio.enabled=true -spring.ai.mcp.client.stdio.version=test-version -spring.ai.mcp.client.stdio.request-timeout=15s -spring.ai.mcp.client.stdio.root-change-notification=false - -# Test server configuration -spring.ai.mcp.client.stdio.stdio-connections.test-server.command=echo -spring.ai.mcp.client.stdio.stdio-connections.test-server.args[0]=test -spring.ai.mcp.client.stdio.stdio-connections.test-server.env.TEST_ENV=test-value diff --git a/spring-ai-spring-boot-autoconfigure/src/test/resources/speech/jfk.flac b/spring-ai-spring-boot-autoconfigure/src/test/resources/speech/jfk.flac deleted file mode 100644 index e44b7c13897eae7f78beb220c61fe77429a3961d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1152693 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 z zqVDv|!iS+FB$9D*Az(w(`8}V9(kFD2EUuISa)q+;3}|WnVa=qLp4AwBZvvDHr(Z>zI4NkmHm^K zT2{Z{52P)uW~M)MP-o1$WEmYu{8jw+Uw=Q~V0x!95+#k`#i?xBXW>&@E73C+byFAB z&dC@3TSg#s`*+i@PMRg?i(0bHl_a@@81jdeXx8&uWy>iVChvt!=FO)+i|leNC|5(0 z={Yz;z-T9!a8vH^a++S3u_)ylRj)QvCYpmP#Pv^ivk4wVxB4vi&Dk+W1FMRF6F;AE zV64o|A5*`fsOwl8yREC!xpcH6$4R`JaJMmoe`H>rT7gdT^BgCfe2y{_Vw8Zs6*t?mg#uQ<4&{f@%YIH1ya<B0Sf5<18)&#&YwraA{l2^$ke;2rBec7b!6U4apg9rAah0T;oh22eqeJUL9mJ}KvxqTbM@kibD zLdwF{8*JJ0nwNF3j$&2BU{5k=F3+V^)d6r$#xmM4fq=o!FmdQjqIK*@K*|xbC~( zHm-ZCq4FrP>rJszC+qbDp(4FY1FEA7Bh%4`xItjE+`}j?!6z(>IdygenQ!2=k9MRS zwAD%s%D4BMXRZk(>ndX+VS3w0kyom#@xw{=q@$C;e(wQ02PC1uLnYz#H#8XF2m)`A zX!r=<5JY#U*9sNm&!GutHYG-9OQ+HxhE$8OQhM>Id=T&gZ=F6UmF`~1usA(gSv_dr z>I-xa#ND}TMb+^yN5D%&ij4Y%s7(FzWS!B`(%eqN(`r_^kd<@r-(; z+Lw(mhzlAI9h}`8TtgS%J;6M8r~K_&e~3%e`Y;h-K+-ie1+OY0QZyhHoJsgZ|9y>} z4hbS&nx~x42;CALBbjN=q3tzJC7Os)nu<2MDwYAPC^1OPFZr$3H3XkMP_?s+L)F$ zge$2F_c3NzlBu?6z5mR@)>yRl!x5B(kA^5tU5xFdV)3nWFl@xRFl)eGlH7xQ%E*fA z2j`dauN z?e6)%x{a2GRR1n`LvPwsO^0=7X-k%1| zw(vR@x5e@rfp2|_CVaBy6>~HKL*vqc$Y0GxX&$&udhM9I8LJ_^jf9B^RQ(oaAgFPoGM9Pf%cCdR&ZfUdKug*j zvk`Y-BS#wJE3Lk6QNyEj`+WJrIlOd{#&j0mk5w4k0`d3!?=(aTWAU*e9zuhP5keqj z!*wJzJTK1=$_~TicH;dE<=JodCZ`BEyHIJt0vV4`2E6H&b)_%;Nu1o+i4*?tFU2V@ zK1?UNCcD}bZT=0UvVjBSH;D^J(qcQK`|L6tXg{6M&qhxZYN>xhZxf_Q!|)F*#{P07 zyc%Kuz{RN{VS3Vx_|?rh{jbWS4AlKW%`hj$b-d>LEt89+CBEzgz9ehS+)11W@e;6$ z3VPs_Jo}upF$w<{0i%B$$r3$i^L}fAX-Iyw2i)87#Yku*HR*^E-g>4fOofAD3sJ9C z>A?mK&rUGzzc%Wg5kV@U^ZSx`PCjhZ`bs&+^=w4(y~a;wvg(cLaQgVpF-GT6mTXNc z;*zn={6hANAnaWddW2Mo`I{E}W0dnD2^)>_nT0G@9z5-#Iuxq!tu@*KJEXK&hO5Md z7AOa;>I2%p;C-!&BRuMbw!ly@oE^*>(Up)Xiflc?7+aK+pun!BmU;gYP zeekX6)GIgAL1ontPMFvuRfkS&;Zum0WdwtZSw?sDY|dk&TaK(s4gzl;*-2^3*LpNG zuzh!Bi)epS{P*bNX|#+TP8ugk&hK`OpTu-+zK8O)N2_6Do{P!hsfFS)hke?o^EL&s z6o0;AG#lXVjq~;IZ`dy-+J0}}mJ(#={uTx7IO zg6lz7mUVgIx@jRQsS?0U`VjC9E&k?~b;;L9-fNC@yclQi}W4Pu^5leMrMp@ze? zxYen2lFA>DgTzjRb&a<$mH3HP$nAaO`*s?Oy^=X}517SE63j$&g{sRBFJ|-?(!oqC zBmQjYd2M>hhPGM=3FF#a5f+_Q15Qb1|8UL1AnWafUw{@KDNR4AUx~q=Jl#R#`0JM9 z)=4Mm>JtqXneY2_)41Ow#*C+fyF6m7!j+EFHZ9`6=aTQh(c2D@Upn7#Pq)zYcEW+r z*Nja|kcp5E;H0Inm5=t9>Q4`e?E!cSI!)AYPG&+cUl4?{E=eQ#=g+#hcqyz>3xAqm zc#|Stz-TrSrSyE-sa$0|3hxdM1Vb-)?IZP&C6bsePDhFKNjn>hA{B34qycGreE&#u zaezUdqKFmKKD8FxsC79M%~NCZ4J0l3M)hc`r^hTxTKn77ug4ra7b*+$-GT~eH|L&?;ar=mJ zEkAmHrk+Man74t`x8w{HSpOI>VSHht#MRu#`Rb)R{*>2_G{fg-%yA}jS(hTcWyxe0 z;3rV&)??dv&4|;EAOkb4{I5+}k;m;2q8UJb6g#G7+|8z}-_P&!U2MslQ}GLgW+x&u z+iHAMV1wdbP`B@eV2oPMnjxg0>%VYMULex6a6U2t$1+`@f!6yZA2WZUF`ZsFA6rk>J z#*YPZ|KZKAt|;e!XPf(1J*HriVJ?H8x!L;zz%gCWJ9p7|-=8yG3Y#T&x$}{R<51dR zLtbR!VUGWxr_(!RjF0F8@3D;4^m@J?gScI6-11+5ni^|HzlR`S^oI$AS!`l+L%){{ z;_7V{{nYXsm7cQR>%xg+>$_ThCi|~d#tuc=Z{|dcg&O41!e^b>H=N>QbdxH5h3_^% zZF$9scAxPtKIe}-&OL)rCDT*QJYs>1o8*$VlFW1Px*M3d1YoUrICeN^KzH9Tkz54M zuA{!coy@0TZSAhw7RY-7ybKPS-3S#Ccv4csr`hbTe%}|2P{Cha&imnD4YJ)9yQZcC zBM&wCRs`qR$;FjK3MP3g>7?9wNTtP1rl+!7xt_-0gV!GF5$w4&^%Y;A^YxTaR6q}3 z)3H?JzvnoEbrcp}N?xivp`y7(*>$p^CWFq?lB)nP96FMYW_C+X>p=UDAfI$eNrjl( z(AreSY7o8pdsdi(cByRi0+9dnyNgv`EjOKmg=pvogP8hf*hCsX(`?{PRf*y1C!iR^ zs5K*Fzjt$A$x>*k++OSR4dX*Z1Z$9R&g{v;oZG&_9;v^ZfE~EHF$>PJ$agmgWb5uF z&d)T<(y96*r+nI} zfxj#l@4SrMfQ{qt4CIdQ_uDLzNoKb=EqrF9i3wIXAB7%6(`zxrn5jq7@DM7(x9!F`OCE}2LL7&) z91k44>x%|wBjF8kJi6i1<+BnvV&qa?2vxzYKQBu$cJWM=?;HhK@UMhoK&@JjrZTg* z{`hD%E4h?uzM_1>x{-$o6QLo1SYjH;1(M3P2#v{SB)MHulyt%q%boM8$pC!_n0iRhfpv&gYk000y?8kaes9#XR1!uQpZYU_a=GTB2kY4NA-&&ZQI!A0h-JJ)3qHzJKWyP)@hW4=zh;#uWY6G7p zT$E)CyY@|FOOqr!ZG`NcPP9e)Pw zc#cP>wqCGc)%o$-9vKh(i6+l@Z}Yy75Q!YhsY7ik!Nk)X$x^yhPybe;E-Navm{w~~ z-_1KB9PQzSIe<~G(kUj?Y}!j;JvX9>(o+QGasBnzfY!jk$9k>GMw(O(?4DP$Gv>+- z4KD)C!&~m(K*@kD)bzQ10{s<3cHF&+zeS)n_z~!mkm2?D^nK2fyK3eNoN$;I8(ywB z$Ev9^j42k7$V2U<-b_?lc0`a7iYMsOiS*9BYAa65=4?f;-1RcPYk*LNo@CE3n!lA1S~pg*uVJI8(}8T+;C$_%j&=DInLkG5C+~N?6_qMQF-d(Ka!Y z$9=Ok7Clya|B%XT$hr=KxK`d&BP2sL9>%w2QAJf`D%>4;E_JQL!hsZ^c;Ii13Yf9NVlz=N=k-}1#P zydm@=_V6ZQ=!q=q?5Y}~Uw3J3MkYG;mcPxJxngRVF#rCXE{~mF@Uev%`xiO4d>0sy z!;5F7C~EWrzj|7~qRKzv| zm1-E8URki<0?)fiGro$whjL)8( zec*YH*TD+@X(9)$LEb4oB1sy=Lt6-5T;WL@oYWpN*ExnHA8IgmNd;Ls{LcDZBF1Oi z|2pBBmU$?5{X6s40C7|A2p`dw_tQj{)%%t~( zo*<#6Cu)|iH%G{jLMoF6&dm5Pv%i33eZBGr->jU?W4z{2{>yjeLZAgs9;oqMU*Rg= zyX*rJgfoE+rCI2?vF8s%-Y!8_+*0qlyPtgVSPG``0huDrSQsK|*AVl89O~%lCy&3# zY7_~)9V7|!X83bBxVqPiv$?2lc@8W2slh}b4>e^n5rx0{gx+jDLMu4U?}|}Ne2wG1 z#Pf+@FvH;=ib}x~<-sgFk6Z`Q`!3;l81JxY=6hLwObj$;3@b$ zP}eG}x>-p3b)F4I60*-}eIv~-I?lV97ADm^v@JUt==B;MWuoJ4SMiIdHoRkL;5h2| z{ja7{o}p7q#bR_t1RzrbgshLN2Ehdp{wZ<@QT?LzM=gzwS&`W>+~_UIGOdS{6BEs4 zf`ci}Y)?R4BltCHa&ur1|CzM~PTrVk^LYvE&aX_RmXp{pQ6XcLYiJdJFHiE-q1DAn zC!?*cHV6<+fFo1Q9o}0Gb|+8cZ#OT@nfG*Kr#9%;iobiY7{JQ%9QI(J@&=R|*%Kfw zH_>BN)E08e7e>*b4RIEJ+dS1e{dZEUp@v>Iy_eNGcu|~(xJZ)C(MYwhH#p$S&n(^l zd|JCPlTfrfcDQ`pV*8li{|BD(%O&19Uni|>c?O}siha^eX|mN>mU*~PoC3$rcx=?+ufYkzUD;# zNZ(D{(H8!-pm~lW$Ri_zViO3^vzsV)yt0@6p0;!U>(ftKJbM3LfVlgW^b#H|z3~thL&Zv)dxu!ap0cLMz zd?Y-lDs)9$9BnUCQGFy`Y+?Hd-;|SH#XKGJwDAk~n8?NNGJM2;4w3MA@$u*VFl9y# z9wE^kvi&rBwT&kcz}m$%rXIGbnxkv7w5CHGC&;r~Loc9T=j%4?c?OeV)j_LNZ=%hX zICm=v-Ketfj@>Zqy!}tie zLQIIKGwGM8aC`mtG=CB>-6RgtV(GeEnWT3!9tnNe(rNy`rI`A?S$kJw99QNn^$W6a zOETQB!iDF8``rsT8a&;&HkdUqA%&DwPu5tINphbrW2Jgc>;52X)o7`8NW>Qsa{yGo z!%INfl)_?aIbwe30E6?;RhYPBC-UzL!4y+eG}3wfW|Q8v+i?XRt?@qmmn7t^F?7hN z4Vv5{#N1k0a~$Xm7sZZ3iE~)LU=N*5ot}tJ!8{Q94_rC6n~Oh${-QXScl>0OiB3HZ zXrPI=wXhINChMIu+zZY*yb7F~COt# zLy43M>+vT&?ijq=cWc7!mtV+(x;JKTj%w0Z-v3*8_)2L^2UZvTj{f-NM89;%vQxMF(ej?o$sz`3|E`CzAB~e% zFqXAv)dZ6o<(gk2Ph;D(O3?Bo8%N_paTilEeqO1lOTBj2F&pZq&@y1mT|iDLHnNDv zk*seKTvh(n{X(z2RBTv0OWlXAg8qDd3 zxU(s*Q+TY0CbDao^6*k`c5rJ|w0NBV`#398TC!Q&i_46zD#xgPXOfneXfYnlhG`v9 zX}~hT5rLqS&1!bYM-~Feh%T~dR-1H3FBwzPP$W9jv5y&KdEA%U&KJ#q65?m9sNDVu zi&Zgg)foOWL4I#>=VBuI5GQTJpn~ zK#n!Y>wi#7i}Cq>*`X=roZpT=J39_FsRNy92PD;E?v`N_^z8_4u zEfZWGRS%6j<9gEkx9E+#{6YrB-bSdmHD5^RoxqZI69d22p-ks&gl=wneJE)e6O*;i zJLWmFr;24gcIp1Y_LlA^^MytQmrieU_ejbh#Jo`PxD}aiSU{5Z#Xw znY8F1zBV-dtfEzAp?JZ}Da%Xjx%S(lnf-U2&|KppEaFzEYM|d;2gJjw!;%gWrcPHH))` z>2OTu6VN!7=1k^O3)w1USvH8-&5SdbJrUd1l@tdB54Zmk+TQEf?>sl1!de*liu78wn5uj6}H77LoO3v~*`5Q@{&+}3 zjI7kNNNBg(lSo!>4CP@w(2kfoknn$30>p)<7HwcP5&`~)pUj8LSqthE#EdOiq{w*0 zoaeV~FkOR}_WIb|s`aI^s92%WZ}A=#E+6;*=(9XMbylYB35-75s5O@TXAn(Os(Ooe z`A>DjFTRFGgn}RNZduO;u+AJ++MyB%*-QG9nnjX*1UBs95Zhq`AWA|F?{c~e}9O}U@(s6 zA;Z0v$KA_DTa&2bDsgZn$|PgcbPP|jzVdVC(UspqJ3c#L`CC}RR1*~}jO#NR@P4?l zl!EM9>J8peQ3^-Ws|4^96v;gMYFyOCE5#dVo z%|2vDI`y!8W05o<1FvwXWau%R)WVrjqkMMedOcD|GAX$fJrxYy$l$&L8Cf5={#j*9 zH%wXe5m}XbK6Fy~VM#t`k(eaB7h(FU%ZWaP$NiNWPalTy4aoTd#VmN&w-wG{SK0-A63& z-+X=0Ypr7~j-Rgl<|1uatkwUSFt~A&Y@!zH8Xz^_Xo(EJww}F`K|kbi#4{q>i*8eQ zBRK9wGE?H~YrA!D1)}YL9c8le=w z_+W;A?M5NF8<4=X^i=zcAO*8j?Dh`RwTI_u{=s@9t%#@Jh}&%3taGbsusXbMWM}NF5&qFB^PdQZ=XP z5wzG)tp3;kTSUW9AC9p+uJ3JMSdS+?vfV~xB&PCg6YJtpPt%>r715C>qYYPmW?(@w zx2l-!KJxe>p%ulKyI;?@5}yX|El^yC4mbjc{N}rdh5qxKFSe>w|B#|c!FpV0)8(YK z5hRZ;Sd~Cp>9uFMm8m;Imju(fDXDdZ$3{M6%I#&es^Nz9sa1<>{LT5eE2yH6^^IRa zwm+V&CzJNmgZ+w%xAO|Gl=p>I)qk+X)4v(rbvM?c#`J(I2>&uP&!Xa&;H4V_OgTiW z6X0ewq?T^!EX2yDiPG2JYx~Pf8?Rshb1IG%q!{e=t(IC0Nt;$-4H%(UCiSS!zoYYE z$~nZL2gonE^^i)wsA({!nZo~Y?r}#)c>P%(&F!h9?*aR_&&$%>c)KQ=S;x_Ocw_3; z4YjL^g=AJsKmOsd0$>SxhPqVKa4c9*A=@GQ`47V!;}cP_Gs^lI_lX3hKAuvYEr{%Z ziI;~GLkMi6{ioy55o3U2OkDnE=O#%OamlkNa%Akk^AGbnutrav53>BC8wWfENzw#i zN_sJDjTKqL3g6IT+f|)T4r%`ElBWH)^|k*w`IEsIiFUB@`e1xrZOKyHAcfV|K?2Ub z!7ZbgDf!A8>KWk)V{nX&G&JnS^tAW`gDR_p{NAIY1Wg>cLa<)5mSr=JNF05fG)@Jg ztx7>+mXa_;@U^*$pK^2XRu?K4gm(h3GWWrq9Mdb6uGTGm+)KUmV213C7&A+`JxM^K z?KjaDq1?JTVSAm$wVr}>QFoDRCq}Q#_i7g6PJ#}_?&{3OZw{vbKPFO|+@ISr z^T{}$Bhd+m%%X7g31z-uyCfX@t_5x3F69^fY0Y9_wH~Ka&y<{{yDGC8>M`PUEN^CT zn!iT=t5^ie8fHfS>tUd912zd>v!UJRjIH7R{3@aRhn#`ja5!|y{S2Yrj=738&|fzY zNxA!SxLsCZk)BUYY0^;<56G{Mp*a5YahmcEF#44OBVPxu#6GJjE!j;PW5(-EWWAzYQ)Sd=?rXiH|Xu zY23;;w5w^(jw{M$bQHLoh5^a*=&%}2+{+0Ti>*KnHJ$i@lrz7Ua8`8mFQ=EsMIg0 zbG~2J(J!p&Tn;#nGqBCdygkN6?~)Y}3?tQIx5A1v9o|tAs|RS?nvqy*Qmb|Iu?e_X zLG0M^IcbtE?Y7ZMV{*4=-sR9~o$A`dfV*hD>@1*H;ZT-rYAH%hZes~6x+A4H z=57rPu#I0*PLNiFuCj3?y8wUG^bavra5RwR*%p|XGAaH6u7g&VO71C2|7~T8ddW=1 z^Xp^Yk1ifj-7swXRCq<{d>ZZ~GE3wIv*CaG9DN({3SI- z0Eu*Qopqc`c^J<^6LX0$7UWP6iaxW6WV=NG8U=XYPG&MP0K*X zoj~WN9l6td5Em%76hZcY`JDe@afF%9!7<9|6?O=leuRLStr2q2@f&eaKNIQ2WEryB zTs!A{*69+al-RIXNYQRF;JdH=M=r(3$GJ}q~+R`$tT$r8p3Un&NgatzEEq%Z|WMx;Cr2AUik27Z&Wf{vS_uw|8i z>pd9ldA_#Jl35wEC1R2#p@svC%-3gIpHn!vJzP68HZ(qr*;}=km~?xl+>Z;Oa+=;5 zM6#fuYE~L6?4(R;-T^urLBCKhD<*1bM%B70G)Q-H=)tOtogk1)1Sj+;k7d0k5c9rk z@B=fMKKn7Lv(RiTXAGpPx?Hbk96%d%8$%=9<&*NX-b3ez%0Z_Oq(7b7jm{1ZAiB>z zmqwV>RKY~$+^8$Yj8KbT)=@U?rdskT;0&j+2G_JOjHlQWQp2z?L^T+32MXIJRs7Zn zRt!>+ek3#Gj^cYIEqQ;kC-rJ9%J6Eh^n#>!5C5K-Q2Px*vOe%=r0S8FC&cK?uLEPz z>$;TTj$r*OACmESlH^5DA;osLv4Sv0&FavzRjQD?AED23}c|oW{q0(8O9#TJi{n$MZ`v_}ZRarF~ z^`Pmi5}4`WOV(rVy}eN70I(l=UgFj+g}R7sDU>8)6Uapigz~SiM4qG|6{!v-t71e} zS?u&0QZ9#64FQkIPRMN~BNFe3N8Dm@W>|=WT~zA{x&LN741N};%2wro=H;DMyJM%s zp*J36%eqbM5l3-hhM|^fx5!28J6vhzEFMGmH;u4r)|M$a=Szyyyz^l_FQrp8c@(@4Ok^OP}6XR2>!FT@Z0#JL$l&-=8} zcn2>-5#5+Upf%@FcUtDiS0H=OOvhW+^5k;Twt} zV9Jm&%F$bK%&45Stq$;jb-x(rh4Q&juWpxkIi^4u_E>p$qEXeQ;vuLzl0_4|yi}1` zDQH;JIoS#ma}YZi5~g5StM-N9sIq$^^gudd(=rsBG&?pF8!_LE6l)(_wFwrsK2ypDKs3!!q zlnLOToyXF8%5qArG6XjpW|p@0IWwv?Yup z|0w1vX8rWjXk0bZ(GaoHzTu?Gsu`e@PfI67BmyGA9~G2F=NbW6=9$ZI4x|Z3QAtI* z$s(&uA)>kuq!E3ozy$8Vq5%N65_kxZY8M&xpA$$>2Xi1Vo<%X6S}Zc2V&6D~Q1-nL zsq8dRcisr5SKgXS)JY2Ol=*FFtu9an{6|2I7ix-U9zy3Jlc1nD(hH_9__dBJh_Fde zl!qrf(}$F-*q0zgC)I^6EjY%7#HC6d=`O>CLN!G_WFEtV%8I#{7fuw4&MXJ`l7#3M z31E%__#&%EV_pG{grYz(#~{sE*^=4!``Z(K&Bd}R$9vVbgNES^m6q2&_Di*hO0~9k zwZ7{nFAk&~)l!w%CVodlli23m-O3INz0K^N2*!8Vgd#N3?%ctAL#-KMlS z<*L%tDy!4Q;;MT77;_MUl_hdI)!xclUlbp~Q4McZ&Z*am;K$Mp()G+bd&c=xx5D4= z>!+{Vrt(eoU@V&A?6C^1Qn`FKZoyhxA?yKQE%C8JEHgGNdx$#Tl01w`7I?*qu9Y%C z4bf_?pOWd(z$aV@Od7fP8q^ii+lecN*T9zfe9l)z30pQ`=!H(R)4npwwi^DRo1gZB zno103fXjR5JKrro)b_DEK4i+h=Y)#VW2qAC zvN(UbXQY9uPm=L6-6pxqB`$doD-Vi#QOQ8&vNX|c(Uap)OUR1TsAUDq$-@C4WooG+ zd7wP;08<2@o*MJWBSl$;#iaf&N3TSqny#Y6hGXBEO-#1&xA^Dx4V8|q4z2DNiaH6K z%D48vFUK3~(Q}@x&o6EJj#mVchuiFggt;s04HgskKrDFZa5_0X8$@2J3s=s9{McTz z+MjJZBUyr%!)?hV zOJh^sD357OH4G>MfpbvZ>8$z2l|49sTxfkwu537qgZn6#ubgDndd9%rI4e*xnmE?$e= z^19&s?qV}V+|y!r)h`2@O-o!5cs(+@QalP zq|eG$>nq*S-cs(!>RF zbj~GynLGBf{oC#wAwThWKEEmO;m4Ga00y{To$G9%e=K#?SWBTV&Z9TFYP>>2B|Yg)OWoODCnb zmtO2RsKGk*yjsZ$r#q+H-o4qx-ec^X$E*z6=#5MzWLsGDc|;3NC+=+(oHBu&{SGoiuS($yL5PN{Pr0+cLhBaj;-Hs9;9_2F$?MuNB z_G`YsoE)uq9n6Df&KZTjUjL}6izHV&rU&G+GHd|FAwJkfZyNNU9Sd4M-L_Wj0Z1+L zo8(CHtTn>3#Su$#PfKBW-u$2RaPdgLfv<;$8po=g&$>^HDKrv1@KUIJCb0WKs!_ zQE2>yoH00m?1N^3aaES3#e$Qmb`ZwGFtx{@cKmJkh8d^~k6RPQYnMAgByk~G`$#sm zN)z5qG>`|ec64@$Jb@78;^a~CkJr6lvsM3^Bu0or-8A|o3omQ1IH%VVbs17#{KmlV zBnBVUtc79vZ6)|K6_&hzb07B)Oh-?#yVq znY2S$MYTF;JC}o|!ryM|NWc|%9tB#7JP}|z@vK;e0cFIRF}P+ESag$H$#3QzYTzE< z+ibB;bz@XU2t)63%}SR&jwgMPZ?Z(uG^5I5ZHP3_`&!ajy~L!R7I-U871g&=O8gaB z{tA(VQL)Wos;OL9?&i4}Q-Rj!luR=c1i*1p%23_%BH*Oh!!F?oQN+``q3o zi;W@}0ZpK>Le~OEzbh9Jh=XE}!09=K7`%A%LnV+Qulnc=C$Iz=71bZ3&@6QulV{7b zq7xmqJe7s>=5>_3+hR##=pKi%wL(3lBnknuDPS$M_quWhT5HhgfMqEXs(+@R_NQ{?vF9r_ktFrUY^0G zHQ)aa07O8$zoFc5Ffj{E{kpRE27)sJHLbshu+%Dt~usN zPxf#nWIG~uj<1k|BnmMejzrPshXAi!A{dItGUo#w36*ymw=q=b*oaof0QW&|u{gne?ez-YO13i)3$OsZV!#d6jlodWazNk3Nr{2_9PY!mO^q%4qlcBF zb-{)Ej{EV%trWARW$`cZam3jr^sbQ(0VoYuc*wgWIzN39#P=wlk4?PL!ZY^c=LH46 z8|tVc73lz6#e1Am zRxq(crOt;Vn$tr)aHPS+th8mm5DwyUi3;!)DtQ}MGn{+vytIPw+Tay=7KJF3{6GM) zl#7G}W1ym4^eM~dQ;-OlP4Nxx4l1azvNBU8LF+TLCf|hv<3PqI<%Dna#~lkrPuk53 zX%r^Fk6JlC&H^!z(RJj&U%|bzqNRqb*=Xp31E3Uiz`!FzT!tkB7(;|{2yX(PR=z&L zzyd?*RFMK$L@(tw-HBy{C3F;C;8(^~rgQzqu^9OP>_=hBoIvsa@Z)6|{C(dCWlgF@TI(Tt{cH55w zoWf`FO40>ek$a5YdaXRrtCFjK&PCUgk~#R_k|)TPA~D7ADm7X_hNR&z#%gnVy4D@} z56WmEnk6w<=H+Z|E#Q4b`u_nlmGZWh3p&fFYu9WYieXv`ll=BSiF$+q7ow&d*a!}e!8P<`A%#fc4m&O?e5X!67a}k|ev7Y7Z_{?;)G+HreQKsB6 zgU;^X<`mKTlQMODxn;&#TX|oQzji^avyTb?Q;#)v3+`dIPn3r?#M5s=9e0NXTGF~p*Zq*hGo3LoXszV zOVT?N- zVq?Pogw!lR87iRi_@?mNRe{2NLE5xFo8o)ghD*H`R|N!EOikmu3#>mUTsC-1$j{kC z_3e|y=bLl)!aO1p*;6W(Fx_6QVMZN8d5cE*Ntfyf8o+`g=111HCK6l0j1lVarFSj8 zAvl4QeRidU5+fHWW~l@;vfjZSXwU?efQcYQ{Ai-kXZCRp)ic9iBWuZFl=luXT8%r z%rUB5;>nl?gu@-h?!3**r75R%5~-(RAcU=BDb#~ODGrSrD{Z{-Ly1z$&%ai1(uiC< z#}tiZ;PQfD`mHJTZVs5b+0APOI5^*w;pGLe$ioUpY@JardKNUNeAC|+E2^auN9PEF zY0pkh$xeBEIo`WX(R_`*Dq1+Z>6KC2Y%Iu7x4P51NU^GnI|?P`T!`X65FX23i2n-V zi%4r^e0!RVnkC%tN~GAo<5ABpk%I;>8nHswbW}pmI~{v1Vmw>L$jjIjB||ZR>&<#< z#d=)57$bA@D5HE%OUs8W$}GH-|2ckl8o7s$H`~ZCsbPOhzp_?81v9}cPtMB9bQGi= zOlc8XMlo!1xpGcVGis(!LYMZBcu&6PH}I}6N)|43A7#-X{8j}mlCAZcJdJc!=zWm zLmJ(4AOl4#(`S<}ETNhF#wlu9o{WC}hqQX=;LfHfnZ{B#W2GBzrmj+b%~|7p@!_{; zwXs~W2Goq5z_5bPQ%j7H6|pi;g1D_@upI9-`bECC2*&j$nSEZH&~kr?FD=nD^qUHz zGwpIe_2eYJXfPuFCx$&rQ_&rn24?%cDTJk4pX4=G_(W4B209zNWjY#}4Itc6uG%D4X zCp?}pRC^!HlV&D9_J=e)4mOp3HOSH?-<8nVYL~ZGX&RJ+{v&d4!ER+f!vTR)-S;Au z&i>zwNK#BTUbZe+V+p=qUr17HnAas0TY!)U7UN>eH(@PG>j*v4vpP0bW7PvxtIYN@ zh6oDcV><WYVi5W`t#<2b-?JG(1A`M{ z3nJ2=NKQpA4!bxanVc&D51r;Qh99bL^Z-p2KiJG(lBT=S(sfr z3xlQDA1Ij+6u#(gLQ|AQ+;v~jESnzGY(t+K@f#j2FIGP--S zOalEvJG2t16c30auywaZ0tgfNTWs$Jb0cns!&QV95xo1OE9fw1Xv773W#icw0U*ih zLq^~dIoy@Ly(8Y!Db!N`8kK7@RlkXLh6{U z_K6_A=(D00&y~1D$h9DHH564kdi~gB79=t)eN53HcT5>F%ZbaENh@v#giL0cztegt z{sm+sUJ5iz#V#m8oF1dpQKzVL%XBPo+Q}n)$yuop>&h$7Ok6Jtla5(p*<+bj~*p`mRkfh)wd zPQC>HY7`!kV5T`C$)nOG9HyNyOh{#ph-8b1RF2@MuDUa2?FXA0Z6lu1cuh~02(dJ| zzZJ0YM1Ufip^Tvtj4H29I&n`CY?3^PVj?~{h?I4_lv6P2EOyRJpzP!DuDe%VkbLwQsAU;-veE3^Dj9V8}zpCX`dxhTb57Fvt!dAnp|c z@tzPN1_CUaf6Bz49!y`CW)0-o(m3-$9UPlN&nCvQET2iXhcqOUS|y=xnJZ=}eH-4i0#)!jy$){fx*TgbX&J=3eZ= zV3bA4mV@}Xx+0tiCnbTNz?sF6EgtVm>3-+r*-931Fnqa53CnuRCJ83@X^r~bjb=~s zAS*hPWa_35U7TOvYLQZ^rYUV}ej(g0Xsf~4I$4uuqM94*7VMGY7!=v;@GA;F_=59R zgGv56BglvJE1NMX8Fj-DYx)MJ+#j{G_02bpz%WFlLpPEONN{So2U$+g1@zKo zJEgE~*453L3uymAMS;_XurD!Bt#mX(gdZG))N>|uwL2Hz#e|8jsyyke*eaQ9co>XfSpN?OG?-JLyT=v$+a$v#nPJtP1znc+k^xH? zS5&4~o|zP5KsNH|U-iwmC{4|gWkdS-iD4PseNImP`Sv*wDqBMNc;)ongoI_KAiXqv zZq?LhQ5KQwU&YkE=-QydhLT&6FKk0LZF$m7W`smCMa>jO$19%;$run||H;T0KnB`9 zaD;B||4V??J`7g@rzeZd%Gcz1j4JH7j;Wyxed7`CVYlGP1Ktaa_EENPHZ|lZzsVEu z6-X*@f`Up9kZXblffT~TQ^G z2Ka5K8D=s8h8+eu7c~vpRCe(>@}sXYcyuw+|5{3gkpf4Lqg&yqlVeMxAYwy6RoH%L zA<`9a*(d+w!gC>}^J_sIBAKq(&*hzjTU0UrTFT$%`SVNE#N($cVZV;X>34z`Lm)$( z6ptMmPfdT0M)HB|Ug+*;@*8;nr0zM-h!6Jz4lgJqQSLmLIXrD!o6^ zb5gv)YlF~<%+%z2Ix;Op@Px*db6E0?$^~%?XcJU>5&etkRGFGDRV7qeiKBW@F1UKJ znT_x+2->Rs1cZU)A@Zk_ka~oe^mE`~ATnU#2|?ROA8Qz`5*ksIZvde|MwZ+K5Mwfc?eMX{Ul)aD2+U9ypp9z+{g*K@z}Wtx3% zgeHz!N-1Ct+F3rZ)p4_y3{urqeeRqE&rJ;}e4@%o$N%SSYR$}*Bx<8Y8XcS3yMkYA z>vG{yC~>oV3#Ju_$PLdM2wher%93)UVnA#@PZ}2 zp{5+R8%{9wf#Cq5F)I)VE=Sa|GP0uxNjelNxsb zKCWDgM!D2?^>itzu1cb1)^S*11;}5?!pE*;tB?qLx!qZ)E7MRL)$zZ?>gP*~Y0B&Yjp)HA8PwuB(b=Ny2jEYhO7n$uGXcOs0>MR;xUtlBX~2T=`FdlzHK=YNA|Ten=7S;z4NrMm=ggjp_;(4JH>xL7 z7GavYmlr2Ahc=Au7Frfs_O_TPa$pXC1qlvPTi3KAc@fuPrZbc)fFVJO%Gd}_95t(1 zXV&JVjO#7Dic+ioPFSe!ijdvjWwGKsa*t^T42mmvS;yo*);(z@iC;>+KX${=*xi$z z=qb-yZMus%!bwAvwXKuW+BMFw8A1?{UllEHQw*sKa{r8>GNa|bPnQow!9GDA*|do} z^{>?Ooi04gp*jMTGz6&-R8a^@WFoQ*1tyi38S2+8*5QP23sN1FEJFgdmzpj7IGh{? zc0M`rp(I5#79nq?4H3<7Tm*cB6?R&F&MVCsRQ7yDwc_Hol8X_XkP58`$Qejw`j}Yu(3$^Iv z%2CM{r3d0}*tM6sj{(UOSg#x3Jdq7#A9(x6dtd~CwB`O&U!S3|n_f6be~xf0;3Y8u zypnIygL3nRVr6OMAGZk>aBUuMgug9|~mqOZw47$_=19L4QP z8NCmPn}EJ|+LE9TKP1MkZT?Gnjz*F_ZiGP{v%lpb!F{H;iP$mR%9nx=ap3w;)uc%m zy#h)ER}(HJu+4NTDIq6QPG_=kUp%#kV&crU^`^TNU%0&Ps9L@yw_gQB?d{eZ{B6X= z%5-j?wPIddgVJpfR?neLMM)<>y`sS%8%BnzQ(fkMjgp+Ua+*ZdGLfv=Is1IC>bmxM zbUm#!(+JRI17XdFDMjc^JTazW=0B*tZ@&d3oQQx@(K#Y8Q4&X{Uq3N8;!EGes8ZFf zF;2csSxY2Gm>n_7)hk*pT;g`+x5XjUHL#mxWlJp=rckVvJiaN>#k*xJIk_jpf?0y| z&N393E$Lh(XkL*jR<%46eb(z;^@$Kk<+l92+Es-AA1RD>&@DQP?M%2s83L@ zldc@ieQ$e2c&7tk^P2+wPG=HWHqqXQ2Jai?VwNN(WG0SVfHsw)j>vRb|3tINKdfp* zHffxXMR`vMFS=SbJv6!SdPv)lJtoMa2LL^rEt`sW18Od-BjCh)=!P4 z15@LeZIl_cbbnx%banRrItqXh#HzVh1PyiShI;2O?E;Bum%dC8A=US*edIO8q*Yik2MP8o42QI$90TiuSh> zEDm|E8P9bsRNYhMk2)H(aI28<4iV5Jp+WL%K!`w>V}}CW1^c#>Tofade-RgB7y0C1 z*1NmIsH3WBq0|(b_MJ8Ioz6BZzQsbY$$X z3>AL%lsi2Jjh(m@L>|7_@^^E@>4;v`I+|f6tMpUnk!plk1pbO=Jc@2dXCx`Yz-PME zyL+zv0gi#D1TO`{0mRkzJ60&GaL;_2aSvk7lJPVt!|%|bqtG)2gAyG zC)m=Fd!nae>L(wIBcCXaDpY|NFuJ{=5JG%76eI001dG#6>#likQ+GK4sdtYzKGM z!kKosniwbXG8r))ha)CO3=@r}vsz{#!UiYCAmJ3wf}^wL_QRsEtSEzw6vyAA=c3Pw z%P;q#OmZBkpu|S4Ugu=v`lXFoDGK49k5|PA&tx1fn z1gI#<3AW9#6k?1~6x?E&M5$6Tv;Y*sgg}A>LrM`WLXjd^g^AM2tX6UroQET#>cz@u z&eqw$M9dgYlq97B5Q#vLA`%ir(A}XDlZgo#2^k2~h(yf{jfgBvL}Ww~iA07-Xc0}s zWJE@yLvVt5r|+Bf@vXYM8q*N!kRFgsqbFH1GWN0$jP;sbe21W=Q8Zz+p z8*6@T+lB%Z<;n4)&%O}(=hbXKDj!-P49VOh|j5h;bB)6d3O zCKCyh;iobZ6JjZ;A1p|2H3>ptJ4Vfj#1Vra%Exju8P38OVvHb%AjBC)aEuVa#TcQ^ zBhqTkagHdHPB5~}G?Ixhl1VtTN)TbDoKXgd!pJ1UOEj~~II~GKqaIVtEYb`l@(d)R zNF;#RV-8V{D1r(hOyaPKBMPD+%H~?5Gf*y)C)VYKTICgcl#Me^FQ&>Xvbry>T^C(^sw5_f6=baTVRmm|+AAgJ%0of)HVY1PEaemANsDAj}}f80JEuRfyx1bBsBJQ4ot}GYlZ2 zhGU4rE!yp7IfNAA6wFlS^{PnX7^YxAr1t3qAVLBXTQRe95dlmOmRjSP=gFOp$a*I2r%M^ z;|?&xj4h5tnsMR;ap#IKLBb9=;|@IY#26saM;>#99z2qZ zI7z3PBhC;c;|&mL#0wxp#T%6mcdv zx0d3HETa!BGYS-PoyQ=KIf9%q$2f-}ScGF3<`6-QaEL^0+8Kxt!3Z%Kot-~I2?H=R zBw-f4e567KLn2&v760tmj6z3oRBc;fH9eVe`diH;veHa_{@UIokkZ3VBmHN;?)sW! z+b%@ua%9TB+AXg8B+8pUE__7Uvt?JW=>1Rnm8@8|c9rT?=+N??)M!k>-zoY<(nUz*d}PoIlTK{QDh zi5KogowB_h%u1)TF0OUfL{ns&DUn_BV>`RSn|z&t<=_~TrGFK zidfzMTXD3olW0Ete%Mc}^uz57LZ;04bm-3@LWv=(tYcg?mJD6O*dQfh!WJ+~4Hkh# zl5n#G3ou6tCE}XF4Oq9haSRDs0!iV_+`=|7)D9w(h>wH>tBauqxTzqm!=GweNKKCQ z26TZ>;HQ=~8qBiZ9a53hs+dlZHL8+E(H9MzWJsYiC0`_2;Z`POkb$EjN5~A6Bql-O zG7k)pC=+z(>&C^{DlCnos|YL*P#`>@XhGwLuq2YtIP=DwFi_G)mZ2mvN>VMNKyq9d zg${NG9f?O$CHD*>S?r{e?MMDYq%@Hja>SDMxho+g6j1`qJbTO((u`X~DDgy%8gPb; zG)ZqXf`};NM2ZqDX@`hWuo*wy~VkcbEVDn(Kp>*rjEh!{g z&l~AKiGJD-*;6olMr@x*s(R@*jHeSO@HW0p{*UIwez$au-N}|+%+rk*v>S~EQ2D<^ z>1nnlH^vF~Pdd1fUX4GYgwh;s59H!~d40Ui z$SX2ko65~3cjWrF`oS;PFZRB>-*kd$lvgH`L`mF~Cng)Ig1mWjm?C;f6e+uEGD2AQ zp`^a2m1E4jlcJVXLrshmEQ@uK*!SBk~NUK(A0MmWBeJibq6_MWQ2;nN3qCgt~M$Vkn!Tj%+Ar!a)TM z;W13~i&N&bi>$sCy{X07^(^mjR;rNVKiuigL+Xwp*aZMUjR6gWF!RKj10WOt(1~$D zXhMWY5-_l`h?o-u!ZA!DWMVM2lJhL+)17$442D3$M}Mcjc*JSTS{NaOF*LA?LWPtI z69mB_VIZK0i4h1;kU>b07AQp$EJ#KuR;6_2v0LYh9{b+=O0F5;3P%w4*u6tr=S4J< zO9+L85)lFrK?oo~hsslwhA@RWPGg)x9Kswzh*5|!1t6t4LJ%N<5F-Xbn8q-OL^2t( zV@8=Y(FD51&xIlJ{4N(2T~%pa=~t?hIp$~h`(rGOSs0K}5eWf6EN;efn1(TkW1Ocb zr4YgxQjVdRBZ@(cC`=iG7-5JJ0w6*Vh9MA;XiD2n(O9F6r0>%0xBKf#SMB%dHfmb5 zmY21y5yc9jTouYu#d4fpQjboh>)l;H{|Qo}wKAKD#-^h|AlZz>ViA~#VhMz%Lp3rs zOq*DqdGB@V>UA8IzjyrQJ)^&`mG0kHN9(4~)m=2#Rdm};k|N4&rm8Kb&(%$IQB_oa znt$qlM?czkPASYm`P5!jKX`t(l@5Ld@FUnOVt+_JX&-t7AU;%S1HtMO4sG3 zt0iw~X+0;b_mW8^nn@;>CFi_r$lhN~l+$GwQEe5~ZB|ijRntwC)m1jtXRYBzc0;N*3x}G@QfLOm}3TPW*#1F&pGd>c47+i=9VncrG%PB*Jbu!Rkp~s>n^fQm(wEKtg7oRx~nL< zy6CDi$vvXhT1h2uO4sYv?cJuXwK=o*6~l@O6pNyosK3{KMKsxE|F)~Hrs^-Uy6LXUtfGpljQ5iANo)L+ zxBd6CE3fOS%FJXsih-GiVag#y7(vP*#|R*$2q;qyaD)g@q#Q#C>hJ&fS&RzkPBUfl zGK@PhKzlDquPj=rC~5 zkiy(kgHW4PDn{TD=7}+)fNitm?vs{oU5G}NN78YX(JUh(Jl~m7i24S?8{-#wx{J8C z`$}|HcfE4dQIw?=Yy9zg!btD5os4ankkv$m{HtMEp`%8x6?IHlq!LnhSra%!5$x3x z1xBb1Gq<-Yjqz3www!f7yrTaVHZWe7A*yTI#SN%Ls$RJ!ES}KXJ1*iq+8iKanR7No zV|+T`1DxSIS(f^sMy&mulsZB~&SX7167y6Ri3dMLiRZl+B1CZ-Z0QP{dW+CG9Pi&y z1&Sf5u5;kT$}ak506Tn$WQ@hn1Vbg0qNePsXB{FV*1avVj&UQx1tg4le9*{DhYxxN z2)>T>`#Me|9DXr$dd<1da5`R$Ikkvy%i}!lKS)?IEe$&qte=8+$7w3sWe(?xnK_Q* z?KzMic6KV?0~gIkU-QOXM^)#ce`CJv_wbo^muJ@`s)6TN>kavp zmprGF1lOITgqXsYlNC8Ns6N3$+l-z#ZyE~b4UL&m?P?(NLBeVHq6FS0K`n^|C%SE( zR??iJhH*kAi0HVOOktKR5N`5YsMMRL zJYjVwYYq}pmSSd@q*N`R;gtmzQ7S9a^`+Mntpa0VKQcLJ1uY6Kn^_&NH4lYVE*eQC z5=`mFqU^Ez49FA`NTrCujRclB99Hwilm~BN6b7{8eoX3YNzKzTjh1ceh2))~;0v`= zJvF~Q$N!bBVI-O|mr8-8Y+JDo-ExW1BETgtPcb&A{Saa55&%t0@K0#WiJe(Q&uXMI z$kC7~E}EvSeB2{2Z#udZo{=Fx2v!zUn<${cbEUZq4CfZG%e#Am)=2jtgmtk1sfM3m z7^Ulr%Z?*8w z@QZoszfxNy^=zZ`ROn5Qj}&c$%`cw_=1)dwrX7t8N_z^2$(*!}YJ5o4aF)BBP;vd` z+dXVDAytFMUh9~muzP_p{ckda#G3q$&jxcSVzO7dBn)aOwG+VW#havdAe-x=DoG`g zh53nN@f+UN%OH|?NpzbHOi=eLB6b9d-6mmV`U&)&spg4vlU_d|sBL)Nsx9>s8X*j) z3^#I`=fx_IqY(c71)4iVMp%|Y3tmcK8FWO;Fldj8wzv4Bpp5wD9V#BcR&0^{pu0YA zjT>F#iuQ<}!H%;|^py{yQC;T4rbPtgT#5%QIp5wRs+>h}=>zv6r3p;lb-WAVANB^ z-ok~tPt2cGFT(dO-v;4Y%gGuBoQ{R8UoN$+O#@>;qUm)#2@<}I<^eg6=pe13>`pIq z&?NdZ>n?AVPrQMOYWmEZ(hIt6u5Z57b#x5zIH74zU48aM^69Zz8(v}g^7DCXur?6Z zVew5vU@FA{(nhQ|%vS(naF=$^7RoxjQ!{*7$VI^$<`rCqy96?Ku|~_$(1> zNOrFzk-2{T55E3*u~+s-;Y9_DQ4)T563vVc4=M|bRyqLU$OAq+?8b|05{+Wi{sP_+u;z`!a<=J{-L4@Z;kr4Dd|A@~n|MRpUUnH_`L z*Uw64vMTn7&BB!o23$W}nZ$FKd`|ziYko&iN#gPH;I_YJV?S;jhq_7wakz z3TS5to1oBh@fdt06fyYV)K<|1C%n67X|d4^dzvd1{fjX`ifQ>4o#8DL9J9Hj^7Z6I zatGK*G^3xJ)bq>#?ml-%&_W}9YtkZQr1J50M6k(rD!i8+7F`>3n0k?WI6TN-+w{|B z`oYA*Xpx%ux@tv`uMGDKbG0Jr7TDacX(rM*z*MYA{YqKgZM^^=Yx*WA);l6{%-nEl z6(8QO`Ry1SdrQtW(vIrf}kKBqBMr-qOPbx4-qTzK{T`8Z$EvhfZmv9ZmN{{Ay zp!_C{reP(2J~a55XGgt~>fUNBOd!Hg7UNKE+X`0h-Ttq3WCKWlmoqr${Cy*rkt=tp zJ-tvcjSBqgUqVII85VS^pnj+jXg@vKYhVmA4WLzC2;$B~OR4aVx1gmt>pS zNiCr$iUpnE=*D(MveVzEjkG~vS{f~|%;!HZ5Un$;QNCXWb7*5;Gt$lIV|o+~A$)k* zSeuRUI$R{)ErSm$iYPv@+S?=rRX6OJtWb-|d;RnF)e`kd(M|8N*xea&u0|>fO>3}! zR4S0a5z&Fk&hS-`P@rfT;4HL$FxZouojK|(#g&RKeMH7>^d!qvc2rk2GGgx!oeUGN z2pigHiK28Quxba~7iCaIY#d}HLY@rEOMPwk-ftP=C5fvBh;Irar1`>%^_3_jda04i zrEoLpj|2(4WMG}XhKp_>CL#W^?%k1y(>Xr2sc%rqi zbuUzh{bvl605Nx*H!&zgz?`nitumG7;m^S-ie0)87)o@@8-GMUh(~hbKi8OEc9&%71sv;^0E&PDc#dE}LSXE#l9LB86SEgT5NI{$b z5XXid!?`lRI%M--f-N0zcJHoTO=CfnN_Rho(WUilT*fN&qNQ}RSv9Twpo!k+vT`E( z(=p$Ej1=sP6gy3G*|4*)bhEt$fconJhiyUYM#ex$DpUG@)8%YFJGCNS*ps5|OlUjdZ?0Or1E` z;xASbUv|gjoP9C6mHI>l!5hTDuFc!hCY^@1`ES@+8NZ|=znQwn0EB;)hD!d9MmM+g zSZ(E7z*%G4sYI-5pR7mG#}ln`e^ikzSA7s(Q{tXn7$qU@hf3ZW_GQL=B(1&KyItfk zjg`bvlTw@=7nnSt!h^aAR59!6WZnFbkvJ}$XUdiQybV?9yaj6dnGgSv&qMHXGu9-CT zkF1b8La7%8OIK~w)eKrk$+R3o7$>lI)d#ZVrC;Et&VIlq92d;)q=-bu8BEp1RPz)SVQ_x-cjqE#)5U-~8a^0;iXer4l-f zVkptw9{RG`{cHs6Bive)BEX;%8?BX)%to^)qiL&dm}mplrpp*&ftr3=s-bZYsr13SWw1aBp%uMx)N%_fx_Jw)?C zhbqYIk7iuzgT~m`zaiqBjVCw`Ubhb|_IyEXuBzh+g+VX+K z#G~ER+YA?4CH$U=FO`oD17+IRNzf!DW>8%s$xL;qxFk^s^ZV_1*@Ac9>2ht1JE+6a zjuEaBr1}!OX>WfB!y7QRiic@Le&xI^X$I(Rgm{ThDFw3_PsttR-IOq-^;?pP)9281 zHso_2A7i_FkI#c-`=WOrb0O)^Kecz(2KlAuD77pm-B{y*9CQ*TX}wviOpoY@Kl)}!t6`*A6hfG%F`vn zNJ4&7T9&y#CeJCE3Y|XM;akdI_ht<8E1cb`QrRKKTrR7BHYRdxJhwAqv@86B!Uqt((S zOq+yV9MEi3sCZRW{jb#O(bZPi+G!*1o?Z*YX~I!#PwVBT6%KhyfDA<)y2cR-Sj(L7 z;o@(3KqGudK#Bo$qU%bZ5fC3@QTdOakPwWPgkR_HT>lARdp7b>r2m!e`;0d{OBeD7 zyBkFyKfdCV)8X@<(6IU-%hwcO3`B+5>n=yK%w=ovwpkFbWQ|j6vzs3Wv#ikd+PT3I zg2+pQAkjp^K0;bhQzUyBvI|_KUJZhK=FitCB&$SF)bsR7#&ZX1v9oTfjUHkP$n;ld zElOrNZVKWg0}vr4uq$Q3sYD;-6VQ#cHs5;Yb%c2FCAKwYsQp4P-jC2ZID=1B=Xefw zE@&g{+PcE*el~64CBUuqs^2;lCW}b=nSz$ut1=R2dQMWEsjkmmM5j#^?X5&HD$!7y;=L#2xM zXo_ecaF1)d?i&aVRo~NV5gd*ZJbV+Siy#s@FqQI$p8GcDhM17E?w;3hw}O@H%x#); zZB5V7Kho29(OYGb-FafkQXKr-dvN`2<<(<7vX`LWL51!7GiY(Br_zj)z`6dw9Dckh zuG>$PmQdTIzYF@|y5%*V3vqp5f;I^Uo}rB}l#~2XLZs$dyqxd!rBOH+WSbx>!h>VYRJholeNiL zrS#e#jINzWP{xV8n z32heKAL5(7Rnjf@#Mf}4zy zi!?A0!dzFy0vslxu^lJPy`)6|r-Jd#BN6g3sP`4QFCB<17od9eIdq;G$J^CuL(n(5 zRO)sNx`_hSP2gyhNkTQbg~JqFO&KsPvCd}KjQ@+*iNwc5twzGfKT%~f5MASdqDwPH z2(J^qb?;PSp9A!ZU`Z7kBFM!l3Z^Mt-dX?}SemhGJ>JRXUi8CA7oC$?U87M`Q){KU zM6=&Bt4-tn|5sheAeD8=!rY!5m=aRib20&wv>|m6a6}-Y?i+OYYjQAgA4xda}Mcj&!Cj$^=*h z_!-L-YS!sHx-jZ1_DQbz@Gd~rqc}Pd9%7T8y?r9^Aff+{`xA;-`I2Mhj+u)4+9=f?fiUw9=)=0gOg8Q&vW9M$8ntq4ja7WH zh!OqqAGpEjQN@u+R3=?|VWA>ZioLHia7E#bpR9{B1!$BwfHUU@Uzk!x*`p+(H4LG$ zcNX(#Pv|(AX+)yi_b$i2_Hc~*EAb|2M|w&g*q4TUzo~OZPlaWIGdXJKN8ORgqW67- z=>i-A6V38Bhy?4j`1cB9 z1A{^}ihOKO#5B^+G~6-Hw?tx%QCDCWvW3X zP03KgnJ7o(LSkwEmv9iu{A9A<%o&((o3o~Z&cy%rS1ppCjU*?7W*Rw97!WjJ+OOY}gc>=L&kgyV2y zCT`fH_D0v3WDpCsD3PKhW(Z3VT(dXe?DQo3Iv8t_Zl_wFb8?tPWjM#h4 zyc6n*L<+2mF#EtsBFo2K+ixa-E%i_54$!a)zzOA*BegFMLyzxm1P0@X56Jpk+9kj*O!7f_+nYBCUg%n zU4gLNZZm=4kri_tb&I*#;d9h&j+dnoYd5|pqAlKNU3C0%GXa|JNFAIR?kkOPtRV=SeFXV7U(9cgAGIgn3P6~ZEF%qK{h#

Zxg2d7YqdMI|QvGD%YriZ~fMSPaP*UMvb5%wj2DO)Top=G;#LJth3GwP<@#6|E(s|W?)`o?UCvi~tL8&Q7+4jW zdO@Y`%URg$g_4}idaC~LAi#qV2*<``Mu12AQ&J15|aOqHEy?kg*Caq{uixf4se0mcuTakS$-T5w#(!%6?1mM1UqdA%Ny0>dV0^ zFr$@SoJ%I}aQFbZMkEqog&5UZ#D>Qg&m@;;H*$SspkB-80qY};g9gM!7_2`yJxo>O zsi^yz#0V&DnTwgo8kQzRk?H2N=43g*vL#|j<_Y5%oD`qa3zjK&p|B;|3m}?g`w(a!iX~)0yR#G%@PMEZp-F<-e9o{)VMWAB(*`X);j@pbtNv`7 zIsCJZq*Go^dUceNoFW}3{?Wn%Cz#w|`QTnC(vv8v!4MHZ2NjD7RhJ&b; z9i?5?8^y^d(w;k}PYYQvy)bMjz1Ao5`I^&(w4cj)mW_gY0=iIK?&`IP1tDr;Om95t zU5XMGL(Nb0-VSpu*s1y0@-gwpoY9I9_$hREgPhoZ#6wQ+131NZleuuzs{keBMNtMf z7lAn;%EeWgI3s1%3dh4zexwGh;idkhnp z1d8K%=e6t1pQBLK~EmsBWQE^;z}PwW8-qD}Exour$QJq>owp z7Pe-9tyqVx{0{+ODD7(+VtT<84$_38NG!-6c|?V4U*i1A>9JPR4qxl z#1gz$5LX2)JU`hpl1av{!Z?Q&#Rro|>K~*lAtS=-Q=i#IcB}7YNaG0cebxN;YFj>tv3z_QpAxM%?oG41tyu2?AgD}uFO~Z8M zIL_0k@H`JA%=J9aL&)+x4%4vbIt}xD*fmVUFuyPh63nA036dy@eZAcdMzcMHyjLlc zNc1uj9h=eX_j~@2Tg&YHf2fL9o2jbm&b_fM3r^j*E?c_wy|4TFLP%*FBV=@tk}^I^ z$tfixWQ2&3(i%Zu*WKxNUKa(qYT33mgIHCx?L9=%KTl_awaVaZw0Vq{3lN)8trqM? ztBu_Kf00DVQdt*;@sMep$4TIMp5g#V32`DOM4+G)!pKsJkwz+oh_n^Lgi;D2PAY`p zk`cl%NQjXNcO8d0)^M8UF>qa$<#9qu5Cj1R?{c-6EH)CsM3q7zaLIi3zj?^s_B?+Y z0)(L{I-0PrtLqBMxNeKi@4j!D6(=OTn3HmTR7-hpDdohhmeUSi%E@UfrChg`Qesa~ zOUUUhBd$h$DR=%rh`gWD0zTQsnTYbIL58dIB@ie)LKj)C*YY;opEuXdL4SSp)p1eQ zHWfo<-L}=ed0O{mkdb%nkiVo1o|N)VN=W$`B&AH5kdh)nUUE7`+?OrKV_P>BbyZc= zZ6#AsKEDqykJIC5@%Ri92@9NJGT6-qGdzqYALO9|K$phBXkM2EIlLf_Q%Kh`PEj@@ z4byz!Is~|(H_ZdEcUgMZ=YE?ma?QiK z^EX|jn^%3RX1rXheKom8_f!4lpHgB~P5T34#nc2>ABC4pQ|B8mT zwbjyy&?i2yyO~D8n53>oI}AgaX-)V;6-IM9BY1)(3MK?4b9jt%C8MBK52f~$q@L5k zF)0lfIL1m;ntbVH0-`@Wk=2ME*mYpMy?ySIq^nR~@sOx>jLxD$=K5go_LTF3ku2s$?hxB~sf8Ci8Rq0Jt1PcgxrPU3ZG{WUy`NxV%_-)7KO>*xkJtB9xK%E zSic%QxKaGs?}l%`XsbQyvZAPxeBpUjQ!}9DA+4#JQ1!a1t&&jpt)-{D*+)ahx|0D1 z%w?W@Jo&x%DuopN)bqAbN1%tOU-#(*p5kds!PJWKVNIRg zT#{AOKdy2aMz);s*vLO9_0W>KrAvN^=qv~sa9+Bs-EMd2Dnux;|6ERXwO==71kcuK ztteJrj;=yVzWF$UNLWcFXe5!B5?_&)*Bm#mF-_#oEg!{I{;4~#jU?WxGHr;bAN};>C!EwvTec

)TXkvsxx28SpNSNsMuVl*DOl7C)L>r62kW@%+(SRh|-kMVc6w*T+6%{ zy`Duv0uz+Tt_w66lNCe=gh!>qiGd(&o$#?hPzw+$HY61SAqUBfi~<0m1#bxsf`yE7 zaNuOvg*rt=%^jH3kb~tXDOD3o$q8CvBHMcts2V-S!B2zdKk!?z_buVRCltS1q=#`n4A&9J`w97#3UhJHy&`vjAteBY0YR)1;4aB4?3K?`( zUQlhb<|n4dQ5Sx8bWJfg&8UH|0RXOYhi3;zskTZN zCQj(sW*4Ah)S{F?lHE0R)78SViYs*0!ejmZp32LFc(_qcmX~I`;WAUd#OEo(&1JC{ zSBU=|&it!XP#}}FUaTP8&XDyE@lw8)r;?KXB(jAy{@Xnl&i*7_;R4dcpvnLzXR8}M{_>R8G5znPX z6zdPQzkAu>hdrxJug>}2Cz|GFJSiHsSNBdEcgA?tQ_ctmv@E1idr{SSCFEL+L-eMO za<^WZjAW?27ks2u(5ywGwfz}k{->>J9JD=|MAOWtx{@ zxmtH$t_>TT8|<7lEEI|{kI^0<33drooj5Ma@##}P8TZw;In(N{(L|S3O|6UBKC8;A z(%W6~9@Qn(c_i9ON-^QNr7Ly)1s(A+X&T%zw+)5tdkat;j7HMLy3vze7e#LrU4J7zXL61qY%;q{1xf!=0@MX$4koB;E0b)n-}psW++86=E*#PtoKI!^hHv;D$N*+uccQSH}}4p`uXC3K@t5S zrkapNcFD@8WCLEJR0QL7l@rlw$EWsxqi=oxFa|B z-bsO&L1Tt%;+P_tY&)%~5l=EOr26Jvi+UDmj5Ag|azf)&q|tf$50jyK2?+^Ym?KF1 zxyNx?QY3v6sFJo9a4j167=_kzk<6ARHG~(7p*3+3AoETWQ^^b_yPIGBBR;R*vsJRe zE~=G81@CF71s)VO;VO`19b@u=7{CQt@=$V!1l2c8TM#PQj0bm(wjr#|sS;v0e~|J? z$o5!<>K+BDN``6Ah~KzM|1YJknjYvn?1k1OA)ufswD_ms5itT#iLj&#hkzKu4vpRc zfOZW-;-uq^fafqMt8(F}atH^FW?DH&6)+<;&ox2AQ2!`13N8h@-2sMzS39I8&C0cP z89-;u9iTElB_y4Qvk~b55e~-3evhIxI#eY%77Xqm64(?Fr4VtMWqjC4_PJBbX3FV_ zi=+)`TFjc`bPY|*kPYmdQjnNR?NhYpiAO3rCuGPaj8MiiVz(g%3rw|sC?xmo5tAB4 z1av%I^$O@Xx>iMeiV+j4%>1a(%sO=^({kQHrFm~x;<}5LG}Vn~mF8#oUWxWilwNs4 zQ&uK7vAIFYXrz7IQ1*oMO-(E%S?vkdann;CXhd>h*q)}~=WKYsE2dO^DK`jdt6gG} zIGko7XT|xw{bQG3(u}gmmSv_ewz+=G%6p9G$P}k2?_@}WOWGgB>ru6-*lN`hyG)j3 zNpjTFSs5zT;>)0!3%&IDE?>B)*{8v3p&vgfz89O@*P&B>(px}4>Y0#-9b%l#-1g=` z(`uIcsStLdOGoWv-;0VivVG;V+c*l-}c^NEyx55=QZLO>t7T7TzQ3*v|kB3^~ zQrfAZUHs{==Cp`g;BF_XnY*@#kwv$r<7&YWU#=}N@hvx!&j#Gj;vpBIOIi)?y2>6M zYSKYsYGFgc?K$y(y#zGkQo3Hm`^PTtyQ$?Rcq!{lw-BAo?ySi6utpnvk4&p0lUU+3 zEDfe6;J1h(8_-7LY3w0X6HCd1>VS-OhH5OViJI13F1oII_exh-oUBjRDipLz3$nFa zu%OSiI!0AXuc-@(wmuW-h+b7*6v-m@gW5ME`>>E{S$X`YVHTC(wq>;1h zWHv1I*$2<~A{fRMJ2?y=i$jG0$zy=TfOH{RXr+jW0G=m{Z5b&Rnq*#hsC#iUzlsS{ zrk^BKq27lP#S5ECsE%bUNfJW>ReU(>KGW7n)Bbr@=;1&0yGSg>Oz(Ow=?XR|mUZDf z_nuZ@%wFm8@+*e1pJy9pr7npZ039Bliex0 zqKIdlzek-}*~%oGHby0})WlYZeQcRTM6F~vRljRloERbZm+FR9rAD_S1em{9vn3rr zFG#~B+lfh#L5(OG4TXcr;~-YRJr`07jDWG?bt)<-3w4vLuqa`0swNaQK$(#Om_V58 zgIKyQsuXeb>(+$2BTXcS370fkeoS2;QP)%_&|?7`RWZYYW%WmmA~Lvw3}{S5;mL7u zn!zYaV><~Yd66P0LbPO?M6oTEvj`!wFpG^yoqORcH}+sW(<+sTp{-(f7dL^yX`31NKx-(AksKJzPn{MqDic0&$h2Lx5{W`TN_RBOe7st& zls61n#0>t?6!CUzL8O%`y@DQr(sUSE0;4t(NYRnG7p@s*_BgtITR8d9qMPwsF+|W# zQxmMdl-Bf#96)NPWCxQNx2-U{C*|r?4v{H0D^xUfh>SzO@ULpN;Oh=q!EeokZ{=Ashv0!g-BC47U0%1%*Kk=La2Pw|&5 zZ{;CGDs@_!_8X;J)9oQ%tFr?J>W5bLJMlh5cG9?uL@YsPU#ZJb? znVOK+WN~BDG~~ygW_nV%>6S+#o{NeV5QQ*2I4M%ly(*yqlTf6$W8vgOaa`vCSAtQ> zvtW>b)N)GjC@^w2Gw>)n^j32S*hMM1-y%cJpPi>BH1jjH?3yx|#c!B6Oh%T>+&Wjx z4b>NCO=bKUNvG#CK3tIqo>T6xHljhd48!6v+~ zEnF&sb|gI%X%cnxCPg;)^KvV(K<8H$;P8#BZ2A-zo4Q7rCMOl`YCy?&=NQ`rmyAqw zCKYUc6zDQ^;EW#V79@T69mEK_b$A-)2o)@>9`-yeSj;8#xKzme-n{}xfjfP_3k(5| zYL&i_q0X2H=Qv*5TF;Hdi6<6KJuyIT+aQ)LSt zs`%Wgmq|H8eSsq2hpY~UECs{o{s}p0tl>QJO(s+L*}mJ-v>+-L`6@oc7-hr}cV|x{ zr^m!CaWeuYYhOh|?F}>^^0)>YRUo!U&a=Q2_|dLZ$!k|9EoNAB>)y8PfmnSMQKUMh}n)hyt@GVPq@DPe#+xgggVH7rR*+MinwfP z6jhrAl{@9+AEr31C7hW1YFdqF&=8=6r}nBP$Plb#VOzq21gje2jXOVjm)UoRtZiT4 z+6bpDpt{S!bOFFk%vgk(%1A!Uf19G_FC#jpHU(ljB>qPBLb-JU3uQP|w@wU&-Ye_-fem z$BVaq5exRvQH)$z>*zsNTI!)8+G@@GxFRP>a?)0F{dh{A%L2)PoXhY|(8++4!q$X~ z0tLi{r0~Xf683Z*093TN?nGW5SFsg?KF&y#B#IopJK}Aed8Ocz!Uoi*BW&T)AxB8S zBzKcF{|=i~4ALu)0Gc!AUX`XII@*k`_D9!ulR+dae>x9}!3RzXQUus1L=peA5aHX`*Z8RdDg(F1b)A_3=s*im-O)~eI+WI|!%S@@O z=3xvn`*l7``RG+QOocF1;@py5%&MB5HYABn=G>+>?6!e#;6wO4{8jmHW^tpvA+=eT z#~bk`SPP|IHPo}h%D)^rC=N)xe8fZkm?;!Rvdk~c2pcG6PzAw6nqdyvk*<;5cvo}# zrEG1Np=8x6>n9x7e|_PG0g2@KJVAT<9(xvS3n^K7W?-*#nAaOb^9~O{?>%Bo+lXl5 z#|%Rq;lcM^L=8b7wjAqZC93fVap*ZgW%vpTm=&~@#+cP_wxEEQ$eIKC0MA3hUkP5G z4u9B@_u}>XO6z1jB%QEq|!O0#^czP*^pG-8L}^~o!QieJ)I?1 zjT)~Jcvep6cVG$oK#f6n#!CnK0v(TMCADA{$$St{`FaywW=;cfRm?`!eXKr@fUC0_ zz=A0CsTF~O$`(}^iRXVO#H&K0V2~*Yvbvp14P}r~|I|yGFE9?bf37NAT+N1d;F5xL z_f%qbhbSQA@*qL^DF`?cEYTv%d^*qp3fnWbG>EyhUT>m$G$KCoe*95nnp%g;*EiQ# zfG-#d!)w3FtnOku_WN%&0?#O_(6r*3tPhlmCmHZ^Nb29tg$?hTN}U7)?OuFgg z>dtx;TNp3Qw9}~v%d8r!Ss_iM1}#sumpDccM@5}f2t2A^#WfYZPAj)Ja6c`lTAupz zcS*uB#bWYh(OW4!*5w?zh)J3^HlRH`+DRP~mBc!Uz`auyYgElRNKozMv|IcFi2)1B zvi}IEhJLnf0OFi8Ivt`^<1RcbE`;(WnvsyQDuU3v6G$y!Mjst21?C%I2ggAv1N3)NUM8O)I!ei94 zZ3dO1ePuLrOO{j|O)X!(^Y^2eZ;-E(WHEwZhh-%_unF)D_$ku-|BqX1K-bmV4KLEQ z5P8!U1Jr3lv-wQ-=TbfWf#|{j*~%^Gf6{vBnYxH&`#2fQ5P}n!T{-#Y*NP^Q**c-j z08>Pm=t&VCVc67f+WuYSLtome6q%RpoJCnNPVT_0!jxT9y<^cbu$9Tv_P1CwWnFEy zYt4}XH`c3f>}N+csgSg&&*tr3&FNj>d zCbX9tz(Be%O^P$NkUEK?=dV+}AlfT8N)yr(CJ-&OU~NO<0c@9USv>Qq*QBP2^;<#P z!_NroMXIrrEWi_L#Q2|HYtfO$@E7{k*~m5v`|&ZS575_Oaf3wDSRK6LNaOFXy4ra? zvQvF8lk7V=CUA`I8_MSjMBdByocSnbgR#)cJvl?hBlede9T`RqoCuZ29ON;4L>h(V zgZfGaCoaLztRe{;W7m-%h9)Df6Eh4Z?jVL(w=r&9KZ#Ju- zQ_@Q*7u|4VpD`|nPDlf;IJ$I3nPPKdADPNkdV-TQ?w9%$eEd?%0w|4LGFt>wrPIz5 z?@hG9V~kA0rgqH5iiy?qW|c*3M1S%34mK5iS7aBG+8vEKKydK*nkvVyiM|%mY6~oG zA7t|KwW;RX%OMZ1l$XOIP0X$YWR5mmJGF5-~{6VFmHVAb5KP)E@R$l-pR3 zySZdD-|zn#5yIW}<_)kGQl8Zff6l$(FZ)Q9o3gqQI&&qRKg z9bMd3)|^7RkHJ>i9|pP`?6EVQz5h(|pxeu`lTi*TY3pXY0n6)IB)rq%pzk`!yQKU5 zX+(;Zo)5wcilhBzN7rdRX2u$>S&1xajzO@_gOr!3+{PARMP-2n4v_0>7!<@Jq=vg6 z#*Msv*v>FgdB=mP1d5cr#zHK`D7f6T&E(Kjp{FWk8^X#zO?_b}0=r_p%1?eM=`}u+(gLBV0h5jB!#PArn%~85WD!p{YIVw)2hbv z)upb6ZS#_xN!s~UkC1d2!~u>k3AN7P1456`vDV?yj$(y`04QY)gj-Tq5?q~fWUghfBFYL(aNWmSzkH|1Q!iq;F6*%>B&Kqe!4}JL!<&j9?Ehs6 z8gISoorKSgOL~)}MK~H?l&J~NQGYu zNt&-p?1SN_tmr}2FRvF`H}q?%^vnw409!$cPy%k<{`B z#|QJJ-(q)wqt47;`XK^m23Mu+p^M4e*b zRY6^FshUl>95Y@cU&r;fRu^gWuSKflK5p~*BAT~k?ZM|(Dc!J+3wS@Wd@>Ptv?utK zl$>0~5uWEHr;5`qRl~Jg3nXGDD8h?Of_t|KS6WUMfQgrOmXsxs8R&(IgN)uFYRoT> zs2mZI38v5+vcxD$F0;R?w*w{s9pw^pDn$*T!#k%Wf6acu@ix~D4y!HAljk@*C% z4hg4Z79zR%f7d}9L=fK>^X6`8>sm;$>MV%5un7(<%u#lwi54&G%epZ(W6#s1M0%D@ zTDKaK)=v;M+}i8%Eq?ZuzReDNpB^QCc(_i>&zQp}oe1V8SXyWxbnLYy~SOghuEl;n~` z`rTyGedmO7@}ttwq=Eklp+bK6&2-W)xNUyvnl3~f;4fWc#~_NAbdQ#%i5v`6C+>kt z#iN;X{qk={>C#^(l-l`8ov1>P$&b8(Vpu~P0o~zPV#?tll?LKf zFHXH$@8}{tLEWkBk*zR%P1xEBsiY4%I`f%tqPz`c?!}->`>2AX)=d?QdiXwk-jz4Y zjXGG_j6qm>KdL!mU0`dwFY-Ggr8`Jk5Yqh^O!k%o;+^{yr6agL*_#x2)S^TTUB!^K zJWjo)4+EF#eilEt@epOiwvjjma1j4T@r!OAA9$GT0o1k)H7VeO>mjzZo3la+{<8Xs=MH9}~F+0(Xw zgXl>mUkts4%-(m%#YKCB#q&t*+qUU#u&|C-#NxjGazvgHcf#~;nf%d|cg@Y77A9XM zt^g(&XDK>=!y_p7j{H_I>CU%DeFCYq z%nuQ9s>>k_e1wGb(0ORgh(Xm+Vg*fzpo_9^;Mq)!R0*8NAy^`qidi@iI~4AF!V24k z!zLz;`ZF1)cV+NGMTFC7)CF;d514&+=r~Et={=W<$jG+~rU5|*NY7A;vkG`S>-qNH zwI;?YQ;>3XE!4rm?Z%R0Q4(q4?D6S)toH5Kl*3V}r%`GQ=7K||#nrSZ$_di%rRlFG z`n3wh49*1Bg?iAgt^maL7uy-vJxD&pFrpZpL6V|}l;cLxqB(xV2J8fO`xJwdQ%p?@DXhqKRQwZ)2)cH(M zYc}H^^;oIEZk$#9kk6indRx&z0h7{-^s_}xf>g9P9C>7>4br}*aD75Q2ekJ!k32h$ zyIanZ8W(-``>qK@zQ#^Ct+HhFp=ChNPW8I_6Snnn@$#U8xLwqo9Q#BbZ|c&+Tl01l~<%{{29(7PIe zC4VQT;p+Pewk&_YqhAOTyQ*QRkuy8%=K(Dvh>K95Tyc8rlI&0_obn4kNL8BYYNZdy z6sY@S(Uu7Gi{?e>7(+^o@$y|4$#r=xF6^+e+Q({R(b4#fWB2#&Kz{dftY{?P+CcUyHcbGJt4;lm+ znGTJyD?)hD`SbxkPs+ZgR4B#*?lGMd2!P;_91HfyCgxRzam>Jo_SIHaLq=znXn024 zDfi|$MITc}w69m_$L!)3tg-&FAY7hXT$DauIVftaVHSgz+Zzq_L3Y9sF3OmEPEeP9 zAXUuKEy9HY5-Lxf&7pVfcwP`xu*6jhl2B^G-^o)EKDOmFi#9TK0y1Go=PWB_6POw352D65QLrHSjl$fjwSM*j? zhuS|dQG{#FcsRM4`4kW8-6ZQmP9C2Fp{Fm z>dFwPo-hm|T+3ZdMtYtI*&1(zCoS;jaO6WU(gz@Hbzu|FhcUhL#}kipd3wNQ4-DaZ zKRM=i5gKDfD5G~8ze8lPXP*rl+%1)7{LCSh>>=PL8gg&hom(8MB46b*Wu)TZfs)l` z<5>a+rC3F@>(Zi~rA5;jsqNi6wp#{eEJ8Z^Kli>JGdz2pF#!Z%hzH@2xMo_@TV6t( zO$5K>7h>!L9zxJTWh=p^UWAoWCJN@Wc+-e`IB7Z9&QLaiX5=1IoGAHf8C#193I}Nc zG3F+d!(cZ`AGa0njHtDu-<19Fr7+Dz{O3ePHb+}{*ZxT~)~9PG6UyoEJ?c@95;kB?5cWJEoyb_Pf4^Z1=33= zo@vd2<9aUdqcL)~*6KwCqtkj1GVqaj+q%7ig2hD7Rh!x>z)m)GBM*odEtZub9w_>C z0ICL8BUV%BzojHKOk+Tj2jM#f>LqX(NR;0nUCuVeT)iQgZYGI9_1z9Db)<+2xn1xz z@13)Y(nxBBlx^t*qRkGs2%%_V-IF#H#O?dZt=dw26af`6#N%9HCpYBt`lXzoWejli zSMJX9cMu;mzcKIFK|g^gnBVqC-aTA~Lm$|;(#X3eW}V#^k-K%G zd9pKp(JV7!`H))yXH~lGfkpdHwM2j-SN&cP8jQ&fb+L@WA&QmhT0?nK65dnB$lM`Z zz$C7r5>$w?l*wm|Af#stU>@#ZQ&QBrf?wX@07vNwq$qB9^z^0CW9vk^2)f-h@hVCt zb4p2Lmzh)c{?A{H?52|%H;UAc%<=%`^X}UQqco+Ub0M(=TzWH1m3mhP!e0$lqgl26 zR)zTJxi+hMoAPKCI7gD%Yb$3(GXBVBCECW&oJN<(wzeSV^Q8At7Vvw6d#Jw^ken2< z9{|9YaobK4+(r9mh9e)YwSs$20#fR`kFqe?GcAE5d14E~Xv+ipebAPSt~WsVIcUyt z_rOI|l@4oao=I(eJTi6c*gvOYl4R7qt_Uu!AlJ6Gcod5eXQJJ-9?x*bvTFLNmZ-+U zi^-~r!&IWmy3y@dYFErWo>TbKd4|%tNLXc(>(Kblv%JaINDkx;6Dl#p?c7OPDhr+_ zal$wik^V{Qcj`VqaT-YI6B=zA%5k{#_0wWz^VRDd$&YHZiWHh*t=z>NWl6ts95a9Z zXkI>DE#aILrKU;0KKicXSzH%=dB>1-HOj}gYgFrOGbYt+`lB$nZ`tHZfUjQk5dhyV z?-5H6E~kpAgMAv!M(@cBmUPqloPwE0HWCP+Mhu#AM{*o~$SDxZ2b`HWPJ)CUa9qO^ z7*=NiX-n#FVGpQwNwo|QO;ABhMs%TnO&zF89bcM3`HG3Dk;v{#YS!qkLrW;U_h33c zu%@d0X>ne#I=U%pr|T$Nu`U`*mT@o_T^%C(%AvE$nLKr2H~Q4>zrRBpHrTy>kkKHe^s zZqhhu4iUa!-L)5jl1?eV!P28qQxTuc>exyJz3za9^@#|!E4b<6KHkf%t0+d+&Qocf zntxdM|8q6HM!)U5=z}8~GOf$9MX8S8^7&oe)b2c^L-^hkEB*Hriq%>{!B1UDr6oZW z(%2*+Es{AY6P)S>oNYXwqS?fYA_<7N_9n{|NlewK+e8aL2ve6=YA?woDkc2L(y8J% z9T)*xmMTUWpM^FyCscj0WbCy%$lOEb&oN(y3_UlZbY-iqiAOB~?OHGEt#%|z^SbBQ zm$>JLk%HiahKKL@JL`!t9;fk7&>yleEdQ%K1P|i)<}0x}t#1bW7NSu|PvOf)KEME< zU@q24j<#-MiR^b2({a;ff1pTr4v`#%TG|=Pa*wtZtRZZ52xyPbe#zGb?SZ>;-p@ z>$(m)KXqyE%(^ns$utts$euGPJ0o8nv?Fe!3Da#@586}(jF!bpD)$-q@rI6_>=<;H zB#bDEpqXG-azHO%BUn`Qtw3y3f+#OZk=Va|6A^gaqC|UywTc)%ZqwaJiqd&w4>eea zSkiWW3hvb}CKG$9$I=o)W^Qo}-hH!`Vb@W8|_ zF(r-80vT4qP?|ktcIoCeOxqe^7IuN^e(w0rdOZmEtfZhA)wxpy^w4oLhi)E45HjQw) zejg2fvg+OK{Ky=#`sp%Kf`mJ-SPC?boGokPH6X|I)8HYKqsUcwfZJ>q9<^V(4u zYk@yKmN=mrpIdE4h<`t;78)PL6AR!}SZ5N>l7MPGk5*q=*qd11jh*n;jbyr4SY?fC~ zK>Wo8-~R2%5|{GO)zU|DJ5oj~)meCl{c~(IsVE=Q%$(5fkeP1^-<%|Xujt`Q7b5{j zBvj2p&Oo@l0|l!1ULcVR7Yp1HELxJ6&RCmI%Wr(E?#NK8WTO_Va%9^5$YW6QNZqwe z=@t*m>?Uxj_`cEs1E(Q6*8vg3(5(HCA{sRgLNq||Fj@$~IprEkiZxpP5xlyMUbAA| zj0k2cmJPt#h+@59+s)}_d+rUq;-GQ7;@wV3=c}d-*y@-dT=0&E^g$xwl5`MI1pAJ0 z*hxT^uDSvMl!Go`XQCts=wjrFmM4j5LY$$1ED4CBJh7Q83F${^Z+1--)J(F^ZTL}N zD#|~WVKKmmAqVJX`5~XA*k#ny0j3B%qe6 zc?9AL!3L*+;_k$~OUTq2F=Be!4M~lbGNa*^l&LGa&b|)_o7`4y5g#m(UtkdpkclQc zL%^4g4AdfHoS&_-&eXSKQwN|IC2V5546e=QuC zDA0pe6dQdtRkN2$Qsmw#z1N5mlUq^Q3>lTmDlG?OMf0k9;DFG+C9kjuL?w}pZAd^V zCXMUnOgvT;kon4wYOp6!?|tmapBv! zW%l5R;heHZoFEV_mC7;C#R%l{EptJ#p)Bfq6r&p93d_avY;t|Gd-fYi?pEq1sl>*+ zb*W%m&PZ2l4xN-jR5slBt0Fs*ia_$DiDV)yo%`w{l-i`)e?i>BsUX(&48}w=MUxPy zUsN3;$D8<8V`!{#^O9N-s2-Wl4a$q(x*KgR=#QOv{R9@L3Qrc6?Xp*+=B6F!`XZ5oSgrZdIQm~9=$Jx@?F_;qt1yEleUx658_8-D(aC#Xmh0u$L zC=HWFk@irqF-j4{`zFB}kzCVd$hl#~zTf7G$$aP5-3uHD(Ph`0`t?*E@LfJ#X=RjJ zGd40gCK2eNjIMWtB^{}pL^@)Sofz^a>kPp_f|RIHSh}iF$08+=c80V~y4|W& z1WBnUWAOtB3SaC*%HmSnojAd%tW6(eq~5j-zD`rhM%@>8sBmD4)#+tiiX`ayK(cI7 zrO9=Y{=OC-Nz2*RD6v|{>GweDBnCX%Lvrd$J=1lS_K{;YF>eYq>DwnIBdaq<`P0DO-A~>IDFJ%wNS{2 zti~;R9TzzfxsV_*S|M*(*x@;e9O5HQ$x2`@B9)0;gordoPayiCZ&S=PZ$ z93!b^QM{>EMC(^!r%GG>hF1;X^%%^dBEl;akkYeEph_m3;GNkyg}tyklF{lJ zo1nA~s(^2Ttq>kaD!T$p2xW1jMk&aamZ23gOc=?JX@zY=$~xnofmE`W10z)p7{sy` zQOm?@4)|1|h`vT+plLdB+6ejR}cFqg)fflLlA>DEO(dOQA_ap(C5eFAQ24HX(Fkanogd z5gN&?LivfrY`W58^SS3HX#|lz!7fuOO*xAMRT=oo;g!K1z>ZO;3|ckdf{5LXa3D~` zQw%6(h#29SLqTq^iy;=H9#fjbnxmSclR`fT*fMI0X%%=y%dXvNDiTDjiAynk zUaR$JlFp7uSkBa>g=7?!$=si_#Dx&~okV(L!z@qDe$Y~AVi`4H$K>YM{9bHa?^`Xc- zcYnpt%7=xIin_e3aY9+7dT554VGO#Yxt2x{V1o!^F&zlP4KAifNMap7_$<@knfe<) zHpd%{o-}#v<8h<^dNlH6$*dy?FeWHjsi`>;kqCx{#v>CMj6yJoHW17*1VM;Ej3J0I z1|ZB3gAhRkFoqb0FoqEX83Q218H^!_LkMCam}DXlWHlQIghFFMp|&IvG{l6atxW`0 zsM)5$g%pJZ#Sucn!7(t9pF7s`EgLFe6jlC^3GK*FF#dvcRKQ@ zTr3}-l}>ecIn?SYTu0-%)x|rzoUV5|l&*Sp6ePUbW-G#lMCe z7=|Iw_dB_)oZ&PWOhzUliIu4%5?o4=Q9@!!g~`cVB#4a~G-Sz}BO)du6C-0`A)y-^ z8XFr42IE1ou#9A55PYn3I+TPn6on}WRuP0z7^ErG=sKN@QWT{tn1*Aq*hV`Ygi#dg zav6w2gvMbAVi}CVFqk3;f-s0C8wrM@K+xERLqj15goc746A=lsA);|Hy9EgnQ6fr8 zP@KxW84Ms|1|g8)&Ee+o;Mm5)V=;_l7|a=iG5hS8e!Xd5wJlpxziQsqx5?7dO43i8 zrTfxK-jZ5RwQVm?v+R|Ul(p|lTDFMiIHf7d7J}eh9Ys-iTo#@9v|j(7mK9dNUrUyE zMct*A)H4sOi>(F9mjD#3L=a0#aZMrbA%!%S z5a&1`gA73g2w|8b3_%7lh(U-W8HB@Okc4I;84#Fgkc3T<8b)I-?HX;&IEEBQC70F8 ziq@;D`6Q=sxKwuh;3A$`VukwYtSI(b4-RPoM_?VejrPj*4n)IwlRQn$rT>=+v z$bFuVK93dUZP@dcUu}AqW$pW?kbXzR+}y-tO*ql0A7`bO_LoC*nx zI8s$)YvN$|P*92{CF#)UR9I|52k%Qz!l4C&dCn<(e^er9Q=lLc;7LPE&m8gaSf?N%{Pa zl*@&}hm4~m)r};C$HOERPF|i{Ob-bJ8zscOy%|0lDl2OVS-mIWGeI&bBRer(N7Kay zUoZ4RD?*-w?wNF2MrJqWNCe3&pu}F}Uy6+k2tSO(A!_%r0wMy%YTJb>XN>sFl@&Cq z>1XHC(-ZY<)>`-T+N6}YMy(#3f7o@|osym{d9uc%qEiu3DGMV{l$AaRibceTk)7E3 z|K!-D^o`!@`5!IwYAYluZe3+(GS#(!tkD{?$(@bwit;mq=3g#i8_#;~1O+8bWKucB z?iOMJX6*SndqEqK&HxZIIPuVI2;LGlu73@sqAqR%;rY)51Qp)yBvgdH5Hf| zs+EeqXPIqKw3)0K1b4i(=1Vohg!b4py!lTMy}2lB0TR#m=ZQ~$ZdacN{%Cm z=G~UF7FgCjQ*>bsGGR$%Pq~|B>};{1FGQ-Cx0MkADR|y7UJ+tYlW0px$1^NmY4$oo z0L4VvaRocBZI!A#2e7tg6%?m-Ql0C&VNf)sL9|injeV{@ZiYaN!J&DPXJ*UTIw7C@ zW$e!tA1OKuV|L3;l5#zDo<;nLJ>M1WkF+lQz&JS!vS=kOi33Z2SS~3aBTbTc~1w7QH#iqASBNRr}8w2T#mFsLO^$xTv-!mlR0KOD$XAyx$bDZ+~@p5H-+ z;ppZv+~--aIaQ=ot_-t9s8u$v7Hm*ijF_3}Gd)&!RGV)b>>8J0W5$t}@0rY%;>d!& zxDBvLpP#t>5X=dkM~_iP1eYCL9MmLCfmyLk)D}lIn9=NudI?f|czsScDli?Wy+xip zq#Yl{+-XtP&(Aw1iJ?!5fHp+he~rEjZX5jxbjmQyEPp}uG`NuD&p6IRq$BH{cc}?f zp2fHMxfs3W6Qpl~5d^Yl1`3(5nfPQZ=0RK@LNXiL)c7FciL2_^8C3cvAkn5-5r|tP z@?_5@j(NzHQ4$~}p=hFsAM9|&9*FM4OGdOb2xVjaiIFU% z8ENR1wI=yl(BWaXNcDid-&5>6B2tSQW&64EGa7d|;&$&feBKtxunCEzuv%z4hYL$T z7{-e5&tQRJB8bodSqfe=?=K783cGTr;l3|FErUnQ(wu` zsdw1vl7X~qrCc$y@@giA{W+h-YI0(buJ$)I5k*2qcO9Ry9dynpyWr4%wz$z$xLIdb zbg80YtWAcNl-}G#m;F?@lCbNyBW6#@a}{m#li-N>1(YHxTEPTU82!=Eq9t(<83AcU(QIFxeF1N{9tp3sf}UH9U4le+TYRHQr{}m7h2_ned0iJ<~UM@@@nq#M6x%Q zZ^YWkvxL-i;H8~qx<;){cveC)DHx)nE{qzM?JYi|5o!vUmRdZ{{B}bNrXyaMtn0hJ zI<#ZI<#B5@ETgYC>xsa}O6j>>mOH5lApNr6YFNuRwomOW80IEXR31}15g3hvmKEZZ zq2zJrV{e5wcNT*ftFPuq` zu6qUJ?@>t#@F~M}J%bTc-v7~KTE-|+^&Lq)BV&0$0aZkOBA-*^(aNeEc-zk?Vn&!< z2NNIirHl-v4m+?6S_r@S;pRMb^LHPkI_{oX>FR;ay265W)AxYxc7PTY)o0=-%6 z29P=4?I5GzFII=Ps9`qK#xNiiH8RNCvy9k}_u(;hbsAL~{`5#;1p7SC&+;!smC^JP>D$gK$NFB@St%Ht zOKCtb-#dhMi9^&iXrEa)tl}Hn{A9Q&3bhfM>Y_C?^$slF?vmWv63q`RF|ikk?QYn{ zPj=KRug!Aj|59_d4Wvzk3hmWitjSFi7?Z1pAjf2h7+WrzbfIq6h^rl9oQK?&glAe{ zpw{fFfhr2bhX_aZUYjG04+hbx=h1Ehu$%6byumA?xhfBCCWhUs za~+ym#h%b`eY>Pgdw=KLGSP~;>zMNwl_C39m&BLlmoaXfF~&TYQ;KLK;AWqA7w z@djebY;4j;<$ta~wkhfT(Q+VL00==?rqK!{Lf-bgm&BJEO>(Q$V)Oo&G_rN!*QzW} zeu70YWo*F8eeK-Up$Zj~5pp-YUZJXLzs+Xfwp}d?PR%H#+YJ~Wxr7Vs^VhVRCbwE1 zzG^Xx-cR+lDWFT%@Z~irN+H8m9yb=Y=6j(lCPirAOjyv9if>rvxp`RCd;6L>eodDs z7`u*3Hq;#Omr@p`P)5@0VUCWHNe%a_b zB}peK9R$pks|UkLuFkybwrxAdMc@wkNHij9iM?Ycmf@zIYNg%S(S|s+x>n*IUdsb! zS4Bj1YFg2!++Tvf3dF6|BEVIJz^KemUM1kDYmV>K!`1QDvu2iACoSLC5Z}BfMU9X9 zY87-s0yR_J9P!>Gt7MUPVzQ&zUCqq&O+<>A`*G)bkqK*koO)rjn-N?7F~qDoDa@)) zi3EuB&+tl0w>Tpbystq{pbObDu~=vERjbb^NC)ZP$0F!{R~961olkNApi<^#b>>;( zU8)Rcb)OdPMH)bjTPA)aMyxg;WULcbGS9ESgvS^x&20jQdkfKs&smE7Jl`X3L7%a6S3*D^`Dxh;8r$(BHWba6PKw5 zKBYLAunNt%$=FriB9zRsdDE$v%~KoMrftr;DCzJ1P7EgTjETUr2om>yT&*sWx5{;x z8;U$*m5{lJpBJKT^OJvg=>#^b?)+>;jYXY+WwIH!#J)n**hmR(iR8IJ_{GKX-IwmS z@;;ST3LC)>q8AuI2IO*St+DjuY+vz|RP31EI&$M>8*KX3!%4phb25LYpXfOfo;050 z>|(!>Q)(yCYlO zM36XYl)J*5NX@fYotoOK&;09cUe*-+SsidjO^4Iva?R5%-6)aMnj#s!+vHlHdIcWO zsks0%y!K(;(*789RCSB{2&LE6t)G{I*ylZoX{d|&Yb%Sm10c0r;&p4r#s-g$ORnHSYGNq@GxnzcUXR>2Cnzol5h-z1z>g}sp)!SB|x5YD3Bg@P@ z-uOj-I@-nRif{VTseUHH3XeYyu z*JTLM>{M~CzZg34KvwNWqHY@CKMb&BTsg-&eXINyPr~}H2#+> z6GSPVfhGYnpoEJe!#!p{>?boKV+@pm^nXqhl*x56x$>3pT@3ZBwLKm-(_Tm#Xzzgw z2~1JA4n87=MsmV=Rkz z`bdmCn&i7g0`D}qox3LS?i&f0DAo|Ft-e8dYWNfpqt&SNNg81s*pO9@&uvVzl(+DQB(cv zEOUc1r%Bh8@Pu=9c0B81py>{KxdJ&8V)s-IQwS`RJusxy4Q*s)Qnp%xr0JvveYK0` zP2%=$p)iF(V)S@fY?TL;v8cM?4Vh^_XmjsXJAg?{L*mGoU**)e)s~lT5@OjHc(?zh zR?yU;ij-M8O5GP8#NF+$a&>nWw$g`MH&ok;Z)KlMgF!dvC4ye`Mp7y4JqwKLadnBb zv*g>dD7u5RSr^c?pce{bZtN23bXEv8dUm7pT}yUpo++tlAmS4x5;Q0l_b;7)YE8+4 zo`J9wNT!S(qw0>1X|S1OMRURiMVh#%P+#lCdVw)6G?_8YAhgj>S+_9HsgW20-mnV*$-G&Q(&kygsdH&2XG@NMVjQl0FD&r_psG>|eCZqk z{1rk|%&DK z8dT?;S7_Fj%&pcrvO`(y*Uf4T4nQTY>K1>f=ze0n`u{^;PpYK&ejLksV7MMRvd)I7 zJk_pI*6(--LBiKn=+jQmCFbb*59G+!b)5V!#Bz@{E$CLbmTRs-QVFTN$CxVq_$nTS zKE7NmCV<@NclLR%3U?(4onM)ww6vXv&~sJdS;;Un-czQQs{^86BgX! zUtrgMbkoRbT=mR7jnGEDu{kt$uR5@7M=+Z5E|ay*d#^)=u3&j%tQZ-os8Xxulot>%6*D#%lE`BwX`{_KkAh zr>)!M9P(D0zx}k)Lpig=CdU7;^J99Z>{7(rMlz8p_@>@Hf?Pd4;s!u~e5tJ@*;Pn} zmu!lu@C%3|BOE0KxDTzsb;2dw**Mp+wRdWWDi7M zI!|z~qKnfz>1`DOnXYz!b<=FIj;8ZnK+3}K5((pun6`*+i9FcTGhAJ-KK{fuR` zk`{a`B3x_7GG1QiO>1;Cjco&K=#mv-g%j=0I=A-sLy_FC?XTU&8t&_A03yu0EDXkRQG1d3$4= z6(8}oxJxE1=HDWa_q@;@0I{s*BOBlYx~nQ z#HwDeTgly1xcJ(B{k!S%E;UNFQ+nTOXmdnVnarWWv-P`MXW-R@fs_LZTr?e#_&NpC zZXpw)bCDD?!(555QBi7fJE?06BuoX0b8{K73uCrCM&TJbPMIpbq@MVGhYBSVC@jhJ zejOP8>ibCY+X&KCQoGH*MY|uXsHzI)!johIV7J}g_Yt$ll1zknkc6F!j~l7!ne6Io zo{Ybq3P;SF0^l*V$JzYlYBG|xUuog*J%{~Z6}B=Y)BkL+!VD6_IZC>_ExqI(g@I*k zj-jbgZ0DrBQD%0-oe+SC_}Ps4H9cCdnKW$^$noD$TLD%K%+JkSu8~_7%ot^HlKZJb zt0`R-h3Dxbn=ym*8UY6rg)r_eS%7K0h*WE)GQulqZrzKc?50G7M&V*liLWJuv@_y3 zAVh95Xq{;%c@O}>Le3Ylc)OBrm-fby=|Lt!m~Z z6SwgX-H zuRq*H`EZQJn+ybfXVT3t4(lZ~Fj@eq6Lfp4X=$vI!yVY?occ@ot8rL}a|SI^hb#i{ zckdEdm&1zIDeOY6h7A`is&{m(0UU=gFT*D$hVqb+ma8NZ?t1@#0!6qQl= zL34{;{xagJGqz5Ujf_6PyH+s;^HPy`GC20yF1{_Rx_xOd#or2QHjbAt;J?Gc1{M*T z0wdgGwxrW~y>x#;e~bmc5lq>mADu-heHlN^zT+s{m~q08cn<8tUAYm-3f=k50uHD> zz((VmMglpn{yJbJbcOE%Y}J2{e~;s|(A5V*M`}@4(uKxb)*70q{2y5ug_1wSo~?2K z``}=Z-}j9smvBy?vx4>&;a@$O_s$mZq+e@XAX%cJIWaDbVDnrum9JS}Uf#w0r<^%a zuo+1<7~-FfP8Ox}{5rMFgMl!j_M%(p99cr4mY-jA$+J!%`3a>^TeaQWoWka@R9H1y z;>v9_G}xK`;f<83mx#oGkZBf;eE!l}!PXT$XW2Zj(tMY%B8O~>WRpyqw648JC||2! zn^pLg65 z?jZ3A4nS0`LgV$BA+}yg)mhE(_#bOG>RFojNFoRdz$pa7WLlwisi_nKnie}LKLOH{ z`mg^NZ8V8+*OuM%7NNKS1_nm|gftRKVL??z8Fui8W@&HlIN>?y;*`(nmMJ&@DnbAj z4gd>3fCYJg0+ZkYMUVibH~>})lsdaJ0q_g`=bf|X4IueEstC^t$WzlwGU`#GJ~x3m z`lC$Dg=$0pU*vyPJzH`Z+X^))vTQjq>#7fCy1>(GzB??d(5EcQF&WvAD~^?%bo41h z>vC=yRsj+u>IB5OxfnF8$zb&4*}dVdMBaeOFe^f2$)iyc0+wd@ZS-zd-6ZsU!&@fb zAZrHuYL8G>9w#S?xliGZR_3$VWbFT>e?ZE1cAp0JQC@O*$l`j~dYZK%5|Uc1NLI4N z5Hd4fc~*O5Z=gtV5@0q!U1Mu~?&G9(2K^`;~P_djhMx<445-!{s- zF?{{59l!6XSC1=*J2}WoS*%L-{w20kxVqwu;zMTVs?Vo-l^{{4<6f5K;n71VaO)xLyP} zStf*tQg?{4nCtfl&$Zx|MyNX&s!B*>V)=N)nk!qtmu~^k8net5A6a z%Pv^EmeO#kxk$R=#D)Aa;Ts1})!<5_uSdE0GE{{kG=mgI8>CGYH-m9n(ls9}Z>A}I z*+eX>I*No&^DD%FAf^k26}noNKS~38voQ&S9|ZA>pm(dFo4<;anmEN zqwi@lA-pXX?u92st+Zmxnd!#t4i!Sr=(JtoYYu4FMTXH@vWzJyDV&t zvDV(gt$v2q8A0ri)Dil@8g*M6;+{hK7}_}S2)|K+Y(xO^xSSVG4FI+|M+MM{6lVsf z$8bJ3Jc9%RNJ5?DC^AwXh>!+GY{w_bj-MEMXJbU2M8T^W5v;dJV-xod?6S&H^zwNFk%cTttz@g%aAWa+P3Sv1q?S0t3Wx4vITJMP$D?%O0W+<{n!%@!c;~j9GB2DO)*ANb5+lm;i^50?bS+jMO z>eL4=5v@dkEQtU3P?2Du4HOWvPpyPOdkj|y+G7~kg?tM?mm$BjDyzsLe2KEo5NSG6 z$B_jxRUxWeljt|bo+%`yXr>La4e+I=3aL%OWc_3O3Ky#ohmS6lctjHaX`HJ^CG`tYe&e5HIpf^S&EN6xKg6So=nVPu>FM!kMW|KH>}DhLhsUO zOy*#SR)lkeuEr!x3AX9kNU1cI=50Z;Wwp8U29yTjEM?)+2{;s?l9*KqbyjiW{vwD` zJk}JfWC(2QW}Rk&;Z`s+neVR>&0xD&x)D8Wf{%?vx%wF#t!CiEN7jVgqv_^N6huGb z*wa@g9{tz`3Ff58{OE2q#&$D$GEVT1?WrMVy%Q?sf;DKAW1usizEn=#YFCZarl|yu zvTk4O$TM+nmM~jPbYlA@^ssa0K$z*TrfV`N^}?5DpH1o_+DON)c93dm`tqvIwaks3 zVt&jzzGjicj~wwku*#Hw5nTJw`qNSQH zM_+FRIfAhkxST_{3L0(Jtt_$EgKT)WYXvT;Q-?B%`+x?uy0V zCLtB9^f|4Qj5fnneM3-&SI!a0>mPp=xyg)Ot33_N#*~7n=ISMsPLEE(Q+R@g7EUbF zHW7^eA*G(}mlYos5ng_r?AKq9m1p;g1j$MAl-$%d?a1om0{o#kHk*mja(6}0Xu zLfm4!Iec$sz@TJXtvJ z(k>pJ7NV1T+ojo)FSb$|zE97FMvq;);YU+J%RWMe&kB>VewlNG4{#A%VdI|QN_0Cn zT}x9EblyrrBzbeHgTP)Bo0S_z5e*G3*t~7iB`4D$E+a5^F0-zw)>x;rM5%DmmuS$QXL&i5O#ukV7LWu{73BBuTG^U0 zwAz)KcpxZcm8Jhh2HrAT)C5N&lZWy?GBIvx&NSkY2roR+3lDv11lZZ9Npn<@$e zB(zkL%YS5MMri!#*2S`>{Uzq! z*Yw0ffSeGj9?NIcQ&A&oNo<2iN+k$OO5rR=D)=9a(MzY2-NP}!sNlALJ*VP4Gi!PC zop`XBf)eR|?nUS5B{zCf9~Yw6kdl|<fqdQ;&uF{P4?$P+Ux zGAa|-)d!%Mc*LI)UA)eOZyC;hNCUy~qdV)#4Ii+KPJ;GzvP?QNSjO^6B%{0xN9$>1 z8ShFRydX@Uhl&$M34A*fj7b(NKud`N*U0+x91Krc7JU66lk%i4N+^kY1f%w1_{MuE zgs~quge+zq!-{&M;;HP{og_&YQA~#_f@$6XgR)7Ft?A?w8u%N94OZe92@s3^>JcD& zNihmVWCVkN$;e|2uWH6cNee*;y$r*}z)I2`L;Cq(*paPJ^nhm#=D&hv(VQ;tXe3ER z%x3&c31V)C%!JY`)}@G@QwT!r69uXDEYN{-x zw%&MXw7@0_hC~rmF`3EblIs6w&CO-F3j~ER#D6orRPu=hAV@eMlT_Xh77Zd=UyAN8 ztw5j&g(Xxy#As#neG(#NUS*(G0s))3Ej_5a3`R<-IfbDcSmrg=CN^U1KLemlu-|OFOt3V+&lC;Mg}~Sj0W%rYSGO`!Ja*w| z@btt~HzF~Qqlu4=hAOHaX6*|3P4f4lEskX-ylS~k!ZJip3wb?ak;Zf~x!Qk!S4r_EOUkCTFkn2I z5$!jPZ*@}bqJi9^mC-~!92jjiT(yNt^!c@lD683)!9c3tXQgm+)hHVU;}SxS57S!B zO0)Y;vsJOHCPyVbOs;!0p-0utEn2BQ##W|JOjQD0*EQ?X2o4r)3PEiekD)?;bD3^g z7S|LVsv(8_i*IiA zv!(dajHc(1jP1JOu_0qr`Oy2cu)yeK?hFJXWUnmpC1oO*Vy7`evW#eY+9H^0>asI{Ykd(P9+&dGe^YeSbDJfwIk`EB3K-gvjQ(lQVI88zE-)a)K?0 z*Qv4Sq$eqY6eR6x`tq^qd$%I6lvBL$6tjE7pTpAAp4*m|@7K~VM|~Q%G5!RQM=PQy zm3tKoE$1GHoP8HfpIyq>rqk6i;L>mZF8g+*N|nyUewoKSj9-%5$}*B(;)4~l)S8ZQ zje;pIfWh%sOPvXei4hVM51kq&J{AcJo;(o=3!Ee(E-XZYM^3y+>uH-~U-nNb^uNWN zq;>x%Y*(qq)V-zBt|Nye9$^H`lM*6yU~DcfCN3-#5)lv>3Iu}#K|ru{Y+%5ct(_`J zh=KE_*gT;(Hm$`TY5lIabxNf>w3Nl z>R;QjKT0ij7Kp{9?O&-wZl1#bztaBadpcx+a{{Idqq9#CVe?}H!iS5VIND6(vuh@~ z6HTGrEo|0^T9DGOw>$H@vs1G7<>scObfbl;Ia0`KrQNj8iEPlh6KPs}+etQ1VDPbF z!O`)y&O9*jCCCaJ6;!k3$i0=F*y@!%u~vg9=QyyU+; z;6+4N%993Gs_3|pVx@$I%NJ^m1xj+!O{iOsBmzo|sHkefKR(JmtL$f;u^BpN zKj#(*Ug*^IG7uQE7;QP~)D=Rzr|?WK>peN4WzD@frDsF6&2yR6%=y`e%NfU=VnB-t z@1xbDYu7LF7k6kz(h@|v#p7Z$-bM>Bt?>o14$)*GrO!i7Z1pXf3oV;n?jJWM)rm%3~y`>6`5++pWsAV?x!BHDsAq8Em`MPcS)ndS25B zBV2BqaJG^8BWf{zQH=78gCqBdgo6>pCba>4jhUX*EH1?(tANCkAINMoh}aT+M4vve zhIr#xfdVXDccLG|Yws-hj_V<$mEvU)i?A|i&>hQX6;`&z?*iB(n&nnK5v*aGy_qFG z)E`v9SMR%FLQ9KD;gUqOnF4o_4@R=d@YhW%EiCav1qNu9YFgBmiT{Mf_b5(s-sQe- zi*sVujt6?0v@-&kiXzg&Rc1iChz;*mZ)Q>;>QZ;lRihDg6uLK+3V{XIVo;o|X}c0% zRQ!V8Qf_|K`HwfmA)u7r&-H#@l77=N;|FIP=RCC)%M1OMNQll?c&%WYNOzR@0$QvTzS}}i2ff22 zCk1?PnG2Lqxa@T>9V^2!xNO@MusnG)|BN(C8$>9gfujbpvjn5+IwL;Qkg%UTR4+jj zWC*hv{1B#aov5jX_>gtdD{`7y;v z@A~c|8fr*?)+B~j&S}TrK2J@rS4$aF7`fs~P~vb_{1ktK1uJ{&1J&52Xn=FW`LKQ) zoxO(Vpy7I!FRhtAw{pB_uJ6}xOD-L_SIIpuY)|8<@*r_z|0@sYO`A1`V=AC@Nie3a ze{s_FGE}uTuO@O8sw)*PdMY$+E@9jR+tM0b9?LAj1IPJYPyV zu1&WnpKcMYKs)-jUDqdJ%>-zpBw?m=xTS?)Xk(6;D-yCZGt>b&MZ; zUs&dKG!hV`y3KC{@byc46hz1teYs42xQyC^RC&rZsAqKhAdj0A!6Xcwoqg)$$>z}% zdz10QuD%mrrHL|2jfqC2I-7R{DNTCFwr}aTC|JEO1~$(v2H}0E9ezxKVWnZD*9|A@ z5;9&*GkhgTgN*lAs{Isa37wa^^^d)z6iD&g;gl`_rOuo}v)JL>EW)cjrU~UPa-*sc zoD^km#;ZVJ$xz-%z+QhgjL&y!_&%1E<{KN?N?OoR=P$RGrkJKomPpEXpovaqAy$6Q z8yb_z2R@b*(x59gF&pMYrs;Bjb znF{!n|unO#LxGMeut*n&l-;#cobGOeet(T7ZGQfmCb-}M;ikYBTT zbGuE%E7pw4(#aKn2EwT7Skf{*g0lqs(ga)D*sv)mOr|Fdb%aYZCQ;0LdCqQ;6cjdn zVQU*6zHm{-wLeu5{f3S2V=L-+NgMj?rompCMG(s#rFhxGrs&zZps%ulE=#Z8)EdWS ze!{FJ0?9RcKyHl8yQ)Z67sV&vW_vXBbyIQ*_kAF7mx8IOivRlQVhaT=zWT`PIb}Qw+>M8P)Tw1dpes|no@q~ziEfmtc7>qM@j(+Ml2lO0Lh(N{vHbJgKx>aL-8QU|1AH3m8h zYZpir{V*R)A9I{jBPMH{F;ZdvIj1JV@w^|*vG5UO``Pdmw_2gNjZ1a_TQpsDja5Uq zOzmBI+0flfrakUt+Nlo8AM#vilYmqq((AE{asq@@*!}205GqMS_#K^WVuFrsR7&&eS%CiLn;O=AaN(IK%X{ z8wM8Eur;wqCkGLuI-HRI^o2!fRl|We$OzyqZ%q|}RP%kcBo@I6zRN-f<5(o} zW6oD}!TmbELTp!~1=^8qK(xnV0?zy1iFIb1^OAe(zYA@btP33}2G|gZ(cGFhbCz?q46OQPi6%; z1LYL)xd0Px8wxYmKyllvR2|XmtP;}&L}b6HLuQF2Z3Wsug)F%nY7Jk9-F)?u!9)_1 zD=mqPw*OL)t~fK3ogUtSL{O^=K#UuLi>k}CSeFZ6vxrU|t5sjP0PJcXLuygQQ7lgY zLO}u_Eels;e1ZwH!;T4B{1_|H)25Ar#X`)~?=cJrcScE-P>`@=SvHg;&ZH#FqW#w{ zO#CWfa-Skf+%52w89P@(PIL%kmO1zHp5f0_X!#Uv?1qahp0^!MrGJ(NAH=L@PB8V}(G0I(~_0D@Bmlp`|STGFzD+(5FIVjI>g?m42SFqqfO(Wy81 zL<(DlNsG{8?}q~P7kDDlnlTwB8&UBc{N;RAMwoh(cBJmf!V$jmjP~4KWO61S<%pm2 z3E=N0YhrvR`BUXKV);rIQq3ug;iHxxw=6?8&BcfvrDOT<&FS~k0bWcQx6v59<~Rtz z*^!$JK`lVZ#0kh10x1RfC|V%+9_nDrutRZ14<=r#QFTfXY~W3>idLJBeh73z}s6E!4C;;u`i5wmZF+M)v><|(E+V2JRJ>7_ICcjar>i7oEam-rPENe;J= zE+dYL|0_4H`AyOaOf{IqB5hYWvKnmE7F&LG1euw{BiuQVbG*DemWWX@Mb6R0Ge2=o3s**B)leMC(mqsf7ChETFFL22z-r1NXo^#J* zLtu$FI(n`x(0QWYD38guG(Zp8P=tZYJ^=xR)<3d@%8TjX;w{;cyVo05WYD7;{b}ik zSEUu2eJ`?{DY%wL&QdrzC7J3{lMFi0juK+Tu9h@U5Ru2dH(R{Ibery%0V2Y>TfR-m zqf#Fb#oXoZw+|z9E4W4O;&KFCGkK%tx0EvS7@?TTDnK$oH_{r@M3wG^s6*AK31lYR zsOKS-)>+BT$;SMRwXmWQSaj0E)n%6jA2V$<@GP(Yy3Go$d@v?O5*YCm5 ziOx@@qk_!Nj~Tcc+11rz-^JFQ^2}Mb)MCMvxu<{HaD)a<8`W)kNaL|vO5(~yY>+Oh z^sD9_NdjA8uv46UuB`FW^V~8>eB)k9)}rpgQEh2?yvt}p)fm)W{=jRKJ&5o^krrG^ zpxR^cQFOkuAD4d1?%oMWHZ+G~oQp4hLfFtdmq&3fX_}cP7DN!DUD~4Gf+aNG$I8UK z$fm?KUUAj(#l5bfF~zYA6kyPiIC3hYSbe|pOXR+Jr(+7eq?cv~s5EWJoE0furgm{{ z6vsqxHe+qXo44&!$Yo5y8-^~iT_NThj3qI7w&wi=RSaK#69kBOOd<4_=4i=Jg4Sf? z$$|uTKd_=7_=JcM_A%lpv*v!!Z-iC6xr$oET|W+SXBuRy7>q&n#rdZ;%|u32%)%%%#C~)fsqck{}LouJCw35@t`5vN^Hm(YAD>>Del9GT@ z85_Cko+uSM?zKQu6gGV3R^&?uyUVonR!wJ~x&5vAyYhdeuMGS7wlT&L)h*UjTu}s$ z{CMq#t&E>kbl9{aZH<3OkIq~1nTBPba4P^gK*ql{&!>C}4O1;fanNW;DO0Fp`UEDS z=!G!1K%Q6pDu~3=&K(^lZj2m0(g8JgN3M9p5|ZLZpXlXng#Hvc3%sDRY}%2bfU?{# ze=yM2nJs!*qG|UE!?3@P_Inq`$o{(1CR4Ei5)QQ$M6y}$`k}_!K~pI`e+O*@a4ghW zwANm&YyvH23$d9^UZa-=eoK_=>22(Tsn-h>X13&tDMlK$nnY@0)U`K6VFLgSXG9r1 zVjoKNdMR|{2?zF_P@orIjzk=^;pCcc{juo%S{aarvZ17=EQ=1Y90tG%$(Ms{Q}L5N zr_)#I&xs`QVZWo4c#yIcQyhkPV73w?vc>HXpfT3?-|&23Sv60fDp zgD*rjpZgtCca=w{W1vye8Ef8fj&Z6Wli*tbVKNBBZ{5rRC_qzTI5R9Rmj!18`wyrM z;5j%idt!bG9DrX zp$u|gcrK6OOtH%Gp_V0EmKKy`*=BMfIrJQO7OII7i?n2+2hQCx0++$bWi>?1X>cdl zB<#c&h)sFC^Os{bg1tprBA4&9Gm0c>v44}sA$gEdTyQNWpGGRczUY2zyr^$T2@7#d z70)jdwkz_dp1L5l(!fqxuD(xjE(}YFXEN=ogY9_#VbX6)i&(5SjJJvq5`4lv<2)z- z^8D=v$)pdqHX733z>x%ol9r_nMvj)t!CPfRM5j*Sl(p4+st=h)4*@laCgOEI&xS-N z^jtmiUJL~pfT+MiqGQ(}7z7#$TGN~i^fo-BqL;>8gKhy4%vg$Q?1iL>Qd`EQgg3*Z zu6mTR#L?3d3_0(2?5PR}y$N*eyQqxWe7aUJ$b(3b`7e|RaW9yQQWHrPUu9o6=w8p& z5>7x-TbxZJ9y%FJQ8~bU$CjilO`}98nXE@y;^8=AG#66Db$iO#ss6b#r4;IV(9a}F zH6lW|l>v{iCz>3)O)qpK{vg}SZl~xP@+aDu*|TEM4_k!x$WW1h6i}JDsXe9z-HN?; zS31RPDh!I8G@~V(nYvF8bw^JG5Z306rSthZKLzl!q_^kmy8erC_Cv$+nC)#$H@}gM<4X1Ud98+^{f_b46Ynx zXYiZrCV5M;MfQ>RI$rdg=zktX*WmHT%&ia`2u>kgAtz_-lnsTb$3xmrHsqwpL*-i1 zCGaFPOo5+75hI=7LOfWITSh{z{xfuvlcA2|YYX2cg~;yzR7oTbl7ck1&OyM#J4t3g z4Amr-bag*8h>*xE+C0DDphtP)Mp9S!91yhErCbD2(FOfuIjo7Eg-45x2nb4;5on^H z$2U+5LaM7iJ+p-$qjRe&teIOTmy+JztU8Xnfu&s1Z}iL1ehIQ81*RMDROVt{rJnbr z^4EfH(+b2l`SdkB^Tf@>%K)74rv+wXKI`j|sv3gX5mRGks?JcFyIJo$i^FJ?-@cH^ux3;?T3Jo3s;!Ixt@@d!RI#w4 zB_HVaEj&1F)0Z+{;3=(@-MgU7q!O#kEkOqZLrsGf5|_PMN2dZfSC=~0LQfHlXCYPY zh5Rwve!cZ~ltF!PZO6B;8Xk`m_kCbc3X#ofk!4V(pC8i+=adZ;ls~!uT%H8Mi#kW8 za>a8PRorO_s^}5{pW73Xjzu1d4hwuP z{Z+wr0Cr$j`=<+&DxpBwUTky9?0w7oJDACYk0~>H;Yy^gK8Zky;F#rXhD5U&Dn^Y- z;w6cKzGL%hDIho@ieoMzc9)miI2ppuiDDptq@&Dq=SZ%^I#u1ctqd9rH!Y!WR=-FS z_*D@TUvC#s&$3ae@5|M~;`m%(gL35Cesi6tlz>8ZpvrpjI7kx(2#EB6USr=#!0#p{ z*}AsqyU5>8R0LQNy1DVh!6s*C3Xl}Mq*_S7EJbObl8!Lkb8(~%fJP$1Uci8RXk}vF z3;n#yr_h^Un&Vy28muH&(+e(=%%6J4d7}+lTdY_>wP! zz}5g_(TeHtJe(rbV7uTGhf2VA5N4^gV~N4Z;WX{UakENUABB<`7N15x=1EL-UW_Pu zB0hd6=JQz)`3dctdn9Jh90YL^K;|y1I1~>JDDwSz$3VoGbwOSN-BCE7P55%QbM5sj+l2vbkY+WPF3QVjU%D*_RQK9Vsu;vfkk2@|AfyJN4%{+h{iSHiCwG zJT8O6BLS8l#v}aD6~Pi-Ch;t)xk^i`m>Ft90x1ki9@HwR)S`wFx`PW^jhSE;awQ&| z5W6lJ3lS%c2#!is1lD_Lc|6LglXX`2E$&N&S6w$bq^M1c(F<3b>}@xc*=-Me1}lYI zjF)MTuy5@*_^T@c`vzlCJ}+WUTVMqf7b-zc=9Jq++ifj!lCWx-@=Vo9 zL;IUm*3fp@W(}WaxQVcpQx(u2d$@eS z!1iAl7CES1fRo`H5ELDMBxMPTc z*Xx@YnS$K=$}MRC#H!MqchaL;dQy_eD>c_|+@^-epYB)he7TW++HTt9@_Yu(#WLW_ zTjH%|#C@|6UWYT?kx2p;ygMDj^LcjtQ@YGo1Fx*>pM*ZFg$8N*2>+HiCs(g+A4&!S zcKdBTM|c+_4sE4nTZ&IzGC9ZFE2J+gu_xwK+(^ZcU)`+sr}1en8&CpvO#m|4mm8dx z&@%xg`f&_c4kRoLqCv2t$jg8+u;>F-OvNIV$R(Ybl(B?ibDcyj+1ZxBEqk~lx0a&M zo2j1xf*EJza{my5fafk(wS7EDROVTp(|l->&)E_d`Q&Nyi^-@yrt1(h>rV>f@2R51 zHZYlH)ZVZZ##jUFj&W5&e`Oqthkd0hvYl&_F*7wDKmMA~mxY8JS4iUs~at&>Z4qprj2-h9TVL3SCU`IFH4AAdmUuzRANV zR?Wx>7ipNS+MRH#Ox%l^LNiF|#`hBPr}JGSjS`n;r{8NX#xcrO^_h_jG9oFAX5hE` zq_Vi%ij{DR-&s`ojguC{M&ME+cLS_U&lp%(=^kdct6tsP8bkXD=xxmt1BrV2JA~1pB<5Dps-l-=!=!JvU-WC!1CW&n?> z?|yFv9WFwiN5gi4iKbwF3S+gDc&l7PA?1iRm3>dugCNCcCD zqeyv?&+$PRB7_Fkn3AQU{LNEe&UEDs6U;I~K6ZJZ+HHR>BzgoUBM{XVi2awnqdAVy zVuaP-Ik(P2mkelLh%>Fqn3Y%@hdJ^v{$Z^f!3u_0CILHM9_naAS)#?-i#BY%m>-YpOd;I49u z7cl~2V3n*zOdVMR5b~*Rl-W=BVV$kC>`8)+DVRgsp)LimB|NX6UQb>GiKSjzw@8Zn zFq0jC_~1x#R9of}U_YcdoC~cPC?n#wSscQP9IX74D^AuE@Ne;Zp(-Pf&&Y*{{f@lm z8d^J0#Lc-~R>O|fC6*+j+PREyq1ioJh?Dshy-%*T>WJt+RFtbHh9~NECcG`{N0da> zc#?I7!J0U318|=4hT25b9hgb#K}ZR#^fa>2K=|{M2$W@Hc4e%YUD=IS418<81I|Xn zDa8q-_P72p=18uxoRu_oMaTDa{4#7`TjM2B;|!8#xyv}d_uhD}Ohr7JNCi7>u(jOm!hX%gdcc5WK(T^ZCP%4ZS?PA?R`dKv+=|`5LEESnu+kkW$VvHqO5q zx?H*vY8gG{(>BCX_*VdkNmW_4Oq`CIyg7AieX4y1u#<(U(Ty+N6!u<`2}as zrrzdhv12IzBJ#7G?aE4=^MkG&n{=jPV&RMp+!L*{LoM=`-VS@DCtg`2MyMmh> zVsR>CN^T}lbnfuoPG} z0%pepPa8}i2ZIiRCE6^gGBU6b4pH0&0#yrw`zWL0Fb)HTm3PLxCJLKKNk-&I6dK9b zaqo_uz>wCVfnx{=iuTw=x{3`Pjc`g!iIIbrgb5xJhS7vBU7ED=R^!fjmDi0?-q<9? zEd(L*DgIW?1Rxj@Dd|oH3U*;JhBFj$oT96cK#a_k>N!?SLIlm}ROMBe)YmLIMRMQk zSoG?1oUbii)*M=TSE(ev@{5-B?vD%N-o2ks8Od}; z*qEGXLJ=8+LSdkdh)hOo&6)rT003j8fyVYlBg<;fEcVQ)Wox5m*o=sTLm?2%hGa7! zK?oEGh$4iAixXPoBj)2dpN$(MYo10XOiU&eBqWGPLP8LW1ciYz7Aq==P5?G4X+wNa_3eR6E$4_2-hpOxQ z$$m;mghpYIV=zG&L5wiQ41tg#2xcJ>k(ISvw7jmTae2q$i^94iHg=IA;pt2z4=$$hxUb zFH`MFD{IMn%V}?UX*Dl?x6=*JimM}`%43L7gb;Cr7*Wg=K?Dfmic=VccTzU1cGIa( zy{%~4OGzcAy`;5mYwf?gcQrXk#$y!cInF48j8LTraYQi25T_vu%}gODKt2yuoeL5L`Vxaunv%8rV;gvwWKdRAJ=B__S(weK&_)}=rD zcX_C;La?SFf{H1{9CHRRQ;sO36eEapIBd>^Z+<2hsao2S-nX>1o948XmY1b}>xAaj zM|UYmAw($V7~+f|qMRW{F~u>6b}OY*GarBJ)x9LO>piPIWv%3`q`@}0UTsfwFg*{P`zgdqqa1`vY`VFn-}1Q=o%LKud` zpaFr_-V^&`uXTDB6zWwL4+RS_%>cwWFo!83f=Cn~h(Q4mBm{tj5QGr{Aqa?wqERs! zT{mZK^PDn#>6FEy(nzqA1fnJa;Yd)lNsEeQNSGr8piB`3 z5F#KXfRI86gb+j|2?Qu1VJIbpVr8y1O@?NU83Tt0>^X);-yb^DRTCj%ED=Hwpb8QM zAczqJArKJ+K%j(#C@B&}l(pPwF%B`ZKykxHoY92vxGaU6A)yhNh{nV- zA(@24Au)-WnGH?N*}ZG>J(Xu(w7&}QzpC{ub*0Zc$!wJ_h?*e~n1n_%5X3-`Lotko z5)6YP39$)HTWJNPvE}Pad#bC_s=K?o)BgQ`S>mIU>X{-UF)&2u7Q69|YS35;W5h(i#_G6+O5 zF$g4PgxNN!bz5(%`{UJj=f!pB&g%=~tgmz_EZmYbOhiV;LLrTWLlBH47>HzHF))b; znGv~^ow`_sFsxnc)vEBZ{a447BbdZDZkaM-CKO0WBoqip5eOmzK?H!1ArOK=1c)Re zBrGW;m|7T&nUmkU4@29AgCWyn1PoBsl*%dyU{PSP2?T^8Ap$}WNF)&mNPwscAq2rh zN(xa*QpzzsZp}2zL(}skroOt#t6?!Tk`|ztp(ui3772pM3lvF3gfLwdh~S?gg_#AB zk&%(jJDXf&X*-!y1Tt1u5sHwQCM+nCAz?viDFs4BluRcQWmTlfqnRF?v$p*14vjNU z(AJDbA%r7YX;PFfB!$9AktB&EEQ%8pq)5dJQi-Kk6hVO+h{VZ>lM@pxOF%FB@%Ns*UmVsRLna>OvY$c-h5gv6MIA}E5%1%yy2 ziU?T=BoTm&EEXuBj72a^v1<{Fs?E6DERVP$sXWlAk>1NP{^>nw(xankCOp+uMESnVPuGBruoFZ`+ zt#TbhpD;wisB5WpPU5;%A}*uoCn>JLiDA=%s}rVIv$cs81xX65Z6bD8GJnqLm`?rD z={{K7iTb%&A6FdPiJBrLyV3BV zLq-W0daGPKFlo!yZhb4qP|xg?N;;^7j4XEXO$7vkn&AY6G<3kP9B6T24H&Tt5$d*i z4=Axmm?Tirgp_(4|EncYp8qy%M#T7EhR`o!yffpi3#d*%>{*TX*ndu-zdKK;s!6Sw zYAAj0kVJ<`beBvbTO^4TxurGEaY#LrOPsHm`6!{ODGfm`r6>PR7tzt$R2nB#p zJYb?uwL>SEvbU761)Z#`GfX5PvBZe>N-DAuD%C9OhZ+tK3?-d7!6AWR0$DzV=8mc4 z7I4wRSwBC?-{B<l5PHV%TdZl{}c2N)jn0CNYj%%4O@V zq`qUKkDS?s#tMxxTdGSKkFvsSe3R!5j|hqrD33AL8DSiE46Oa8kC%w}^e&ULD{F#K zYT#_FX}~1m{*(AzzDZ1%2f=I8pOkBydcVyIGxK&68Z1T;rzLqeJ% z3ZtNmki)?OLK^7uiyRUuaDv4NDQ;Sl#n_MKDqt+7ibe@~xSYj;?pL<@0n-{z_kL2P|& zu_bwgxS96h?tM{g8lvju9x3AuM;<6FVW&T(<^sY2Bp|w#QmbDGB0*9W<|3C+PY}9a zO49Y*>lJD(QO%ZQv!xv<=_T6lv(2rtOo=987OfOE(KqPB3Z&1Q_jT11X_gT~YIQ8B z+u?r*kT$chivB8*6q{44F4FEBY270^=J0m~WWH-Tjf;2WJ)1qICTN>4^psjWr1j+C z7!;5}AfS^EI9W>~U2vwMCP1zQKypWVxnRIZ0*E#o5h&g&n%#P&5Yp9KD`jqJNGY1u z>gn=Kl=@P?l~e0mOiR%1pRpWv*_1nZx8f!nC``2}OYr|?x=*Pa@p&`!6(Gohey36* z;^{|Fy*t$EjDAVeghhl;5qqg?+Dd75SN@wQM24R{M3U)74_vjHw-UV2>kODhZn#Xb z(@wQ*IR`0al`PqX-pfj?&y$ogrc$RN%&@ChLW6>ugrz|u0z^Ku_>vyyoUsYS7AOe? z1IG<0n3CKYI1+W*ypA=AbBv0f?OT{%BRP@gUD(c2NH{X`JAClApS zr{WhlyNuJY%GhZH9P3SxY_-iT<3rG*SoTPX22d(RElm)!1vnMQGU@2@AW;Y-Zr>Y) zaLh86fv8>~1S+LIT)hh6QZlOBC9x_)3j2fnmblQuMP0$E@=81+)epdwp8PCek;jlG z#o^}?It;Wfg|JA6gjs{^)E;qRP&h$C6vRlnev++Si$;^p_X|Oansni|8j!vj$=F-|mS_qU4 zk{5ZucLel4&@NCe(S9mBE#YLAKi3nyl&rBZB!Qtq#vg{a16IpkC-}flZ=WHIBcWp zK~Fe9mk3d+sjm!C#vG?F8l3VpW;$lqg_zNhV$k%2=uZt0B!_rNf5H;qQVI@(%MscPkroTnP$x8nvtDzOW`}JnIFG0es&7~WavkV zlA6gS#3bg<6C)cO*pjE2a;YgJSb0t@7bet+M2ATia7`g}1$b^vuG;5nGJx8K(0_Y= z?7yKl4fE>FdiL`tsDermflU-qqR$d2u}n3k5Yw7-4pqcSE<>l^b-hnc{}Ul9wQyi%gI-opln?PW1X*b>$4@w6Q(Kh>o=7 zz0fy~k#4dJP)-wd1VfprghEOf^ilp|O(vQgQF&8cojoruTrGEI7~$9| zpsO9$T#xFng*KCcO&pk>(q2bFC})zvYI~*>ZIM>6Mt1r1P=~bnL3CI(CDAkCBv9y8 zEPOGwC>+^55$7iu;N&Fl#K-nFmKYgX@yToBF$GVz^?a4czbmfZ?90JXe?v08hmA1J z*5(+kc|S}+H5*bnc|la9w8qe|$N8CeqUP8uTkqhGqdIbZ{>W}E45k)@QKY%_VJ1ur zrwGBcsmM5&z7B3gAc96lI;bid->!RR(fs_!u&ma42O>_8z--wZgdD-h`>7Gl|8bo0 z=|2^!^GOcLA*xy|sPqj6az4)jwP(F#L+2@tc>)MU+M5Dj#8@6{QOsMBmnZWDR@_*+U&~H#^5!VzQ?Gg)VpAz&XcS6NGRf?af;#x;wK;O}Fqyp=OkEaK9Fu!4N1E9?8s|)WnUL)e6X3mluHBEzcHB83{mrfx zS+H%aus4}S$=&SG`d<;8P3H|`_|Sqbn>Pm$its}|Wt+U zQ&Sfe-I5T0JuB=XA;%+O^5H2zVsq?6LFd?zbUKCAu1S;FW*H3_A=}(@F@Ky{q3ISD1;Lm)VJ+fLW!l3Gj!M%|!6oGd6 zv6VbXRfrIr?4K54gl+fEinYh@sYE52M8xwkw@=HWzGM-ApI131Y{ zH>*n+Oxgk+@8^}oVhyG;P6+am3IF6ZzY!YZYB~A{gjGf17HCw`yn26OJVa0wrN8Pj zr9**nJs!2mx#~13ng~M-<>mGV=9=Cq2h(@?M!MyMvNQ8hCAhm7oW$n=WHTS9>yWB54zIh6c_u zf;2WUXI19U6NNR7hAevK=*=S-$*!6kS2krfXiwWg%ATU0(xA~G+d*q6A1h}{Iv64! zY;r+}%^PJS`fd<37`Aab9O+v)8SO)Z1XK3sw(Njr#=yhE77*D;mt=-Mo8o%t~d4^f;m1&%Azf6wvB-vtU@Vy{eY_Uzg<$l^t(wunj zN%d@XOLB!#%;eGPL3Bk_C!<*20>Qk&aVKX=%T2h5EXkE`X2qh@Rb=aB)Fda9Xc3!h zH#&;Ig@R=n&*a=})09&IQuDO@52nM!rU7ja26bFi143Pe84jzYmMBit&Mj`KiIMaZ zO0kj4@8aCcF0EpirU{tspE_hv2E;*Y%=06PcUT`-b=mtJ?hX4J5NxUD@26%V>t<1) zc8Jpi&xl`beYq5p=IEK{AV-DGWS^`$@OD%(}%xj^^8gbWkyxM$$}d`(5;irH#gvjoielU-@he zY;=O=^=*@T?dYW)9>O;KS)hZBE-3DTrpdqG8LDiB^xa!(oj+3o(yv*jqKl0TLSJ1S zp^oBA*pzg5jMYS@gs7rxhy^9JX6#}ZkR2PJ6}`rcKsdRwTiw>?MFdZI7z)kl>Vb zPmH|QC0uVRE!=_vjP>m|B&`7g9!a;6j-<;`&q9_xY*tt_dfb-VwAzLx%8;!|T;1>5 zWmG9ySJH_AFn_LBhc-z6Qu+sj5n4n2VSi-gPZ`B6X$WM>(uZE8Uz9zEA ze1e|r#;EzjhOYAq&$nQ5{PQyx<%(I>cQKQ_6us3Q_K?+tDD_O3i5OgRdb9~HE3Mpu zE(AU(;~0SigAP?RQE0ppU7Y;)B9*IInPfT){TVy=;~eJwtg~s@Mh!a_)H)Iz(J|wL z#ZOCQ$i8r$uaq5{AuWtv+0xPg>!f{=qI4wSJ3~Lg zA>dS8(5sTfAqu#d{e()U7$?3S8_Q630{FU*9*6%t{;#u5} z4x6veKaxk)3na}(A>RvP1e(2t8wz`DOcvY94lUlV+`7{N@?Mg>iu5}Y1`3*GDGJ5% zy^5R0p`2_~-AtmQCts^_fNFy(m@9qhtw}&RAV+HxHl7=NDm_f~FCGHgtUwHoOc0Dp z5d{zynPUaoj_EXK8!u})3d0_aoDT{*szaV)CnlrllPn#G;d4!9F>e5W0XDJmnT!)e z+x`N)y2&qK#rjPfOnH zmvmy5nfgY>9f63MHY$6h-gYo-iqjK}D4<0Mx)85LJ&MPsNIm6_P0pIsD@2Zb9?WDX z)p+ITzh*HX9aBA@nH4e1%|;Uoi!Om1ZGIjX%#eirAHq&d7u6I8 zj?{}tvBiJaj`h8h9p%A)7SoR2>W{u~ZcS*pgDmHZb)(GefcWIr; zV-u<=DSljq4ABvyftzd(S$MXUWhJ*9!W@a_=*g_G$>>gT_?tV-gqE5PdiW~h8=G#< z>`LvEJy%WmRy1^BB8On-{<$}$UuQ+|gf$ZB{*$)MwA9fh84!g?8S&0t>-4RnlMD>c zF}kH}N=owbe8Ltk>gO}x>eaLT`oqH{TU-ojip#c_vtNd60G|Y{t$gff!XnU2s7kR; zWReI`PTHs9ScBD+%~!3tXJaGpxdv~c7c4#~Au8mG^@Lf8L5nBoR+#EY(~`1=uFKxk z>tm;X{_{Y?JR-=q*`ne%JiSAV>brQVg>$iq6^VK#l?+#R#F!eVt)(*f;mTKJOqjZy z?pCctr+x*6VwzyWdnf`1iDz)B?WAimXhJzqPHLi{J}g92O*aZ;ITVKH6OJoNa{XLq zx{1?>7Qp|d5v+yV(_+pz^J}@ZaId?YaY=F%iFyJnhxzw7$+3&$)qMJN@{4ZFty~F( zerTpfi=s?jV5f!F;|A<^iYVWNDRfTsj_`_9gWLUuL;@5Sj;!89{?66*3ULN3?4nbW z)aT|<6S=e)b!ANAjVo%GW+@SXJqjC4Xb~Smut`5hxcGqG)mK#qHViHKb?B09Q`>zp zazSV$Wc@om_9D`u%eE;=4e>uDr$9(+=5u>u1uLc1gorz1^CCyQyRAIt!*xmVq;iAYo8QqDo>(FRh8y~sVHbB}J`Bhh-dA*S(}G;W@ZORh#`nm8>eGlaraov>@5L)u70O<}I7cUu|E zVas8#BR{m*L!@k&;_Q}L3fLAT3SZXrPn99)RQBmyN$St8DnL4qnok>zJ(v`K9o0&1X0 zMCTbPqPGPcMO5}f2(5biMe^79Q^XZ0LmRR9zG%TkB^od})!v>Bz!>tWepL;@JZXXK zNKgGyI%wzut^6&LRTd-xYEIXf)mc72O<0MQsSg9SH&)}x8wsqt?28)(vPk>#vd0eA ztqWS5MeI4*EH9KR06hIs5iGya`J7& zRG^P5W>}HDv?s)}SqQf81tA+6s>5|eh$Sh2+lNOT5_qNT^Ik3Q?l&shiH;lCGkobN*A&eoL);EHKT5T%aRR~;l+}Hor+T!m@=0V z(TtU%v|G=6mPO&wQe5eZsFw6gMkxZP_Ted2l0$n7i;H{b`rOUP-+KJ#av!bwKXZu| zUKe2d1fwDzak?k`Q>mI}!Id)g0U{w4h3GE-Gn7i2L|h`*7R}i)friw+vh~lS=Toe< zq{@1%t7ghpJBnaeQ98h%yN@p8&P5O@M0h{`LR?G*F(McP5JccscYRdrCW*W(TrsjJ zBxPA6YcBpqzLB_hRqxr=(b|Ui!PI_R;3-oVJ4lz{1~!Usmxpp@R{Lz1#?z=bHp#9gqx4!t**V3R(3P_F2JZLyZ5Ms^i$xSR{K}nA*HXvjZ1rrW z*PN8*BFOZ@?aMnRgoKiLh?Z@8`-(koLfM~mM#CLcN{pV{^RFwkyU&z}PO_JzG~7)+ zi4Up6fTUALl|<<9cZ-r=ZlmW{arU#}CKWfsbvsl1rGZ6_AH3ukieBUR1tYT{+OdqYq>x__rrWDB%}E zu#4Hk0*T-$A$ba<-cf&U-%Gpc3Bpc;Kg4Yn;?Ql{?(M>oAAjS4M`BtjOWxLbO#H^R zOWviY>8Q_>zhYLjR%p;q-SKaX1_C@intYkm+NV;9o!#MYgEFa;VWx8ml1yZxdV2Wu ziJvWFBa2nPcVu(zE3a)P{IY`6g?}i+F52ZMNQnTk827d)?^sqzkWXZ(gN+$ zecXbXQT@~}?iiS;qy@fk-Jyd+qs$d{6HyqQDysJ^ zq}3JC!#bK_Unh$jGKzjJD!IAlJ{R;z%-(-y`4gBQGO*7yq2c_z^->Tg_5oN3|0+wV zf=7x}YEuW4Ew}8QubRfshh-If6|7K;o)ZlV+jTt2ZwcD0>oP?txbnAJv+&<_%<<-m zx6F~}O+H~zWZ|D3)}d>bb;!Ueo>tyDxV%zt{f&?~lVCmo@=)r3Aq+Dl{au1) zgeEQ%8Nb?g`|@n6o}eY#WoLiO=WeD# zdNI6{<_gW;hz# zp6X*s!UYp0?3HXDRxXPBw*RNi^05U2i*#{nRgbV4X3{2f;l=odvYlMN1u9zt5Q52b z$i5TXb%CUJfR(oQ^1x@V3*4TMfLfnN0Kbcq)eCvANko9`JO&t$bEYCs0XQ&FS>OnRV^?dl#HH zykjZc>>es_%``=?oXzVCa!;Bc@YjoZZ|}a^qG5K@c6i#}BE4<{qMEP&#^n*HHn8Fr zjh499`MdW3Jzu(-_f{WD|E5&BJTS>mi^o(moh&RS6rHoaS{|HAMn&$47bGc@FCjLx ztu+GZ-q?{cOJ-+oP)Q3kw5Fx%S#^aHtD1)`@|=vvR3##U?>ddMuuB;<@%(aqsS#NK zvuQy6spi)BwU(qrQ)7Ls_qSDvjPyw)ll4U-E2NA-mBHalxNN?wx9DKOoC~{IS>}Qu zSq`Yv6H5J4-AVOz{llEbAT5eq)p1z$V7;GJg-|VI4cMN;(8`?AMU1Tpu%R2t74eBD zh|KNJDNflE5OM44RYWx4@CkUr?A!VrbvhWWNKs>ZaSS?FQyQNa}U1 zzQ!v$`WcbCC*%*|KW^q1$v*R5qB>=i3{X46%A9;ic{rLN`D$@}AHYZKD^+5|1(P1u z6nLYRSnOr zosqMWwhfqg+OCcSXqU`b$|2l>5;4-YeSP-chZykrN0INuSW$#608c=$zn3!X+~U&7 z&tLudn+CP;siq~_7r|;OTk7!}a_i3f)d-b0{U5&+-c{dk6qWwNQnrP$UGn=V$mJ%V zIs~)QASj61T_!uPl@1_;oC`hmfYNo(mqb`}uK2^>8rlO(KDH<&Bj#SM|<%G6dnXJ?eBnEFt87|9d6lC~W5r;v|p%r0lCGLGwG zE8)hn=}#hzjK7NvOkBD4?aQX=Ed7aKPglKKje2+Ly~{yJt@CWDjSsX@ZG|ANz?5{! z)T`Pas*YG5=tU#;M1YjAudLG?hEf{rs7t37jlU_Ic<$NWRz+lYq&4?c<>Wf1?Z(VK zj^{Eo)T1i&bBvCL8Cs+)H&Y~O2o5bdx^320x{|AyZP^Mw3 zcc))9OdbT3^h<@}*85$brZ0+-qahxs+t;ef&zi|IMc5N3CoE(7s?rY#*+tn=6+7la zsPfnrvPof0?+M(jqJvn_MbAaI691^VFvl~sca0S`6UNz+V_M*#GMAmIyvi@aCr$-s z*Uj*%=Mz_jRT4zuZA*LUJGAubREARKBF3cVZOazuPa>XBwQ4`Wm;o%u^puLy>GsQ+ zsGRMC1?2r*3t!b#8e!kFsStYR+IzxM%^ztwJ!>yOdr}p4Q}BIoSHdN|`)m+KDYWjX zb`v+d1#4j>B|l-N;1tkHI=ahweWIu{Ep1_0Y5x<))w*NKicIaDJCf;3xi7=5+okx+?>)Bl&QX8BZD19x z6W3IBx*5UOy)M&VNaj`3GRqtr*aGPyf_2?rOD7hJX<|l53$CzMssmKFaIxIT3&V{x zO8H;4J4AjH?Tqd=rPsL$@EC{MPNJynIy|1YB^3hN&<*j#S6P2gs5Pl1E0h)WmUbsktj}yy8Sy!1~M{ zpV(ph!Yn@I_6bYIByEj3f(s7w6DV0OBJnkEYOhAya#O6qi z5jqv+XHcP5)RzKm2!*op@p`+#d?4s@XKSSw36iBX0}X_6mx$^6c>=tK=}>x{PqG4Y zf=UwPPSCtXxhFW*s_;k&!UM8AY__(m2x3?GOX)J^X@*zrnFj)O!7j7fwoOK0rQe^A z+Q-VO+Ec;(YL)Ak9k+Cj`jBNfNtnZxA{1l7*O?FETQ}(`9&tev~I> zm@=X=tV54So4iE0wcO+Qq7bq2q}bxH*g;Swp$Ksqo}z)b1RNs5c~rv8V~s5Vyg|Z3 zM69cjgtY|l9B9PE1W=l*hyEfF*lD0=rlP?nSgYo*glIL^w>Vo3rHsUPidSV#I_9l7rfoXuCWNBM8L?= z@6g_FPnr4{w9`&=+P;prFAW6`!rd&5J0#`bsz!l;s2dYV-S00*v2V^E7SJxK*Zy8iBOF3hE}XzddG#rnp>Qk-V4}H9_qLN!!#Mkfy~WX&F5J;_r8voLU+_xy z-rK4$-lNZN$^%1)e zs7&nYP5|K;B4uPZ@arz?sIzNApMaCI4~do< z9O_UF2#{iNd;QFT5+3NCS#_CAaTiICgZcL5eWcdFzTUv`p9KqEK_kHuMieTL<>6+< zju`l3wM&S)31~PXEt^f9XBH z^;Ez5^1uC$C;rQ$|7ExTvjydw6sw%PCV1lJ+l{2zeNd*1uAMhiP@LR;nN{iZb26T1 zLS0yyS(V1&8sK1|UPn2kX%%iqq4XdQqZM$3V-jWIhDW0oEmClwd@3k!_X8NeL_4VO z9@AQkc;Av%Nzi*ul`mvmbUq&VkW4_2_TFXsxqF~)>MakV2_tlD?w+S%|kMIO%Hy_p!b{cZ7TJ& z@e1ReF?lyS`y?Ho6457K^$tP-`8g*YX~8QMr8KCu_G18hn(O;&ujz>4Y)7=|JeaHm z2y~x`KeiO~3DKBBGx;<~R051EiAR6?LL>&Eq`k3eFX-h7qYcX{q!x;qC6`jZC9hn$ z%vUleylBoo_N_%Eqry65u4s`{ACbQu5;r%=YD@U+o?m*%*H|AD;#Fa6ldT?TN}o-1 z6N9*kf7O@)U>TthMKU>67+V7HDT$UQYSIpHQiPj*Wq}dl!XF|HEm~^r5Woo{(er5a zt5INZHs+K`Se^5jg(zYYCoL?a9pe>{<%NWg!E;#mzsi}PT3jagG!YCV{v`j!s3Sjl*rE5Gk( zoP}7wx1N&hdDp{uCB90jR)>WyRMxxEe0O(TRSGLnx$R_*zKc?yo$IA6)0SpY9!m&u z)f0noL03l9un4%78h53seNM2W3JG&+l%`yw8FKF2Cy3)(RSV%3Mp++MU&Ir%$IYYP zW`&p}>~&_pH>8tb}Tvh*`RsU2YBMvL5z*gl%*tyVo!OlG!*u z^8bTzl6cvYjlle|YOvKBiVC+TTmL zc`m$yNMwiGTD$#uzW>ERJT9YU*>%lZFvykmg^pyG_MPq}g~YuX6HIas+5A1G)Wa9U zGIc~KNksThg=AL+Y)1|;ULxUFmPp;CP?SJFyn#_DHOsOB}UIM2K745mo9PFr#1h?fOz0 ze^)A{H2*x_A6>x|cCFrgo)*oDTovWbYiC=cDq9X%hzTf$o`XJM3E?PxUk8+q6k{N2 z&3Tt9#NF_AnKjuwOVXt2M^4eCM@ZH8jiY?vc+Ojn=Q~e|n%7HHY;3h*q<-&Lu<9AE z-P+SDEqO1E%C7(S1&Ofli4S|2L|>$&^+K#rep3c;3rYDC!5=pSHCa%5FL%)5Fh+#3 zp3zGa^UPLS#6fZMzQ3o7$v8)hsFTuI-z^Ak%in&qJN583jK2{?(X=fCL$Tc-T;h0c zV~XWDS`&0Pb2qJW5yX)i@L(Xh)U|_PP`bWRRwBiK(-jA+;S~F^E^7<*k?WSH@Szh% z!>t)iTM~LeT@>eU53X;u)=2M#NpjrxO_x{Eeoi*?ci#BYAKz}rEu>JdRcUl@8`DDi zG~3dwM-4&nJ_bSb{YJQG-}a}vb%Y>vg2)!Z>^+PG6;mapHR15n86$8n!+v)Wr z@vRA{_ln6cLTL%Jkj!NGz6JKwr2fJU@l-j4(-OV~B$+#8A%&fSw2em~@cH*1b<_ zS9JE%7Uh1wnkZjY*EHhRHlubbCcJ>CzFfZd=E9LPY9k`1CZ}YqwrP&O$1#u2YBu}5 zwtEeqS+m>^G8CUJjziQ~3z4)lh1iuh%K}OEOO?l%tsLPl5?*#9<>B>@ zSMf+dkxjPyA;j`3OwkG{MWDhI0#MvBiehZ1D~+SbNQx0UAdP~V%&kFYbh)f1XitKm}Zse<-2_dfb%?5 zq^a6UI7<>?NXQ~kIYNoxqjfCO6g);D1sXs*X(Tfo%C0%)Yf#qV0U@+5o1dk& z<<*=T#cxzyE2Ex`af_}O@RzDNE%qlcMp_H5Og-h8MmMjyfG5o#7d^+Yaqz0 z&gWR#7Ggthayhg&soT1cLvPzJL_d0171;vx$MB*i{D5jl6i_h(HZ~35xR(TgL294X zPGC+|<0x%Uz-Wi%W*uNvRaf9IKejsIUAt^2dp<*B8CisQlANu zhj)ns8_m1}vcLuKM$WK14=?D`3%kR)7hXyNn3-h)U@6^%FZI1K>3U;3m&!=9aUV0v zQ46|yiT+R3u}vB4$M_XQP814bNJCBWcoKQ*6nIJtV5hY=_R}ws5tP|$C zMk1u{VziPTqd|Hwfd=ha)~LaEF|2z6($$E>t2`8@f#;-0#&nwcOGyDj4nhrr8RJx( z;t~OsDX-TQvYA2d;S&Ivc@lNwP%5Y0aD$xJ;BKFvKq|z{dCW4>fK=wN0WW}-uI9hw zYP99G2+fT@X|^LX!Q4P2wIJcq1+e5M;=_D4O_7m8hM8RNbhK~n>1z+SPp|< zN{-%cSglfNXjGC(0;ycds94J?ophli(6}vmN3g)%U7Z;Z`qv_9pD*;xyKBa5kzhbmJT@+yzcj{>_-5k)SB{0uclW#K%4%i*BLC5G&T+L5oLSQcj7ve~fcLf|yRk1)* zh^i=kn6J0dM2v3^(thtT3;tWT5v9Dc#xSZC*15OW1vLNM$6HNgrwld?mXLqMVQMT>+luiG}2 ztQxF3d^y25e=;%8MuJl1ukNs(I>-cZTrAKbCqHX#4ob4D_X8MD5)+$opDr!Q5pu|& zMYNZ)h#<~n(NSq{g~?1=aKU&P@3vyjhOU^PpNh(`z*l4%Jjpg%1hijMy2MnNy1D0Y z-8IBMI6g>+c9y!mL&Y7kuM7}L3UTL+8pub97aX(*y(Q|)+mkw#qso!M~ZV3=Oe{P-aCux25OjPoS*g7CGvchxwe8xyN{_W;>l*FB#Y;j z4x=t2F>(?AZ1A%Z2r`ugMZyz>D_hT~szgitt;K|pwj<8B{c>@yIF6Q?LbWq8tj>a} z%CsViLR^dSy~Dc8cdHnf=iO()D@a@8oXalONK<6^W=O9698EPnxkUptS~KF48iN;? zZ%4C0lqA*3s&BdPdT$Fr7mvhMbT&(6C&OvWUokY?Qw&eP2u#N*wQpULOM%Bi@-RK( zO-pUo-Y(w9slud$3Et~UQlx{=frhKRBE?IQlx{Rm7^J%BFHhFx^wJ3@=3&EX@_Z6< z#L>A@T!W>mr#DgtC!m{K^beAtkP!uHpHI;suHAmBoye}9zP$~HE#!=em>2ingzV*! z%xwHq^E@O&;#!^Mg9e~hKR#ZL75b{goc3r!krDgx8Cgl98 z?up+^?3tWHWig!nu243wIa)8V(S zyn2*2L;Gsf-+s?S!ILkH2IA2usYClJiZ55dOEPgA)Q7&xl=n3WVOuo@qiB(P`}%#4 z6$xPM=&~hb-X)0!l8xa(Y}m|P|82m}Sdf;V=8}-J7W4URy56r>(_`J0(kIEM-Da4C zS^Osm6xkSDHJ2~aQ2!mr=>%z+&P3<;^}p3_{awsfq?Wwy%bNJPq&?@*+^AA{2Fp=a zlt6D!fh=zd@Qw-ajTc4vrTc3VA$P&E3 zI7_k(+DsP3`I>Et5sWSb8LnFw1Pf7$-6M(u9E5kh8R@ltQyh+Cx1Xj-@MV@J8BAK2 z#c9ANM$qOSgE(-DQ?S7xo&;0n-uWP34V0qapGH$vO>;nUv>7~#6*`tdAQS8|SiK?k zF9%>lgb42$0`OM}aU3K?Xs#6kEZZoYY6Z`}il*wyhOMWoYQpHhq-2rvGy|zWBN*QNBjS%99M~SgCCdDwF6QT^}EFWtGZ&Hn~E?fRh%Bht%XXH#pJYq$J zTtqbJm6w%u`ReEJ3U*`Ar!_=Ky&Pcx5+N86BZOm#|KCQxNry@*m<f|`gjSHwMZ!rLsonBB z9Y249g8k9deGx->`r7P%U2M~KzR6;2x-kwT%tQ&nL@7krk37xt?5ZJO4W?STHi*&l zJv3cST3FT%k6Kq$?N3G0-=EKild#2OR?OEs*oGm9vM`Jy$nq$Ue?MGI^=eHisp|Is z^d&J=Rd!vEbX>Qs?{{5xm623aU$3WIpS#s)FlaS$!JyV`x*CnPBMo4xP9{*OWUg~> z%IE{njJRX@Zr|S$Ine3UxZOTe8`0 zHft4fp-KGk=ibuI!pKZrV{m$XBN9ktm$D0c(-WfOI?AGoA_6vUc9vFt5;n3UBExfQ z;UuDBH=e}-IAs?H0gQ1P2JxtC8b+abVH3o|;mpooDU``1fxGfB07|=A0gHYfvjN*`*eKZ(|q()|BgvDJyA1ZY|$zHhGt@djzlDS=~)9Sxe z&VP-~-Hk0RZB9MU9(osjVu5O~-y~^HW}BzE{v z+ufzNj*+yW(8T1{A8_1A%lLCNw!>MXgu~@oADLp*3E-RxTrReH$?e}o^bn*0Q?P6hSDYmo`pUUM!F_h$NJ)2JEV?k7; z^zO8(hoi~wN3PlA2^w3NCb`aZo~J4HX`UsivPqV%QZ;7ZMXUV(diZ&F`Fwm)X%ylP zf5HNgIaRzUk%UPs8UeYb? zt~_f-5js@R@>A~4^!C;oxmELc2P-Evn$u+@j*iW>bYBtvzq7|52w5tu%y1w&F3~8R z%r#OVxGr45a?1#Cv|rjI3Jz&t`Jy1SM-h!m@FbYW)F{Hkei3d9!oqUT97pBy(j0=0_G_WMG}@qp-$L% zK416plu1eHvNXl^>ORZ~?+AsgXw)CZ_xD>NjA5-B8>JYEVX&kkM$T~49J(mk*W1-1 z3qmFEg~C)lD_MMq$4pd^a~p|hOpOnpd_fc`s$sTQM>j_cCb6>j&(%rWSE}xoVTC{N z{*KjCDHTk1G#p1hiou);cXh~}Hq%rOSqPGnh~!??Sw`g_8|01vV!5g0D3$VU`gLz@ z-7c9=5qgz|p!fP8;ROr-%)cNKs3c=kUE)TNP7~56wvy#8{)oUPdiXjZPcdj)U&;2B z*Qk4{CFF!*sB~wL#cXV|yi#BzuBG6^(wX%6PFkn;x+u#Bd?+i`S2E+FNK>v;t&uvq zFZ2%0ul3dLwIl89arCH*FK_yYQ^L3$vs#D8WsAe%R#c3`PpTynzDay%UWt%4MnD!; zmS4ezwMf=C;`Rad`x_sf255j3kWpveJ87%Q5;$2Rndt$Hr1W>B+L@g z^5d8nx2W@CCOr1s{@BsHD-(ect6~I85KORzn;0I9oAHOHlmv<+hp0_NwyyOSI48l( zetILpdJsA$sI0;8Ahjqm$W-+^!aw0Oz6T0l16Dxvd zc1AfGDy%YdX?$(gNQkd>$vq+!)a*JLy&aRDiKJc%qWEP}gp-D(!$Tw2j!;=bWY*j- z+iGtiv4FFOu`ap@Efu1OKou4)J!XYoZ7!DW1ybfPsqS_XQxW3-tQ=8);s_YembJ#4 z*G=i*cqa!a2h$e(L`sQFse5Q01Wi~(2Hb9e+m(W`KKH?BlzM!T4iH=c>gdxc6HmJJK1~LXKlCng%C6nskBptz zR7lmDEr3^YZIqIsLsXTHbuH#Z5$tdP6`V!lNqg|OBnf;5PZqxZ@~e}CH{JFlMg+q` zg&0knwa`>O`UHh|)=|J%j<{5YO0g4v0)la+q-DBN#}BJ)>~XLa9=71uFp84bQwB=v zRpHV1Ws>0ZM;KEkB~~^pcN&2O;#j{HyJAS+lFGv97Mqx&m=u7fQyc>Lr=oyHwHCyp z+iEA0r|699rT-zN$-z|2hRK-*ihbl0=#SxbD3Ka;o28Ry?}q|$YOYWr&t@Y0AsNkS zevWBrDve3jSCCt1OO9ny3FtLj?awDki)uX#bbr6o0KRaYl}Y`2f@0l`=3#XXlR8I9snM%~3&?v% zG9Jnwt%O>HQRS(Qj3pR{0~3BJ6;3svA@o*FXd?`7y!sJ5CscN;rITnR*Eq_qRom3g zU~_SeFv=s+N!Dia&Z1qJ&VZY1kujv$VUCjbXoMYH{LPGBEKA?egnqS^Sg@jpKx(?m zn+WF|F&nJlA|B~y>=$RHWZqmtMAR8D!)?$8EV?bmu*9$VP8=c1JcSP-t|2)>;Y$hf@a-9*bqTevd{q}9S) z0TW7@xMTXcvOo;JN&^f27_kM0xpatR$v0Pe43M;#h6r>lxh}q()k2HYHZh8dffzq> z76h;)9W1CQuT-Y2wNhRt`zXHTZ??yp1tHdd2J&QdyhC*KdH-#aK-U? z>4M*|&^J!(kqz$0UslMD`GvD*5-LaQdbs%5yZs}QW@fbDEvhjsRX~uY-61s+O0t9( zLl=orw?sD~N`!E>xD))>66a$jPjb1A z>jZmYgeG01YGaMHV4ZYfK265c*T(p{-#R)Q1-BGMX`^_7#ROlTo=q8}q`cMbJlyFU z#uGSfTeBnFX6)#FQTUD%Zv+5g3*PV6tpZKz5dPCeP zdW4yKe1Um%bHmErGcj+vN6uV|V9Ho(>aI28kUt!So#BTMK}sHfqJ^62`9MxJ{bI@x z?mfIFw1*j{$`SlwO*YhPH+Pa+MR1FS%bg3wMOz_TUL(uC)%X_ZXaXVnkLi}H=WR3F zo%SS3K+Nd_s?usX(Yd$7DdrTX4CKexC9aQzAQTO$Rf1Jn9{)9z0=G>4f8eG@*m&jY z*tqk6*`lt&puAudO-LmW+tiSd-g)Cw%)kG>7ju9d69%4gvacHis1&QFKqNbUv%ZAoUPI(0FEcnV* zGZMV{rf!W|yQXT0st!o1RvwbTy^6^(x#YvH&KDHK%0nj7v=ogfi{I&mESwg7a8jVd z@NCb97UjJ9%&0*@A}Pp3h9FGYS>&B;=#PFg{FAG17iCM^;;Mxy#!~?O{t%A zYr5V_m-SST^H{IspW{2-Ga-^jjItxdOTNCMNFy8?u6U#>oXcFk2zDVWigL$Q33j|IdjB=W-1wC8AD<{|NdOf@NK$s%EW zL!P+BB}%Txe(I#`CA3JEHFo7QI<O|-+UzF-NAHn);XGzr&SgrT$BCnP2@YUdx$7xnjY_u>BBb#1_c(oQ z)BQ)<7;ta5xVAjFfglXMKUL#`hYIr!$6`7Sn;BwZG$q5H2wt<{#aQ_6_<4HyQeru* zb*=G0=!4bwz`LNzJJwhUvRn(dlp5Rn8WSL)e@}8E6ug~e0xml8W#y9bj-nP!tHhpb zCEuX$`^Iv;x4#S*Lhx8P5} z37QkyUASi>(l$MvNsj&`$`Par`ZxJG z;7N}sU%_gL7oE$9uakxSPvkASMCo!r47>=fa#&Vm26xye*&YU5j8K0;&gx$&-c*G* z3!yQ*$>9n4mLLEZm-s9>BtwOHm0um>`M%(qbM+7UU)`Oy>eH=OsYUXQZkZYy9&y*(;OC@7Ij_9beW4x$r-NIrC(MX8L&n?rv$-@ilneWPG1 zj9aa@B14dd33b>WRr{zpnTnbSyC-*vWu$W8km7;`nmp>3L>9s!D}zvyRIFfOaGGsv zGkBlGCzix8;TmSM*QwFXtRa1V9&KFQQ@yuDE8(?#r2-zAyfee6L#M(3xD&`(P<#ChcFv z`i@CVd+AjMrqg%c#zWj#3a;tV3@rLyH}l6lXN1sjtsWvTrT>w3cwW* zK()kN$Bcdq2aSl}gJb9MHqqhY8>r_(-LuDBd5f#>t^k&EJK6!bF3KUvxHmR@@e&#@ z>gW)I(Um5#<13Zj>ZY;`_O`5<-h2tVr2v&qbc)7s+^ZhLFOs0eh22|WvUI<6qU`62 zJP`aX#-Fm12q@9Q&V+b4xn7SjKVVcL{lmZwwuLH=uq&c0n&v|l==6Q(O%R@&*wD9< zze|M^5(F`XEz#nYME&4Qj|x%F`D_w6wH((8XTfs|gd*3uxIB^yWS8%mtoyw*AO zRA5PeNU=hmR9#s4g(Iw0JxLjJx{zv0+Sx&G;w&Wz#;ww!w*2dAH;Y^ZH(2U|i&X9A ziW$j-a(zhIjVkFC1t#OChl^`8S*7K8VCta-Lz4GBO7osG5|I|)aDLPo{$1o^mEgGi zW#4PF;X{p9`q-glqEt-cptyFV5t~?6Xe(PUnX_cu9kO-V&}6!$K`?sWGf2qhbUehT zl3`95E=4q`c+N*9X*`nyO?U2-uvM2XyjDhvy(U}Ee!7yT{gmqGB(-RIPec8Ol|TU@2!h9jnK?+9MV)ReXxY&{q;<1F=MDWDEAb~{8QoB) zL5%PDbxsPA0G6=gbv8c%_u#Iv@HTj^r&@(Pm~ZUKp1=OrTBLboA#end5||(!K7%2d z*m^%Ui|`zfQ{v-45zU9@JKK9vj;i4gg`pvm$&|DvK-B4-NUbFGNtNMZq5Ne2lXxmB zgwM&b(wn0QPN+m!M=6y3Nm_w1Asuem51u4BH>w+2Ft`Le78_w9c0mT+?n=+(Cvm1| zQ)C87RSNc%VMV@R_3G0loliTWSExg%(Ke^i9oPSS&OF;c36=QLw!HY*(i-O4y`u+| z+|Li%yqL?RmCIuAe*g$}VR6X|=uQ{x8sT?C4XCK#b#h=)yo z-TQNAyp+6q{0cT1bwLT?##FEo!J#9v*g{biNDF}?#o~v3AeoY(J0s@gMr;8&U_2c6 zaTLbn2^9mrMP8X$VUUI}>K;qaMvJC@jx+eBU?rFoi9f9mHYj7#YDDhIXpZ2`VOLSB zX<~W;7_)&!x)f&-6<2Mpv8ro8_NB>tIt1D3Z+WBVjE;3%2&@503p*$;vb}kQCK!pU z8FIcmR8e@O-pdfcC$l7!vwG$43q0=;w5XB?&>75Ty9!&m`^{@9|NML?kEQEQ_bNOJ zu@|$5bP|LYuSbD1Prp^potSC zNw8y8*Z{)rDQ!%q$9p2r4_HcP|0^{Nkv0!;6h`UK?CWJQS0%!(Opz->uz^`RO3K>9 z43&Wh>xOD)&2c1M`*;udmlv2){*iM`4#2n=+~nyNKRn7+4=MCpSS9EG?bkZdimQ|v zr%Nr-VkbW3EU{sY7;buXL&=68h3XY-O&tfj3{zuu{HxaCIp=O8uamWK3qXBZ@^nwP zm#>hV_{zZPP>*ti>PBm&oa_h~Se+ zRrSh2QG{=uiAyDrrVkv+nKt7_?zA&mjQir|IM{%wUSNBiN>U;ekV`#)t_=?%nh$i<{%mBf^=_C=2&WKf%|?E61)uxMd6ioy2J zXhceocHKVD;0Z-pzvtUlP~g0By-fW$Y{Il-u+#2ERuR1?rx~Z-Jk?y7+vE8Lk5OR# zj`<6%kxyk2c8>x*vi1hP*}l(92W!u&>cJ~8XkX0N2(I|E52HlYJnl@DGg*tkCR-N= zS#E}94vfnn1>r&%&mKl`pjTVu^h0X3hHHE_Ja`fgtC>d{H5j@g(UD6r!mUkar?1Hq z+x1_XBa`Bwd){RO^*0pCw$(YchV(D2&!U!_IWD+91KI65=I;&38-lj;t!S%=MFnag zsxiKbX-|Z^lfBt$Q_ZJ&3vw#3Eg8d5RAtD<6}1F|eO?2tkab5tq--em6g>%jxTLIPDlTsISjN)W*A{4(}ENZP>C zi9|6HwZhlPc~KRQfKCYtwANLp5d9oGQ(ZTT3>8Z-KR-hbOsHp8PbCDOPEn+}Of3mm zEcr-1w8V3JQ4(FPaQ%`L1Z66$k?d-T`c!!RSbX1F>!^}g=$e|UZPqV1r27Ut zIAr4?nn_D<9{fVhQXp#R(0K`}E>2ECu;k?-V@#<;g@-7luRPQo3bG& zPvYA}o0m$5;m7D1OjuP`71@pJC%G40em{WY_Aeo6TdM?Blc`p0Z1m##AUf2GX0vEa zs8X-nr1A)e_>-UYYgSB@=3X*YvbY1eBxqH(4{aYXI*vTPmk{-)hKM%BY=iQn5rOwh zKWP8qNg2zvb8(wa*JbxI^RkD6U*z`4Jfg3(zAeae$(LDtPB-RDnBHbx|C#MG@Px&j zRpZlt$iL}zznj)}gt3M2k|I@j*MnwB_)Lltl>bP7jk7CWw64rDj_eh1loYvCrY}6@ z)?Qqnp7CI$3zm=T+MN&UB()w?d+5H^!eh(Xy6=L8Ev3K3*-F4(RCJw9)To&!)eZSW z{%}=$wV0T8Q^z{Tm4<<&g*hk z&bYZr`8eRFt!eT#YKE*(BXq4JTIm+L0JDS8ZnCv#d|@jHb<3X9ww16?*z=N!Qp1rE zmoJ1Z`LuhA$7KK(eORSphle=2h%(Ccih-%##30WU#Lq36b*WXMIQ&Tmqi}tBpmLZ= z0Y)ruQPW)wDBOs|J0MIo4YP$W;7X`o zY@*cY&On8DoXVf4Cwu|d!i6*|^9L6YhucFfd-iFFLGR;dDwuI%K?Bm2=OG%36UV2jU32DpXHiOtu; zX$kkA~BB$A^L ztwtp^GYy5@gbwUMNsK8Fb2F{JcHut{@M7U332t01po;D#S@ro!K7dl>;Pzcw=|4;r zi7N&fwFYQA0euM)QTwR#YibWm1tW#Cm`;Orn2LKXMSltM6_O>5)(=1~l=B=%-Wb3c z>?8SEXww~#81Y}m@|bINt*+Py#KpiXC^&O%*^Wp7^4*m;nEZ|aP|E-KS)360PXF*o zfBY)|00Mvj=THFL2mqVZ0AV5kI3wi|Tfhd67VkngJUQdezFaC&mFF+Z%uY&8$xPn8 za-BLDJ>vF|HcYsQL84;C#K|2ZLDmsn{Y`6P$Mbs6c`HJdekkrrl1`a)b@0iMpzyFD zcn}jG8xG_A`uM?jE4vWjgWll zIt+Qwwc$D>?b$GVmU+!*tP}krZLDwHy=Ji z5*}g-3pNj*ARADS)3(f+lWIfrv-*YNxUDBy%&Bm&~FdgB;KyusjbY$e~Q^$_b+2sujmU z%<33}Uh|+;l;9J~Q43?&f1vDD`~|_a&<_CK5rL@XNen2ClSryKO$0OjsnGLzMBc9> z=MY&oRmfXQMM^5m9T1+WMKC987}dCB7JSQ+V@!~e*KkPP@kq|b!Q3kAvx<21Fu$E{ z1kN!yGkgSqn?@gpJ^?6E)3TFzXbnUkGx?h+WfVT5a6zR;kd5&fhYwlsSfI#4U)?zM zfQ7e)t)s?X$mBfW(GL3svQ{C`g~d^Sk!ZxJuBhKFSd*p-I+YZ^J+J&nK=(Dd%JaIBMAZo+hk;+=Qj`7`WkKR1yYy)*lch?W9WrohndY87U}|fW#8+V=(B3Ea zJ8{uATb=0cq-)!e5=%xZSr#a^Kv>uW7+`=xwIaqbzn<{B|vL~d3F+@^fE@MH3n(E3L$Qx1<=K=8Uptajur21 zr%9n5wl_Oa1Br^H5eB;O(lyJ1sj~7Z9O=ihp24Ai?n+TCMA8AiBE1E<%>X$~$#GI; zT$FDq)(U|qm*(fLFjH;=iT;x~HgU@UWZaQ}K-aW^*KCjqltfwgOhUGNPFcS3mPRec zX#ReQ5aKVzcq4{6LLSFz1Q3S30LV2^%g5@XEhgaOm^ixG0x4qVE1vI#;+2@5*-CIo zP-Rzd!1OtC7)@6Lv4Tq`A7oT%D*kM$+(ev7U~<0}N6AfOG(-mIryR{vgeQys@!%w8 z4xxg8BLz8_v?xY(qO4nd;F?6gg4zCC07gK$zjr3WY8zY=NbSVU6LWAbPS04-K5U^f zZrdfXvtK0M{S`SQN;CAVM;!!#qAX_x6?T*UJM72o-6EHV{u;XM=*z{WFaJt@wX?`y zC01_qXt6<_{)Sq^FA4ztH#W*$}}a?T^Hb;MghRmIA3wu-`%h@9f@KO{#70M{RM`7kQ3vMBwKD7%)M zc2u8218>45bg0iiEkiounsLf|9BF%&z^Bmw@kiG2S|>>#g8@3l$b>t}_* zc99CfN~s^iA52RQofHt~my#n|**Y%5u0`A^&_xjQU=YD-SQo=&ST}|{>gXfLsM1`* zg-)1T@Fw928CV04zRhDiqEUG1_RJa{ z@&igD>5tcY#A!b=dE`){Tznw2>j!gQu!2qe7|D8!Jx*Od1;!q>)udm;$!YpBHxKh8LSo61P;Ts8*tVNkwdXcm#p2Z3+8 zQ4awtbV-Tb`~(Fl!5EG-sUAmhc|_uBbvu`nB?qS@C>Kykvv&w8vB1hh*b~x%k$!-r zAMurh*eQdU!b=IF4B3<=a`sd7wVh9uL3whN}vpXXNEN)d32m49PS*L%0V$d zazOjtIOa$|5#W)+?urmYVd(%fKQaTM;8Y(Wd-@~$pWS+?R>P~P?eNaP&9X4jDiAIp zZa5>B{Z52D5x7XoE3~TJU$BPmvaKM{3daVZnP%9e<#aAX3aKKP3cYNFR}V*@f<$5P z3JL}$g!es{IxZH8YJN$lQ>C`@BA9n*!g&#uhxrvjFW~VkCIJBkvY?V%L@SK?!Wc5N z{UVhM7i2Uz_6!Aqs5vhy_9>I~PhG@@N92U86AB9#7T(}UXbB2O{^ZrG?w_<@X58Ry z#_UjpSb!<#wx^SVmZJW)|j-37+FgA=*p>}YmR5K%H5laRF> zIkJy-4`~b1^1dJi_9j#L2bfoGFOI1BZ?F1(5C1!o)l0j+fJIR#H&XIkvZeD!&C@G?pB&!VhRL3CU z+KCvF<2NvFe&LXXc@keO{X~LH-E0&m`DID+G8Rh`c_h1GH=1!74&Sqo*vKJJ^7~aw z7IBVZCYj`BvD<~4B;r_^#kNzWlGwWw&#Qyt`L ztV)#dQFBRVMGUJVhP3XiSePE?D-tYnxW7RsBD)XY<#B#-7DY>myxn#gUaf^w2D*dv z*rXiL$5XGBRzSI!R5dQdJQPjCWYQ4L#H<-32~N)ZU(2wwXc?%%Ru=QVGVBk8RN&yU ztOVzuVjvU4KY+Sh1Y~|AF>GH-C!N!^oAFiC=til^a!6NAFqd{A*BzTMnvg?%r*r9eoJT%z8}xb^UyO)F zl-xs9IbXFjl!~Yk$EJx(Dp?XG&_im;eO+(v|=h^4Udt|O;Lp< zX^N9)wQzVMapLY5N-3n*M`^^vMNz<1DhxtYln4knv{!_}VQn|jPM}~UG~kSRM0ind zHyKq0r%@Rh(^6@cA;j+MjUkM^;S`NOeX4uZK>mH zPtajJtft^QY=OB&wJLO8*3Caqc)2_#RoXO(=9Sd6$TN7z>a|> z=y`af(r+r4%5Q5#!2ta6w|j0h!eEeWa6pApm9A`|6cS7bVMWiua6&aNYlV)3cdpky z$*fp*6FerKLl|W%tn_eFn$pSXgbf+k!wFfhZOP zf_LX=E(UUaop+k`AV4RF<9nE;on8M2c7aLr+ryf6}W>>e+cV5S`XJI(2$m z$O-JUYlRc%DOrs<9T#zcnBKP4TC>%%e+1EpVC*6(T}5X{M`KfWmq_d^1cJvW5e!Lx zlvWak;-YkZXz6P$q%x11CBu;9^%Tw;!89(FU^*RyV}TjkafJaHXK_TG+y;S=%- zTAfryf}-N`ax-%>rp@ZBD1H}(gAoF@mQ{cCupn@y`vDPY@e5ix?bN@QoQ%CwQEL4B zPUYL`iz`ml8~^vB(Ws!bk=yB3j@kZ^;UW(`(sBibohFZ~9wf6flaY$p3J1es^Y186 z>DeeoKM2m&!hnho3-cBJcgU2?AsUQDZqlRe8Xt46pZO4zjY-bdp;PWT<-t)ALyjTE zde!Sw*FuKJh7UZ8vJ@?7wZ_iAqK9ZiOC@Gimgnd)>$)>T;*-!Y;5L`GE6QXF~7je-!ID<7E>F7abY{~yD_*i;t;28@X9B-Q=7nTmx% zQiiDf(N%x%g^5ui5)W6>tNOkL;ZRN)3(ON!%3>Mx-jj9k^b>->xOUAY4K~fh0+V4u zK?5`>5a)PUG93lt1qVk(dny%cMhU3`IC7Mb|7)yzRI7YUO3m*ESu*f^zvzLW|KdEy)%vwqt>a&`3*Es#ot7x-P~VdV=R|lD}c~ z#eEP(QK&>ND!5zanVwCn;Qfn(2uVH{5nn3zT5onc*-+#ZV2urKg z8`b{j?Tt-33Su#ZRmx3>5qc?06umQe!VpFC;@Wk7(Q2k==9EZf<>gl@1#8n|^wtmu zz?L8qmMqdN)n%adn@7Nu5`=;p?ap4EB+8~ioLvZmV2J9=W|Jj*l_Lw(h*@MrqsUT( zuU^me7ROMUE)kX_NUtB`wDnoW5QiYfJc>K7pSl?_b^vMy%$!xG#Rd;pub5eV~D~bN=Sf@QS zy-z{!Gp~~P#VlIrT0+y3clBxX^brEA`w@jY!K+u@mi0xt-x;JBcXWtE_7W<$?4YLInIRSB&d&R?WFbYHd2EsG1aqJMAMw+_%loL}s}~iD+Om zkbj#Z<+Fa6q69#YPK$#%@U*8d`5$q8@+BGiFhr@U!(UhzWQeD^p0i|J4PiiADjrtU zo02u(H+ol{XW?kpR?+s&p;Br&ra+Gp#>+iQZhFODLs5(AiTh;j*sZOvGcnD>@n`Ys3XN(-Wy2VBY7t0(JRl?)*T zn58b0&shgAW;M0n=^FyrTb(Wzefc+{R%6=u>VRJa2Y}MG>PfM98!}U2A(%V$PXORAo#t$`ipLCEd=cClQfLH|)GsaxMZ$XoyvF zhEUDv-6THyLxf=tK{ipOt9flXriHE3^BMwRN0Tc8PVAVny2sm5B0PJK!7M^Gm}<)?!Gt7M zN$sUpJi{zC#))viw_*}_OsWc*84E9_?7cf^%c)v!wWp^$F7{B)LCr8O3NQTG3yoy{ zoLAy9XYlTpWyx%=H_A$`EfB)~lb|C)P-L7m-j7ltQ`LN~iN? zTC=qFJ1hz%yZ+x#$**9IgWO6T_8Bhjs;z#olsAOY+|s&NiX&>1u>XZOp&jl?vpfV% z`zHiy7KTzfhy>b+hL#HF4~#&1jjk+@LNIbf%xbMX@qbv%?;3!e|ISli7lo^Y_kPUJ;mp`P%=adblAkE%}g`3T?;9K&BY(yi4B7% z1$Qp`KY5X`mG0lU3Qg7-6&x5?L8mCFpr~Zzl0U5yA|ur*WG`%nP^!~C4`)7ouHmL& zEYuN7M4FayA0om%caz*- zs=d&$&|-<9tB*9tcur6}+3o}yTmzAOCRfE9hrg_xiKk>P;E1P;5|~pqT#PX)e&$jV z;yhuVsr*f|3AEMiDGw$zZTr^k#b)%XM%QmPMu>$3ElK$*uY&4HxNesDo0C5fZIY0r z=A%D>I9te{vmfGUR51q@YZ6->Bv+l19J-}gW=oRy7srYQ z&i`!eKqaGL7n37o?{zljW#q7SWP378pqdTH(+O`1`5$g}8c$PR<~X%*rPL^v7dxQ6 zki{09+;n;TL0b-JW?+wj@eE#mNl3S=6ca57^j2ixAvmUM^QsPfo%AR841{!2s){f# z=)A)Rku^kf;>9rmY)`p}Fyx*9IuL9BqIIjUg?| z`j?=ng~U5bV&M^-1d5hT#cBwVW+#oMa1|LB1yXCzwOsPJa(c<(&eVy$?|V6T$G$TV z%*8LSkrd}n1MO0WzL7`5h7Lkm>7D#X zLUBG4;2BIr7r1$HL7_J0a)}8L0F8PeG7(D)7uQ=?F>yj%CCZUS!bFPpB5;3Q}! zT$(BQcUtK5BUqh6cn3>0o~wpkW;O++)j`r^x~Cu+@EgcLo5XMA@Ldft&k+JI1U3ae zMbb^s>2_&>ACzC>ld!Z2(NGBFCZ#-}N=XDyYzY-^qUdMgS)BDEL|$_uJ;R?gLO=W% zGCznrnGDR#AYm}IFcFYM-6(lFVDi@%OeNT>;)SU{aQjK_7&RV3MQ{dzhy?4VgRURU z6~%lDffuZdJ~|UXc=0KmIC(#yBr&UhAQ*{w8~tYlluZoNx-Y@T;P_C4+MS{rA33O> zsfUs}D=)oOD3Fufu*^SX;Rqn-B?S_-Gs_hZ14fJ!l?Y|1C(8PQih>QS;gbRcf=9r_ zn3ptI+M$?O_<2G(i?b2vdTqQZA8_ohVP~c`3d)akPVhHw&^pHeJ|ZkGK5SO!kYfa$ z`z~Txu>=eZEKMUCL9?WwOr0E)p)0AM0q6z* z>orhPO03%X!iO(BEx<`%ZA?9c0T0D%FqY&Otfu8F8b-by5yHmzP*T#F)i5P6uq<2g z7!tV=?`V?aB5L9aeRP*siFprFYm}|)5)Pc_$csZ2(w?dl3M=|k7wIl;1N>q25H|z^ zVOLef2QD#aTeCz5`efyq6gF>yLJS04k=9Ij4#dJ>rnD(zprhMMk(Wvv38krt&buSn zWLjF)RR%dlcA`i!zUg}AQn9AnZ48l;R--MgBmwZXxS)GfyUo!T$WBdHv=h@dtezX> zCYl;4f6KqAiwKj4Ic$)IS9mYaFc$F{)?XWLlvzULBu;)zF14(rEZU#7Jym=hPyz2)Djt0q)X&l0|3?hVmWC9HjT~9ba6B zUYB3mnHbSaUDFI=P2l>JNPm!>cT7}egyVP{DJ!ALE$EFl_=`r-98|ml1rXM-nbMqQ zdM{m5Iz*$pE@!0N3*8i4EF*F*4GXlEQBYa@S#|#;0%kulmyQ%Ju$&UnSR5cwg9ac9>#$^DLsB0trqg0IIcy{{DBr+dBP8D>Tom0c z;~^C5gO7P?DkmSd2V}e>Y_MUfmD97474bU&uQaz}N;7PA@!3~7+PQDRE~-TYTr8NO zDsgO4U{j^ZY;xrs$+eu6WK#+SjEHcJJGS8G16&$3AnH{b$vwq+jUxs*5(wh&5F`1T z(Q^8o^F(JIil(*8eD_F6f+zmQw-Y7E(ekJ7B}!XN~(5Lxz7^KjI%+toikg-XCOZu4mU) zuCobDM(B~wQx=UtM-jbzL2v|gZ7Y{^t0KN1W1W`dg~jB9Ia*00oOaRNH1)5F+?l)x zqFFOyR!13e_ZxJBbdVR_JJe$(hgFJ2xH{=cMLA#c~j{g_Z~k@*(LI1Y9v* z2^I?i7qCab-mm72QF+uvm5qvgx;|>b1x>5sU_Y43sGu&jlEmRt1$q|!-|NEyljyC2 zT;QP54|L0u!G7Opu$-IuD{Oxm%FI#HpcCxG;V=t;7BnON@Cc+ele|Y_Fv1aeEXa!Q zWJ1A}*pzo<9u>VV0UIyz4pEZ_AQ=`5#O@?Bdc`&~8nE+)8MWqsfr99h21QW=0F0)G zuyrRs09r8)!CemIAP=LlK3$Jq3XmiE>3MQ2gleS>+QWcSVR1m* zyYR}MV&VJLRbWjijkG7VDf$E-hlPzQ{gF=62mw1=^U z=&xu_yWAoGO_1{?>JV}6!nK|ye@+f*Gl-1Nn8Oe*4*7V(Af3)#YpVp1HZ(*f=0hEF z+%3Ew$jBf-*iFX}q>M(G|E!Wqfm5l4Z^q*vp*TT`90^J#Ia3CTf%5FQ)z)o?1cE@q zHKmppZ;m-r1fpxLMZkk(|81nP4x-Vdu6-nrJ*)*Fv~?~hR?#EUN-w*saV^r3uamhg z@3n5EkbZR)5OITc9gHRj`S4sd4>T( zQTmf1^JSz*VY*vNR$e)lvHOTBSlE>Dr$yHkna0*dxm0JLk;Myn#Dzw4Cz6%nsE{++ z*TR+pEfoqMuZ*xbamJ7vthr1CIy3Qh_xHBqR5idTDCOotfp6x!63u$*vRT5iF&SKa zpVw*HyTT2>*u)p z{k|(sEr|U+jAjAdqv@*A{z@4fbr`3n9NiCa@#O@8AO3CtUxP7 zt$4RF8kM!hte?kAjXhV02byJUY$TWJTB4CRjVxlJ#L!G*k6LQkGWkIno%8~Uuo$vT zrYPA3|GSMVVm|toJ#ZbuYcgPw)9NfK{)e1t*8d@Ll_9Q+NrF{VGQ-WfdQw)7(b!L9(<2m{t&pcL?glLgJMkheUPL%)4}- z>jUxG?yg~C=%r~q5>h##RLuG}QJ1$zZ>S_TN==M8l9v=jFW?wCFV&^Sy#Li%1vTM{ zs1S+uivx4B?`g1MT+B*ib>DtKSR5u*B0pauBBEPhUMEXD$!A@7n}wrhvK&3a|6C{A z+vz=Rs-C&=1_X3*nNFxDfj#NWGzY&nxSq&LOEGB+c(QZYVu4Nxb|h%-jsOqCRl>~? zG}1V03jN4n5+BIBmO(>t4A0hdreGBn0%@(SHX@OQO>iyX5iJpfoL4zIY$rl&C5XQl zbdYuBGmI9!BF%YlLftzmw^3R zKRU2xxufaaI4JevE*~=p>GCg?9td3yX&7-iTaiZ5LADdYfKMV?z7dmUD&cE z(?mcSXftr?tXdm5oR_B0Ak3dN2IyqemxHR|}f)CZ28ub$uX054@=CwXPE!*TT{gw060h zV~KL#b)kty0slEnMO3qeV(h5M)>>I8LQMw-O_4umVZf?@m}kLI-@JM3vJmDs^MR=B zC~--O5#2e5Qbm!Y^suv%(am(Kv(0gvosot4=h`*=3=QPRM7!05-d>SDd;cvx3slRK zsEZ$}CV~GVHj@;4v~x##izE>>emfBsz0S7&_KZ~ncGw$L7A7Bzb5f_p_&tcC>Eh} zTWn7TnF5N?qsbTwv-s|dM|P}M8Wl=OJQrxk6~zKx6h=+jd<$+sTfkWGAQ3m$t=5sw zg1qaI7$AZ){=fDRnr#V51B2*2aLYIA@j0CVtQXNu-u$R6;LA)LL`wi|8i5HdQ0WDQ zR$mp7NkZ9Dw*W*uj^p;WP-#2ZXB!QamwV`xpnu`cN$_Xf&K<3}#4^O`j9;L%RPfX7 zdeogWcQuo4{qC-@pU5>Kmb2k81T!O=pJU`?X%ldgcWl`zf!~W_2v$h^5DhppVMgjt z#3}dNU9!S%HLBt&mlc{^*-DU-gid%?t4!n0Wprf2EIaJ86lAEcsNiJ@r|2+T6)X%P z>8NW#xqb3Hh-Zo{ z(p5_GP}SGU4Zi`zM`vflNr~*V7mUWbEg)q)D;=qSVu?Z-S_4cR)*^l6uvm@vb?Ffb z(lnZ#)yh5NwA@{u*PjY%Az2j9h#}kTF7p9MFF+dYG1>0ClJ%sHBkcrrtF$=}MyXwA zj4`^n=Yz%WU#|O@a>?XmJz;KQTfj8wyUU zMpiB~Ec2J&1sf_Go^4|<6t49WC&tx|W!;u}e1+aT&|aX%CpBPh->ErV37>U7ZZfh< zrhkpEy7XqQ7ZDm!c{eQ&amk3}9O#i|Q^-@k=aD3gWYy*W#C>AQ4Tyh)UDgjWa3$ER z-ElX|X=jRsoUP4)NR(DC{Y?3&`&@}k)RoLA#fQ4MIi<8Xk~(PK?Lrt zG>J%s+?sP5i!!it#ZUQdS%;(Y*jicfLz~PwSOswg-&c+cyuxII3&FgvYjc*k7=GBY z?+tQPscU((mIwQZL(^9(8PIA04agv{il{_GUaI_eIIENyIO*hNEvLE0*rQqOn({&= zT-xhN6{B<#&?+TB^3v+cZp?ZF zb2{1OeNw%RB3o3eX69LG!$-r7HZo50W|&IO0*nSmoAz&Rg{>H_Z6Q zCyL;_DfcCaztPKMi5D`prs6zV@yvflD-=o1a0NfHoc%m5=gKOcOl2J^wb_)AO{H4?4mDt{ z^Ug=n4b&rbSNKEv!bYWN+DT4H38uVM^>UfeBxBu0icLDdvhn-wEScQZAp(}h^a_kS zq+x(U@lKAp!%epA-7t&0b}Shfnkr3x$L1RD<+{toxGKeZ+jQW33wTMJ)lC=*7LXfA zCPn5EHFIgu2 z?aqrLEY$Nf6-=#fT9&YYz9UkaAswGfDAE(M6ymsT&THI{Cw4YG+c6O<)kbZc$BwJZ z>{N;wxR_Oynb6~>4|%ub(x)OrWec#n_C_)K@ zbebe!Q9&tnDoTGmc2u}+KK+ViH+p3ezoG`%kIT#}zR0_sKu zdxwJJP>&Zuy(9F?$xDe#lJ8x-Ks-~novDB*VrJfNsw-HXKpue|2rp!i!a+sPj-@4K z9p(9nDNd}14fD}_f&Y(j!1V&Mlgf!q@vAj!pU2P%=q150MAsp>MMf}q|6|pg!CThz z*rZt{Ildqv;ae3B+fOzUH`Y0VZ*AmrE#fP9yFH*_dbY6FES#lb(Qu|o7?|C6(NzMY zLkz?C2ZIm{(FuI9`0+DV97zC(OVON8V2Wq@;T|YVBkdIg)@nfALaJPnBv3w}D_rBT zs-cs}w1&w>((+zrWD3ZQW}A`-UrO=ySX8ya4Uum!%<0JeqR*kLL4E=2A=V(>Fhv5J zPod4hqDCA-)a58us7Q&X(v>jJ^dq(*e6ydjyDh+hO1)!4a=uEtXYXWK*=rinS&}L`QZzSEuAlGx~tf^|iSQ)PVZY_fIJ(2rAGg z?O-j~xBP8mwc|MKa_}RhCCY1ZT!4jCVete125_WCz}e>V#-8CauZn8u-T+jn z>!*~SE;&KTF!cDYjD1R?fejH;Q!7Vc8EZZdDaYKg3O#N;VyR3)@E@Cj3*pLPjX>f6 zaUm@Pht%jXoZhG@dR1k|%&Q+UEKZ?wiN!}l_ax5HNcf1$jkv=G5{oG7k=CNl@uk}-(i&zGYFyz29WKiFu)6 z6^Qd?EV+*1YpmJWm3xS`MEaz|Tze)y)A=n?k+HEecrKgGJ>F*Ot$fGOy@MTu{mhcy z=AkSCrwgh>)PZb)*oF11){#qYWN}S7k5r7~%&6<+iJ&a z%pl6(+0b3cr(`76kR>RHL!L|2Suub zh=dQG$z)eoQy6#6^m-LHElCF{`#z%7X^pd)TUQLlNQS*>`T*oflA7Tw5z1f2wCrse zizNIF0u22b^1Eay+Gca3x9`5gG7=(;BHn%^5j@lv4c%GwF?Lq!=f_iPSxFZFHw9^|egrp0If8PXY8!5Iv#f%Bz@ZUG zB6Q_bVP#}#nTYH zAsbH1|Ba?Wj{`!ARU7&W88JjkU8n!;f>q!XUmgQTyOH4@l)1;)hM2wLA%DvhOcl)z zb&Lg|*l+pup+I0o5OHXV(1bB~hL%Qidgs5Q6=6VExm-|oF^bVZJJi3erVxWE9reIH zISO;pz=k1D3K^tGNh2g8*ypYBCrZVk%n`yoMPZ_XPvtXVbI;LV(Rnf_tYzw#aFwL( zyth;+K?;ywYAXOD@L=&k9 zq{_J0sg87mhy2lj9x7A{Cs9QW6I584B5yLv4Jy*)njovMy&~78n|$Ms6h)O4ZO26o z6A8F5$n4v;d67@<#rY#|jn8xL^G$YqOc_j~4B>HAk8Mf6J%Y`v7VRt*tIGITI(aRoq z(!mWBq%#sW4rtSxou%Ppb*FLdsW|H{2DilumRA5+8M5(hy4G0lDAn>tLt{dmAUktJM!|g@hFs5UT#8-y$MqAb9s3yll5hKp}s%6O~-6I;=SnC zJ#x}6g*VY>tVZe&A2!cJ`o!W`ByQce?T+L)ki91{F%_S=6t&E^D^}2@s`FCvl9JW_>&}r^v>egD zz1BkmS?`902|hg8_g-DT4Enz14j3I9JP|Z_ql8vOi^zmo6AKB70tZDzOcG3jIzGWM zLSbWbQhwbTW`_omY>0Pg>e$?k5 zuTzwny*GA7t%{vcua%lL>)n>26u(+?yI*Kq!aVOIMq1pxIUlsC^4qMAXgQzt_`T&H zcviVX29LbUZTq!-G|htsj)@x-HY$Y+E`cU9@%yci92yu33=AG^wI0=t*eG;d*hKh1 zn3-1JQ7shSnp6dv3Wy4k89^nC;?Nsw*QHDUP~MK ztIIF4^2lNBoa8Lk=I1PMWto)-SuS*!YUPv;(#%M0c2Q}w^9aBLMS*e@Nn?JV(nPzx(4&9i!d}8 zEI`B~vL?|t{_SFvGJIg#o>K}!%R0c)Jq7NW_tOXday5ef&LEid8r5im%Qv%=X* z1?`zMNN9t@P0-5y6XDAY4-`p7Nd3dK%VC8?3nz#Upf8DBl^$IpC2BIQJJJ*6cVqNa zol{QeyCqT5vXeM$tWMqD=^9>3lc`fMerzJ;&}a1>X2CpQL?}lD8;2GC^j?@|Y$mB; z5=LI%6rktOFe8I+rgaT4G!h^dOX~*yY>6q*$Bzdh<~B#2K&698F&ga3utEUCLF0@X zGlAH$RB{dlo>--!oauI{rm^M`StQ6t$Rgy~5@qRdqc0<6bc&*g?Dkjs=AAE;-ns5Sc>?i{MuIJ54Jb^V@SKK zRYkUh@}jenKmGQcq~}j@g%Mejy(X%1`6aAk*40^zOT7KKznSD&E!Pmom?KP^zSXjd zbU2KNGL8zQoEo0B2wup^<0ib03JUs4@@ZLOC7W3mV_4OKT@b{4jc(eGq=Tajr3omC zoJ(v?cYzZG-7Knx7_rHynldhoo8_iTDk)b!;=YgLV)D^lVGfjfZ%GgB6n?rso{%9Y zwVPR=#|6nFD_Zi43F+$OwaMzO$NKi5VwWNc#T48#;UcR%oFt4Z9zh9BV!FC|SuzeL zS)1h&GP7eCu;OY^jXrg16*(_$Oggpo1g(rs9LP@;oJ?uT93rFySTEF!vG1^?bYQ7U zI=AiYiXTa5NTdxY!Ds@ zJzKJQ#j#BWG(tpBuwYG`8t67oX_^?%9dAI^FL@+^gTVwmGg_~fxh}?>f#WA!ml*m8 zJ_CW7p$s{!Q2by?jO87jZQ#3M z;uJy#Za7`IPl5u-5d1{n9MmPCbuGr{u!F>7-OqB>a;AtDVoEHGsMUS@zK}u& zfx}ubEyTGE1sW^tpzAt1)!ppimc^(4x*6xBwQYqWyJ7FEf?AmKFY z+(ckX%}MV$}^iQugCI68{F?D{H`BK+t|yF zqVbq`uNRL0AKt5={&c!?;oWa>CP-G%BZg;vr-tDdX99YD^fH4xqcw{?!5qaKs{~?f zfpl=p*^)UX`!?pBQ~%Nxr-gAtu!B8Eh;02u&c zfOL79PA#EgFd0Ja1maRa(jJAyZRrBpzX&}Qaqcu>@Iz&WjvIr3@Hp~8Maw*d(mCWp z$iEP2l>{}!H4NA(tgSJ8C}?-lRi}M2JgRBV79G8XR-F*#nQ~HHA_){jSUJ2YG$Eo5 zA*Ae6yh|MR&1Mf}3MJPRT=m#nB(Sx~X)(vjOGqKjEY!?vOeO3g=Bl!s;>%#g&p_4O zS`A_Xj+ROh}JtD5ce@tSu}dtaOxeqm8&zsth_YLRAp8MYra) z#R)*dj$m^o4K@_gqgIoMXtW-44=94kOu}wLr4>mpl9fy)0j%n3US2BNk zApQxW`5O%H=%3+MW>4*|UZ&2~(f0y<%e^NaowKahgz4q;)9kiPie+6?|Jggz@g3n6 zSB}f`CDu)UCtN4UN(^VjI#NwBLJ4T)4S2K595sQ$j0-eq(IW;8G!pR9=LV7vcu+|I zLdAns8X#bo0!=_Dp%i$6O(e2ha%@qhTGZ#teiT;;tK)F%``OwRZsM>JU9u!k%v|qX z49xqYy%~KGvMGzEDw(%UuDI_9`|&s#n5x3H%lab==JSR4xklR8Ui7q53N+Rl7O5jl zC?G*W2LzNnP*I~t4IXf5;R8ekh*{yr1PT~5@qZSlxP2T5EWz1>VUjMD)DlmTAbIQ54+N?tpm zI^>Fv%#5A}q-ruPiNUQue4x&$B;^XN)x)=cXFo(<-YIp|lN)Lvhohnx;ynq zPz+>_QL)orHKm2o2v$gxnm}T^_`)V+2V!lw)w@xd^IA#)X#r3qy;k`rtXc^r(-~+t z;0ajf0KagEAp|G-lhDw{-Cyn8FFz4AG&frTIYt0C|BgOkv+Mts*~3jCJ-9?7456)4 zPi-D%LW6V>>g|s#ixNxZ+^w2Us~C9f)bOK21&mcrEn)s&#FZOWvtWOX=|^(&kVzfC zr>3|&*H(5t#b_M{Ai2?lM*viPNn*igR9_RfZkKx6quE!qE?gw#fs_(3C_W@bL%TU6 z(Jg4j?JKeg{5suS#%1yvpk(T^*50t>p=`S;QclzndRrZ5SFYtWfXOaLsd&o9T;G zrN`Baqai84m>{4<2hopNxy%c$sxW!yt$>${GoT<**PY~@wHr1M>NOhaO zJ@EF9D4ldV1q_!PG>3_`bfo^B57IOxjo9z0EGd;+D~E%1>M}v6dH0B&2rkQa+$nu^e7tLV(mEwG?xNU!gtzAR7~?9{{8OhBWG`?NE);t!_JizN$5 zx9<{K4@O6%h9uHS}*%F4B_PGS4h>GPO$n zFcB~*aop1Gq#9Fk&g7U7dB2SRs5_LDPec|+fZQ#ctDt9z`BvnCccLDM|5%&w3{^?} z)H$e>qHS{>z5VkssKWu0EO{5PsS`sO!r5>;%*SWHc%#swKifE_s%5I=p?ixv-6df) zEN`HgZeeGS=B#&}{0H@=k+N>-+149_=eX4mEEr1z=qn!SNge1}N4w%lXLd5T6?$I) z&XabLliB;-%~pm_jrognNaIwNQRvw%<%;Re%w?mij++rHbLLit*EbuoB z-6Z>L@UxcdsxpuI-7*m*_*&Bb&`u#7d3vLTU(quo0P6Dx4L4xOrHqG;fv7(Mhi1woqqIA8VyYBS`&hAns?HUC_zToQK3 zl=xZ2U0o>?W@DExJ;-reQB)q7oAm!-?Dm3Qio_A8)kXX9xAZ?sxCzNmdx?R2A`{?N83|l2@W2*Eu5993LdLrkXU?r?;SZ#=Z;%;0j0@uTe1$xc(})ds#d)rs)_2YLh}qH8;k@9K&^u zlkKD&yo{gz>-!$u*qGJk8{R1woyU>*WD1?%WE;|PGUsKu_Mhi{nN zYtx7ktin;lAw39hK2H<9Rz6lFBGx*%?WD;EQXCt_dZrd~ZdZHuKvU(l{lp+)n4Xy! z(u8Sc_-c^;hckieJG6>NANslnV}6w-v>HZEcOPd9gg>FM(EK-u`Zj4448avvdzy<~ zGAAU@7Q(${OY)@Zc>FRh3E9h|TFA%6l&~~S!XRyom5z#t_e(8gu0PDb&bd}}GFV@# zng-9!d)XH7XM4qF(~FVf1}21C5m33qM9j;!z~Ax7rK&EP!aLln&d|KE!A?{*;30hG zq(3U-Mp*2|Gb??INqA47lm&8fdZb|pXjB@(C~v}~2#W5NlP*vZ6rxggDHK9u!G?zD4@EI zjK`GHVnO1hBs=4Vl~q*+!l(H)PiJ-78b28hzjwWc0DKPbu|$USUbZ z73y}@l5?KlCAw5?WLU!QoY;FVWQB~`YSpy#2#&ii33Yt#`KX{HvKvZI*s%jxR|xwu z@D-?tDjo<Y)IY`3MJy%KY7a26g?E1yU?4FTR=EK-0Y7ZYW9986~>Sd?~9%eaFBql>) zfQyp}W0fY;))64mSe=X9r|;>bNS2eA@asg~^hx&kySSS5ybG=faW)9wfWd&dHz5=A zw_1Ie;C!^Do?VU3Fa>pbwy4ZOs^t8cHwIgX(^K^oAI=-pVe%QPaS6sLW&Yj#2)q%5 zQt^BVm^@WEsenp@T7Y2~9)w1gPPqi7%;cm~mZC zFO6@K<<$uk6GJJ?q>8}Z&zDH>+X$+TDfC3uy^;+?3_4d6vtI`VpPmzf7m!Oz+H?Hk zG?~_ATW(JdQz)fc7+kfGNmHC05nA^*GVO-R|5nkgZv{E2CzKSIl^ zwf+2Le>u8jJyd^8z}NSr36Yq^r@}!+i^nm3TowrUah+O>mJCN?Yxk&%3R(4Wf|KYa zknZDNGHCA3#%~pFR;zzISc*}~VO1%OQl|`vCQ!puR0?)Kd}m|TUI(^e9`39lt4yBq zL1|yLkM}d>x7175vM-`PKJ!N?n+E=R*77%(&+qGNGoh6{_xPH~>Wt1uEkZP7vna}{p=LTH4gp2)*CAs586V{Yk> z(F1iPE%t|yOwq2BuNz|H?#RTz$a{7ju+VNR>D|{$Bv-ew7!0pMH5` zOhrePT~{+qcT9F(R{LR`N11^^1gEzSvEb`+m`FI1unb~xaH6gs4lG&WQCP2COTjyh zi7MHrNLk5Y-i)iosEBsFYl|l`kyki+1k310Cc@i8k*-w%v1nJfBx%Fb$ zv&3vRxo5xHoU9Vo-0dRlqDt%T+La;>hqMsfoD3$a_wSgO@cjTt@f+$c9bco?ZHi(zq z#evN;5ib|FaUEuckCE|g)hJ>h7;SG>g$fW=WtfzL_Lh_jJnU4cUXf2mXRmMeBi8kg zMeDGWQDP{_m1PY!W7`-=K_rbJ*hZ%+`eV4Y5*DZ?xonM+?Y~*M=AYbe>kU=awIgEd z>QnZM)=O-u2&Bx=1=ilLV}FM5eZ@46pvJ9J@6yacW~K=2QpGPDy2EZuu#}2!o@4T# zqdlEMgw<$f(?+a=A@oux=d$Y8K0|p`PPs2g@`Tp8%iTnE7tf2E8MhPvG!GUn zj_T#v=z|J`O!;}ax#0<-4A^x2Lhe^kqzzj8vun2hjOBSkNNEW)DK_#;tTyU-11Fuv z+VZx3=|RF6`Zplql!i6vi@*9K1X`>nrLj(ll&Zp^krPjPlMR!u7?Oi>O-;4!S9j&gJ*~y_9zC27 zTLuNG`A{jPQYQRk$7Yr-7@)%$QW;d9K?%V`N4&Dmo94~pu$R zneFus$#Kq%RR*p#J@n|H>1Em;MfiU(p%xsgcI5&=ns_lO_-gTtm1rbxK^V)vCd zVQ5DST-;k;S@Gm9Sj|sqR0VY2-^rf0*K16rAJ)fLW$y2VdE(ZWAI_ z`?3wuD=8seq7lx8(Hc2UtZb`kq`J^4K-&pYnECD`+ls6;&Tq6+|Ey?|U_El!+;&nH zGLfQ|>0U=oHZ49`c9uXT2$8CMm3}tgoYF+qgF9?v>d<@EbK6Cb%5(_rgUHM4J!IF? zB^fZbpM>iSg;Wh_y00-HL;|NTu6v-?0#M~VN{-z7Erqe(Pa-(tBk1YTl=4Lwwwnz7 zV$E58UyaXkTk?Q?^{V=tKi#kd;Sr18@1ng0gm|96Z2M(~5gs|1yj3OK-X5S+DTJBi z6AZ!^VCA(N1*_HH(3(an-}Iem7mxF!ep3OY`ER{hf_H1Zv23Y#=hdOpUCD8D*e3g~ z17}2iBzM!&uoFi@4hmR#P@}PKBvp|hWO}X; zWN_hv#8!U#kCbT4P@^`M)dt>k(@g}(?voL(ZShFvke%F20Ag7|zNOk9RMkL8m6{Y@ z_Sc9#@1V~c=5nmm>8>D2ts+3>%7Of}))m^CxM9*C^{gjgTDg*@&WK|!Ou-E1f#@Dr6z0ZeK`L8G))fg7A*7dq7NnRl&|Dz-I@fiW9M!V^iJq&-I5o3eIZ*o z=cPAXcHLSOSzD8wDk5;EbGKHMmQg?=j#d;%==S%Wl)3$Z{LE@2+o|LUk#A96ID7YI z?KpKpQys>ZWr)F*ub~79iKF9;kc1qL#ry4(%I+hh5Z-QMQ$AIc+5X<<{R&<;!4W&o z&{(eDzRqLP7>IrSc;ltmA0wq& zlyv>brN(oqMC49%%jUKx0Lll@KKW~(rMtp=2i96oALUFI_j2c!zI@z)SQq|FZzNXn ze26e%yV(Gx`DSVGp`_q9JTkDlleCRcEWE8zM1wDs*k+DW4e3#b!sQnP8zQ%>G=HM> zsBLv8Ong=D7xAZvHGh_|e#q~RyuwgM!+xSrGL7fs)}&P6+s89n?MuZk#-tVh(^W|A zri+2JC^k{dxfZv)pTt*~Dmna@wH=+9U%NgyEKUp zb9j045gWSeI8fJVcE81baH%^!>YZVc9y^39)koQQuraw0WzX(EiBjQ}$mHJ@zeN>m z2UjV91=R7Erlg9!w9IxgM9n(c!0}VbSvFrOs7$Rg99Y7~C1dU|R#SSSKiq#Q1 zw|ir(#oJvda`CEC&HEE={=4?ey;xMtpmWK1rF7y=1V(DJwWl24qfwDPX8utnQUeC2 zNn^BPqNz%l^?;9sydu`wfi=!NXIQFX_=N8#cXI@cqs?w!11xFRr9B*=fHHq}eCjiD zZhxs{q7%_=SM;h_iJRnsY9vuzVW`!?7By49b?+*W&BH$YNYbr)ly<$_&x&LZm%Fcf zA{RJL1{RF(AmOa$9vF<-!aR;qBhzYs{?1gcDqHtK?z~Rv$opOIib@=&aT**-4d2_9O_Qxc>q6MXQ{2^G(cnn1#oZnb zyGE`|^6<1Zbffz}y*o0fv$yR(7=)5_P2@IJvT8}(zLzy&+@FADtC?s)f6rSnG{u_^ zYw4!yihhWHf=PXFKF(m7nw#R06w$$EHUDm%!&XUaA$9_8G=?Y+7oG#*t#We7E+#Ti zTk|qh9+XO3Qqf^Tcu$~K$r`E@qc~}CkF7)YFDeR@BK(0jCAUDGXT!dZ;jB~m$=J5R z<42fH$EdFu50FX&_GsJSF3u|vz7B{!hgo@ZCsF)WAU?~z_f@+hR~EHHA$eg`?7XC_ zzOc($AD%{=u8yKH?V*gHZ4)O59Wb5cDc8*3QQ5Noi6r!*>sd9IIM|b(zEV{|no4oi z%1MvzvD1^SJLes%s%lMEA>1C5YrCP+^l?OKy)I8--fZAUYSk&##(9iwkmc#c(?k zNyfp&b&juDDJ_jqOGS*qdZzKV=SL@Ns*vA$KQnNQgu&?HTS>a#ucs@es;5}_zqTS^ zJtbVDOSuR;*8h77N7wa=wy6F&^~r8{jJcR||+xly6ab~b>oGWJ`D zuR`)g%9iS!!j=X6Kk@y#4y0+3Noni}K@LfB8@?13>s(BwqWo&^>S8X8iW-~!Q0Rh0 zefjW)XcO&T2Ku%CK;PbbiAwd&7G_i(WDWEb&gBzMUu(|4;utc|4M}>?p7NXzHag9~ zYhx{F(zXnvK&?>#&E9DCN<6NtW_0%ggxm4z0`O9Lh9OE2qS&;e+-;q3{k zGgkEtwa+>$MVZxZv~t>ZyneQVs@aFa9ueC5;c+Aqv3`lM z<>p5xAD+ifHA!SU^?lkGGW=JH;$CC9qF#(6m;*uhm(wuj*n23r((@=BwY6p717B+$6G{}}#7I^82L}2(C8&!<7BqJVE^#JNgoWH3#A+7nBN8m$sjQzR#tAZ9t zQ0Umabib8-*|O3WiLQ>Ay9)muEg$%Uqh2$;Pr7*u%Kncih8vUEyx1 z(y7o^TIZ2PBqZaa%q%bT`%Ro* zQ?;b~oY8Zaeu05Bj_ChSP0f}U{o2Ts4kL=Gt)?siS!7^84W|UcK217>Aq$KwMdYGZ zihV5Srm~t&K4w-NDrIX*yx+8p$~pBKcHz}QBQ&9vvJ#I)X^3{MGCOO>_AeyV7zX}#on z5-B`qO_?Ie56U_iiXN*sah(F~<}$kfVFpTQA!W6_cycH?8$W8u_J*@Z%>vi;DqMdI#t(snp3SfWRhYB8lwhqLZmZ&f`-r3bX{>7hbp(65rF zAw&ueRp-AGeE$qSyiGX#F4AiFExM)`XddGK|3^Ku=xi`!Gpe~9+Dc0Fhm49T>T&a_ zXr*vV2hcfCj6bMjtCg|a35eDi5bF~F;L=N7#{ zWV_&rCA+J?YA8t0k#nfdm8+o}waMy>F7B{b3SuPd@(_;GA->;havuU2@1owad#qUJ zmThG2#!N^9#3EB_Jm@~;R})7sn+@@8-G4WF56eL6VW!#|P-vbyLU#zAC0@x<#cs;9 z{^e7o&it0~vHaR`(-hywxIa3@1eq-~&#mIRQ{!nCG5H>htv`}9;uRe$J$9^kUovUA zJ%qG!w2y~bT0#z+^bnIq#&`>ozx&UPN-TUOBTBT3qD6PrWE$$eqtC+Vn2dW}2f==A zE;08AtWa*QlFOZM)R_$OZOpq0h4^~LS#lI__O&e3hTv)a$_;(3Xq`pvvpUE=uQMWC~Xz}uMh zD$!T4+Fw8}d$m#IOk&Vbmcd33aOSGhJDK72nnhb)VJ}!| z_EaREZvXgMj1r(u6&8jOS^x{*iv>aI{wh@V>qhU!Tj@-vp z2B-|AA=E#1ssor6^mohN>T({*>mPbfi&7O}G&FmPupKBq+&3))hcPXLRK3TN3{p`l z1?(IAOp?kOBwkT8PfioOBFjHZKcKF#JxoQJ76CI|XsWs9EZ9L}%@eaoL4L%=0!T*~ zgEN4R0fZn>{{9GXC)_YRh*A$iF3kzbs7DFi0#QWT%_f_k(&ak*z`W@*KynrRc`CpN z`bjYlMsy`1X2s%X2!_?q0&S)UEUsQ*LlY`zbjvyjOpo*2f7oIYGeQLI2igu55Z;f+C?mzYaWI6>eu8X~Y5+!fLnxMwO=RQ<`mV^%C+U)rC)|iALHao1<(THT&FV3dD(D*1^fI>5uQr;-f!Khi z^|qj{tT!5o$lWTg7Gi`XJR@9}VRyry!07aSx!E#ny+BqQntz#rRD${!bgV}T8)!3d zyQXdnwiqofm9A0b{snQDc5M?H2-lKP#7CN2yEWkmTA*&`ehJr-F~wF`HhwP0-dC_; z3E;4HWxB!YBSZR{g+t!MPV*AifQ#@IPg*as02J(RzE zp$ZQ7ZCXm+Qx5!6;IiKj35a#zNl44z>|4X1&cE-wF#Zz@@g;>wL77HX6}_2Lj-zhl< z*dz+3B^oo6mFQGlwu|f_jU>6Kw$lZ0A({#AE`^t?|F=}#d6>yQBQ(mviV68RR?mB! z^aR_GlI_vXqB2UrFrX}y7&Bbh+$TQzPhfILB9^6sB$LU&!{UY1VP>m#wrmAFHo!n)_? z+y{M!0Ju-bNQXSMV!8M!MRAy`zf(Htw1`ENLLm`Jmjo^qERIB<5j1WQjTNL;PTGxn z6$r}mph<`b61L;X$-SIdt!t@GJ~LJ1X`f$Ztel0nIPcz7RQ0-kWz|4ZO^7US(G|`j zMHy#2v`DcvVGO?LCeevwIA;Uk4X1ud0?y*SJLDlLE}RU2?A~tx@1hKm{-~hlCleLZ zP-hu9vZ5&Y9wdn2PDWlzt=>LNj9#%fNkBDv#(kRNQy$4DqD!TI#!yKl_yOo4&r-B` ztlR_AJ}pYwsWG4$SQOk*J?>RoDdcGm-T^_Rz=`wgAZP)QYOACOV)?vF{!lCEo}BLM zO7{3}swU~6(#dtjDl@h4G;zwn4AeB17tup7F13Dd7vs zqemp1Oa7s|Mzz&dg_T?Za}3tS^cTIKi6n;epgll4b$kf}Nsmmw!w5NTZvAfsFHoo| z4y(Z=IKw&k3dfD zQ&jptOz3G@36t07%&9fhX<7>myf<~+H-0@eT~K0KW&gVV;}l4R@1CxZ!BH)~^r~RD zKuo-$KhShB{ZCa2RVMGN6`i$sq}-&8C^qGn27j6Ko~K0!DJXfuMMEK$u-`16tYOUV z`q_gxR9tYD9SGo7LIsfTwgZU>Dm=2odw+?S5w}G=fpvKW70*H4wu9+KqrZw=LAPdg zeMF!9PTa?;E-36n=QgW{BjATVV2w^zys^r`!hlhA&k1j3qZuoqkzlEgq^e#qhmRNK zHT!nu(X;Bh4svEAfL+_>Ch)#v^G-jMnyR{(@D8;!Oxc1hT!I<2)msGzJC;u6fyN^X z@4F7xrO%3Y+1-S$3Xto&^NkUL)t#G@_E`Y}>cF87c7h+vI#f>izBpxDB$NTq;yIYN zVisHpdR|>Z83aq25V&%()dwP~s|Dn8YTX^+uLyHfqAxgqt=hPX`%>j&HbC6b?S5w(1ToHI$tCLNpAUIw94Qh_L)AHt|{!d)EcgaM(lsA}tDlF6CjqEI6Exckx*==FYjY;eb;#60aSM)gNY)SO4ujn#C&%);6Qob& zc%0>Tyf?Z`>O0r-sgg1Zjwv0?Vjh_6HvDQ%E1*{yj+9inNm8hP#zfQteiyS9z-RGtoyK)KTJ zGDyXf5PknzDjnt6|EhvC2vISj{p+-C(uyx>-_$5o{YIBQeK9(yI@yqPJ}>^4UNUBL zo>-26eRYiV^%azmN{`0^6%M(Qx{7uq#6fXoi^9X15M)`*>&95vLZ@GKPVSUNWQq)S zgn-Za92ljh5XPmu%&jl;d{p?PEVrDK+%!kKT|Rf3Rj##X{2Mxbn_8}W-FNiZ%8={< zVA~N}^&oZJEnivMCNW8>w9X>|(k1Z}B+XRUY?7BTT?+I+SJ?cRA;#X{l%4>pKQs3& z7X=k)uU{mNRi&Q9K@roPuYsVEs~P}gi)-W_$tR&(PIT>Ev!`5$iZ7VYM~Ins0#pf0 zn|sO1!3BzJesxGl5Y&`WreyeWNT==-vPK2(JPt6O@AoWoIOE2kqEUb^b$zNwD8t^d z&@NJ$14H0sDgefE*&PL(l~p6FS3L~5Bkj9ItY);&q1%Uahe-bsAj{~S09K_LI+Da& z5JW}^4MMK+s#rJK`b9St=EnCZmtMu>g!d7=Xvg6za0)-W=NsteKGLafFtcj+yhoc3 zk_VvVyoLgG-_AhH*xkI&ZZwd%Z{F05<6@ktT=!XeE4rKoIMRcCr@rx}E_||Mn}a`E zQg_Y-hf5Z^P5*CbA}m0_66O{7n*;SlGN7=4ez}%5tNUGO5yeg=bWw;EtgN*FedMz) z!2B6}b#n3msh8%{n^8~2oug*8--$ArZxZ3|3Nh9wX!OOZiM_ny!zm>TWKY=Y8_Q4a zl#I3ON`jmWDNMCiK@%d6-zrFPc8j=h-sF7J#y1dR>i;8tS=mp%m~aA|wcxzEdQr^RTS6oQB#tiRZ36pAYC^G?Gry=&??jTWfcn(a7fhMX#CXSqZ z+YPnl4g8;g@>0iNrK1Bo671W)kGj2l8V}j~y>9i_zyFYNZmoQK%QN+It=`RJ&{I#E z@pDET9f?Au@sY!cn9e;WtZrg4*AFUt_VW&-a0J{#}W8tUc-)v*`rcXc2E*N_PJ_n$O*m< z*cLFbq(lw)245bzv^a1WTo(-!h4IUI>BTizo}Q@L%~@ruZD)HOz3YUM)Xw}thsmb7 z)`t^^o%!t3f^)$;_(gaDQsUgx&E5rAh$}HQ*|9+v;@iTi)Q=ust1aco5B3vHs~Q$> z;1Xni!^mm0eZc+#Ziv2V(piWWSf(7#{#%7og$lFyzkvawhtfR&X2kE|_su~F4^7Wv zzqeh8{M$NR8}M->0oZRB$tz-vcc)C}RLa8XGbZ2YkhCX~6c#r<8vsUO@rPIE$WyUj z)#`lUZsH&)NK;{4*RzF9;Fv2|R+4>8`Rs?*HI$t6B6VXX{SJAR;%%GD(W9XBj)2x! z@jYPne3#zVxVJow(R`wna&g`T(TB>l4K!6fx4X+^Jt^FbZ)2%_L;e^HilG}rLI?E+ z5?QE@ zV{J!K7na(e+mA77yV7adIB9zw_{no~{g1)K&Gp2Q!;$oQ}GXf#D+K2eDuhCFJb=5$Mp zGKc|}-{G2ERk1ePBQw5{T@!!#?v$FRCL>YY=&184X|^g4O;%wLimA3atLJE!d711+ zhFrrw2EMp(MUkE_y8B>mav7txH9rs*LWC`7jH0SCE-?A8n z!Wx8kR!azA1XhrXN45L}*DdsC>6j;Gcfq;tm;7ne&yfmkJ&w3Mu8`8KT#xGj7Y& zL>Q!sPDQ~TgJ3cFQ!weT)X7&2oppXHokS)6H(M9R6bv((vn7<gqv_A-YwwF{3tX9+RW{Ora(*17rIWIU z+8zrqNw>JNA3{K+BNAP$fS9e(gAVf7VDZ-CzWUF@Z;dme&g;bOA197SaT~{4m*X5c z5%WR!lL?iR(eeIv`O8@2 z<&br#rF8|e%&+t>_g1nIZ3JFD9P&cFmH52ZAdr4tLvItKOc3WIEz!!(lz_{^kb|A4 zLdQSFgJia)G_A@&nUTKgof32*LhgL#=XGzLLCS#;OR*!;hWs?TGDrr^Y6WtHo}K!O z2TmzT`or1PR@GieUzAJe2*!B+glh&$_b-fk;wdqyH>Mpp}$RuLq+k2l%k6>W)*pRM_Glb zx*KDtld3M}+M4Kb%^TGxnUY^?{#OUYvAu6oE!w~E_2L8*hcsYBu_d~q{{fKl8UJq0 z18s6i_|y0$mg}*GGYK%ePau|gr60{8K+QUawzEVgoy`<}yt6#OtbEmNEsJ7zwB0_X zk@6sWyYhn&OnYrABO4KTfa?8I9@xK?JM|1Uz>Y|KD<2JtRonUZyu3H$QJ@$6O!E%w zB6V4po!1-6d~7?5A(CMQ(L%tzQ|LxyRXuBUVB6JBh}&sPH2ad){7%RZDI(OEpUm^K z)b#_+%9d?wI7zt!a|CUw9=1uu`buv0V}p0oCrxejwF+{3aZ94s+cuV|CFk2ZTT|gH z#l);eNyyy>)6)0)Ws^*(p(}KaTByet#xm~X2c;BSmF|uk-IMh`eld@*Rez~8NwFfRPu;p$>%uK1|nV4!?LJ5UL>` zc?=43Mx51OuN*cZV#FHEAIB1hpKYCp@X?HxhwRA9nVK`~D-IF|h$Y_`qf`f(^G{9U zVCRwUj$08Qv7EY6Ab`9)*gn*f-#-14ZOhDI^PyYbom;~Ob+Vgy(|c;!!NyZ9!v3*nmu zi;PB&iLfr^CowCgs#W`oS+s{7#BJ8?ncAPJ^px6u*wnkdXO@}@C^CefO+5>q*XNs} z&SQDaQ&`z+$8V*lWCM*~U{57`W89Dnl`-Kh>l2%c0%XKE->E^mVK3?INr&e9U*Dca zyIP-xrUT97t6!4r2F68q?q!-4y;9DE%*YD;dpPcNxM0~sHxaVa8TBvD<2vG)&0Tm=q- z0pqIVudZYo;X7l2DTmq~=VZC`I!j56@bpacgyjl9jC`~RiZP7Z3lIcyeIf3@402lJI-kLBBRKoJ^fMaz08tgtqF) zLIL))GRV*&4ri>DbEL!O5WI@tZl;# zMk7|tC#-74`gcE;vkcA6#FvH->r7f2-MIr6?AtJJ?^IzygaJc=c_K9A%j@wlom&@T zua+Su5z&%iJ7UYG^wb9DNYReW4h1v@uNZ?T-;8?68N9?W>uUBTSM~la_+jzyKwJN=k7${=x*tc_QFSFUhD2pU3gxYX zs88CRR~Ak<{ONrK$pu9UM1;>dnV8_GfIzx6ax!E|Cf4rKT*J8A>?=Y@&SGFTfk#GORt(*dLA=8^LxA?q@lWLDta%08FB_a^9L3Q3jC1VJ5 zWQ@5;*Fpl5+qof1BNTC{jvxQasV4P|Lnh5QQ+-e;%^@+B<*6CJGaqrtlNR}q85%F= zm7$Y0XH?UBaDov{8;FG)A&?!>#^6gBvdD0y!Gos-&Pv)l(+;$(S8 z6Rf!5jQI;Qr81kPECt^_Cb-!ch7bo;*x8~ga}5kt6~Hkgj8~Vp%ir>@icoS*gu+8x zp^pJZ>IAqtfJ99NMRZCfQMgY=w_V`o!2K}!VfHZ~GUiE{1@KM(vB7at?)DG2SwZxj zd%AQLusaqcBc3sULl6%I$23}lBZNueaaXVFpiRJ|@V8Y2dz45HeevW^0phW#()R-2 ziPFb}Y|xYB&vSMEW?znIU76Lgo59^Izloo;3`+>eIWzl9HeO?Xfz~^7MUe*H1)50j zjZ$bpK6;60hDRnz=CK{qW@OVGAZ$;x%UopNnJFGmVXgyn@xC9LL=-l68OIwQ1UbUo8VC_P*1^H3?)cRETZbr8mt2D}oAp|lk_da92Qn#fBa!lq%wgPgc+&DS zk}ezaE1865=6HCRL@Z*&Yk>Cw=UE{H8<2?w)b3J5Yl0={r?f#utKG?dMmujpV_+VnFdO5MiBsF91*|w z(2FxdJM94z$o<7{IZGcMn5%!mKtOby-6@=`OfkqWhlZ_z}kq?;*eEggsB!z z!n+{b0sT`aj<0qs?e)_i<) z;<|(ifN4$eLi=KMGPiTbt21Ftkw^giylSb0+LxP8$34BtgHQN7(NMCn0HOAyr0N(GOYMBy>syw)*BwN+Z8f9uj!LW|zLv0-uucPa9gH!BGZq38Ba zd83j_$kb%Opb^q&LDB9U9Ji8T{z3x`65eE|hrz0DqBKek5X^Ms6mc38WN3$SXT1wg zi+W^+atx7TV6m?wbs!}L|E12tXD#UOGKii?*IK7KJkAR z$#yS@id`s{E(xAv^7`>e0|ifyMKVjN54_L6G8Jq{ni?*3%g&o6+ID+vYr2lQaWXE&eoQAvql7wLUzFI%a6wXN=>>j^#ayVPx;KULJIB79$?^E z*-)mzRXds8L6}cl$Y&>WITd<~D6Ct|w4ix=q*;XaOYwv8)fm53;t!G{ozZxl{eVaSEjqr(d| zi^!`86k%J}2rq9hM`~6eQbk20(p%DTY^fs`8T8y?$&5IfU7}iQP>Yi4XKOM=64=&r zJ0igjgmj26o96h!lU8wRUzn3Y`TJ?(*@6(1jsH z^|d9$;wHq9CCo;|W+V`tAqm0}hD;DMj0m9!K@egY1|tOEn1&JvWHT_-LNWU-v zF`9y7Kp{b>f;Je)CJ0C{)q@fkB7`y!gdz-#A_#;-A)t*0revVr7$d-M3`Gp1RwiU6 z2vZwzDKaKu4q?9;z7n=-Mi&vnR8AA)O_{(909FAi!MP)9H-adROhG`0W1e%GXA6d37lZOF$Ye9Ul} z*~xDL4un-FxfI3Q$uq(Vs$JbGl6fGcD=3E;xaJih4h$8FL!%;ug=;n@#31H0QHdKi z6PX2>h=V~9xtRf!Aux}FP)0D6lZYV*F&Wt~oDkOr97t{%CTP_RtqkHpLUdG!!W7u3 zq(V_K+4M1uNY3Ub2pf=r!9Fm^B4*6Nvj{SXAkGAWGYJC>$RLV>41tV98E8Qmz>Rd6 zB?DG!If&y3;DXPtqkUa3P(X7p@S_}}VQ{2!cl178rNs+6>@&r4%$Wo$=R<~#x0h2j z#PpkFNTy6CWyT*7nnr*N=AUKY4~ z9A6!5C>@SECOYQ=k!~U31F0Siq=hBiBZfF%2^xvR5twIYj1b0w7Hnof zXE$PoI3gqw`J_T3w3Nj%r0O!UOsh4bY`d35VsxXXEg_C)`0V8 zOytQ@0~~CuK?)HK2$DpG5}1oKbRgC-1tKIyH;9VNtVa{2sb^v@2z6-qF;N8$!jeSH z^B-(U#P1UEGjbFL0C0f7q8m`1D<&Zj!wMM+VX*`g5wrw@4ahQ};oWpbR#7=(_TV+3=YB51-aK#C}JsD(MiIAt9IvCb%^6fziBIE5&pxI!4@I}A|- zFr;#w3@MJ{6;ebHt}2lkl&ohnC&WDxRiYHU<)Br#hX`A}-3Yob$J9=sf#;z?oj78g zhnI4`0E8iIvu^rtJ`Ugmn_bS5gO>h~%g} zRD6jm)8q6p^yhw5@y0l+rFSFG%He5CWGJ9_*D-m>4^KMdqmNYYs;UZFEPry+ycd)~aHFmqvpSRh zrES?owXsYq zPc^P*pIP^hys=r)WGIg9UAyd21vRQLaEHjK#YsmIIhp9{c!cslid>mDMqi4)#(7cP zs3Q?h?!vm9MU>1z(pMQoAc`>xV1fALQOrY>bPU2ALpbIjrWl0?Qyg^;fdnarA%`i% zAVR2fDB?I*aLh6eA_Nhloyrg<2r&o{f*^t#1TmR{8;BvOgfzg4MrOvWNs$wVJ0g6_ z+FE6tkWmHq%XzTDGq~}|L0lX;;SB4=u(I+`D)lv1B4aGgVCb@BIl;z z6LQJbMY{Z`OS4@xuF|cL&ZHIPa=A!#imeEZ7~;WECm6GYB)WMd3!{aE)wYV4P-Q#>Fe3N^+Ahv1KKR)B>5ugqm2*B9t`UO5KeH z>Qiu~ULXtgBFMu=Er@-j;-2!-cFQk2A(o!*EmotN||QRgH0Sk){j97 z5~}X$Yotb3&Pq9k7RBMTA{3rM$pt()IB-e6=oUKE*HCvCQ=h zzRndhk#_c<-ljH~SA_HX@)BmN1YMHfAF)@9UwrZ$&Tyq1eH^-#oX)T{9RJ*@5RE9L zla3-riLFtY@(FsUMaYv$u45#=Rh8@AeU&R6XcX#G`p%fsO2C5_s>-^addx@$LZ%-O zpHVTd4eyW>fI&HMtpWkZ1S~k-n5zZ;y%n&bO5&~(NTo0FB7|UxM2;Aer4YzrO6=7~ z@S{jHOUjiOlcN2N*2AOVJ|b~Z-2rx~L=0Jc;>3O+98mxGS*#NYPWN~e`GY(AuRi_a zEdU&_0&O(}(<28!{N)kxvIOJm45K~F2qQs0t|qk0dsxbl;Zv4m{sraqt6}|%f~%Aa zc1Ci>TZZF>~+Ex z!bU~pm6kTVCydNrfz@>l#k=t=x%0Q$uI8#0p!Cn|dQ(Os1q(;&I2y`u2B^!!2=^(? za^vN~IbFKN9c@o1fo9Q9#wIhF+63MvlO*DR;%7iCezfq2(lD75`b67=RE9!?hpKL{ z74FN{LlI zU^&3!f!)DKfP#QQlia+fLYXZ_TW_aKT(nC8)ptF+FA>z*S{xS_kmsbiIa$5RZh0ZQ zgqLiSHwG;?+4QERucrlcB(9SYa>>RBDL9Oo6!y$r2V~I~N4FC!R?)T$y5;2{aEIlr z59$xNXapYhr3e(VQd$FF&i`LvGk~Lm9}9%n3*VO+AG4dba!~<~ZZWWe%%3kjxpH^d zTP4@+Bn*@!LO{CqPxM_wHY;`&^-oAbdop4*jfo@*o;o5l;g`19@fO958&E<FJEO$!;mARMUI2G?C6PDPS$;$b&Ky;6_Y>3>K{5m_RyHE_JQ?rOzAtUcNp0fAp@ zPi|5@X)hCnuZ30>@gRKPtQtLCRm7y$SL8>ZR&Ea8p5CLSkbiS)I3Rd|FAgPU?>|Fc zTjUh;Zez8ewdi@1%o9r3T+d!qEI5YknZSrP-sGRi+gtSn2^8F;YNmuKkL=$kuZKxK zNoGt2Qx!r=#fW8^jco@3uQLNSgLDy`ft1t2a+aBlxedy{|C^Y)y0!)D5rx~*4UrRdKC|~VIjPs2g5Fd}@qIqo z)z4kjY@rA8d2JPUyEF2*5d@Ra&H#h)(Sn-N>Z$T4QD;(7qtU73>X<1}xl*XjS1YdZ z>!Om2qeg9Rv`_V=XRtS!X%7D8w(%%egt zlQ{7$`#xBd=`0_+dEDMh2ekQh7AeXZAL)I75$O7%5jXM$Us%Ur_?i5jOAGc%B=ffaHO&pw)+HmJ{r zL_H90qV9J%+LB4Z`L*Jr>v<;`H zp_FHV?UAf$+>V-3%QULGOVmuM{z=B?_ZW?eEf(Ns_I(5_ZgvxW!|qzIhqws&yBF8y z+AUSCqUU9;D2d@Si=?G2dzl@I#eei*8>v;g%4vRLmzD2WZNfjlY}Fw-pinzp2qJjY zb?QB2yduE0uGynr^UcLkA3h_+!fRe8imJJjsC217$D=ljLeKpFZ4{clEREv9%MQXn zl`Pl2Akp~V&fG+%46EySOhZSB@|TGb%<(dwb8o$16lL;RwV2Gxov)Ov2t$Ssn7rD> zE8X4pZ4S`oP29{7^JEhWe~MnEuYiCE_NYM$7QrL@QJIO*$s2T>D__s<-QY^WNe@iq zB(q$ptmSHvbx$|Hqd`BZLsH1MP5uieS=Ks3OP4U_U2(Zdd9p3a4(ojUu@Yke2{0^^ zmn?OTwsg&|%GW4X!3Dd^cCk>CRI_&pu#KM z;8(csxis5epjHGj4QW9F@TNJEpIT{@gpmI#Et+mEtVE{IV)%qJPb14;l4rKuhrms~ zZ@xn9$5e}n9^)9$%6CAAT0b(ydiw7KtgB_e8-$IxY6i(H4A%L@!4$ky9B#Ls$s$t) zO{vM1{F81ok4v~sazb8fFJgT^jr+B{{1fFMzE4l-|1XIoni%^&Byq7T(U$B1mov8F zK+D)yVnr;9Z;+|9l1Hnv_p;SJX&|ZW1^T3GLLxdU+ghgqO|(%kU)M`dZ$|{MZlCg- z^ucmPQ=^{8*QkcB>l=vUg*;4Y3|gORP0qbO#Iz0c%AOBV&h;c6_-W*Feqe)(lu`Ac zsgYM(==3k^bcs2*Hf>N}qqk^66ea;P1w3Je-*a`$3Jko)og_YeM0%%o$Pd@>_0LkV z0Y)*pG#pG9E4-G@5~30hbuGrp{83vq-F;dmI;X1klKBS+Awr0lKK}J~70}g@gJq7? z`5S1MZf?-j1ry0ol~9@1HL0ZN6sUd@l#%U<$p$6v3%dl@>#wO1oF{khsjQyz;RiZ; z?hkzyJ0D;wNw{pcMfD^C;)D+~>ln~(21`9#)qC1UbJRl^AgT*Do|Zb3&d(h1N@&`u zKW3nksV@+CzY0>w1|qo#`|hJGX~5#$M#9NxK+>I&VbiE1Nph*Hsw2$nwHuhVj6}mU z7pWkv)AL9nU5WOYlh9ujC+vietq@;Y&qns3#`aX!Cfp2L@B~4hi z_eg-kHi6r|`=gA$;HB5)aoq1L+fjdtGHg1a{meNcE3{WWTaB~w1&C9yZ+^3%36t*B zQ5LSnW=|o!mXOsvR}3)x1N58!e19dsL%H&@Wc^}vFjmL$AWp{i$cZ3J>MhmDei0rHm%5r5vB|`WKV8$E$ z=w5CLs}@YCDvS^b>Is^SIw?4C>efnD)6J=bPA|lTtVW(NlSnvvgOedymXDRh$ESchoyuQJLgd`^wWS4Yxl3DK*#N--hW~OOOB^1B5 z)iE7k-kN`V5wi?HIDysD*SzVS6AGF^QxZor>Md6VwSy$6zC`t_<82n%C?rjbVk(sD z^=8UYB8Yg#v5#JJ*U3a;ocs=k%=RfbZEax7$1m3M*{YpyOWhhp;v0X(#!zZ4aJb~7 z%qe_BYKb6Ok=3~Lc)WezmCoEUw=o%vyeTSemSRAe$;3JCw@ik7oGF8(86vaXKfr;B zrP?_?weXuWx+X=ApmJ7K&^W)mCK8m-MSy1)e^-d^Vj4)(NJ`s0*!G5ES!MS2ohGr# zW~bsW2&P#{c`wpTl&szT4yPr3nNf~G`Y}g;P-9JEwMVNu9jS=m+;(JN54_vyTEMC! z6l3KCh|3%miOLM7pRqqpk>H_-=PBNj-Y|JO;MwsN6R*qE^DT;zwhD-#hq%xQeU#hX zE>pia3H16=yFz)(#E0Zh^ESnL?0w!En&-(r zlr%<%(MuuXR$qeJnliPdTSK8yFY~mb?+%d-tMCCIOjb?yIZDaCt{B=eR-a$DkQul+t<&O+0}1a-(##_#gHtRN-3hWGcv~g zQg0W^5D;i)&$$%iE`OlWlYh3LqYE*yliD$BTC-v)22riKvn?%Rl6+XIBJ&?DPp`cZ zuj^Xw%led138t^{Py=?IU z(VaCB`JNeTP+-rVbhfWTm0kUg`3LcB1CA=i*Rg(}|Hv0=em45XQfqq2+u^&e6tA_& z#lm0he$qA>rsifwuVmvYQ{ob&xiD)xVZ@M!=@+&XR0j~C?)_% z?^&Dsy1*|o?U7y$?d*QHP zxR?4)t4zM@m$ZGepZvHk;ePbCWtCON^p|!Oj zzwf4$%MGM{ZbJqmCVz?gJ$4O4eQd8O_eqF3;kY`xVVwUB+IYX30C>mo4Vus7X~N>>MW0 zIZjdS`JDaU!qu+B?PN4vmfLMe<_rnk4;f1r%{I_RaVqCi+-06j3aap!D2%nyQ8A9p zc__=JXhbx>c&1iC&i5E#nMGA?p)f~bfR$HJ*iA^P#E9;hf2qyIgo?;Mk{7Y&^8pbQ zmv@jtb36NJuAJU?g_=TTBxB`Z-9B`gU&!m|*1wk77oCz2<$|DHMpfx~QBtL|0r*?% z^6MAAvVW$9T9!kjyb;K=BORaa9oA@C0V38m%iNvMX>POs6l1a3 z&`C;yhWByhCyh%c_Q#rLV45@V%w06GscLqXw@9$EJ7gsEj8ZtPxAVM!nvJNYtbOtn zpBwJ^PMjAS(|Cz8fXPf}6M@ZD=e=gSkA}OI$lRxMWdBFza)V145sk$CvHzSlSi-*@ zEpDSF5CoPY2?R7*!pR4$6sbAP!@^j+R{uZaG2Ojq(YWlf*bgdS5#UJV1e3F#};Pzo#Stn1F zkE{+sS*@7r$GG5Y3}FRN@S`aZ;lXDw(0o!l+z+yIy^mrZjH+>K4o|g~Ga_p*;en`| zk?zQJLpMMKmidQs>vc)&ROFwGiK<*t6$QP@ql6{d$V3J$QmUsC{SgMRx0Bn8PwocL zQYhws0qHXbDOIqkC#N8S)KD?10!k_=5c<{eXS7~p$?Cn}ae*zz zPGaN%KWhNg31po?b%y}H?7s5hq{bu3nlkbZfGdXwwXgA{E;>kjYqK9E!mak7wIq@z zplU*YpKeIl@}cdc$8xlrH%2N6O0guIwHx3uaHS4L{%{!iq2W{gGBTrjC1IM$ljftj z#L*O%jUbaK|Js$24K|s&>QUEMTX^vYEiRaZ}-&s0N+QhVQnW`h?`ci4&UXrQPVGv~0PSXA3UffW+N@H7o4O0KrXJa>HL z*hyl8dDNV#Cr2v!G!l~fL(VCuCic0y^qFc`U40@m@~puJ9Myetmv0fAdozvRf^jT{ znyZoXpS)~YJTWPf7^MjW*F0`WlU+|gWwQxuTVMb6nTdv{-M`uo0F}`NSO^iy*#3`R zH0!N%H53J;CAH+Vh=%FEb-dii12>U@46uUT>XQ|GjB{`H>7V7}iyVK=f?1g?npzUe zCgGL76O+=vNUUvpIpU_KR7DtJ>-3U&?so7zA39-^K0f0rJ<~-<zq zEkCPpv&wm)4Re6?PFmTH-LWN1NoIjFjeV4?e)35RFVAYf^L!Pbn#>4{)3RvcJ7~nz zBkZUqIJg`4B})#jNnX;_giv%P_@H;63r%zn%J5&kOXiQ1Z1K{>ohLrmi`EgBg2DzS zMr4C`@;&hQ#jI;S*AMW8$=r?@ot2gsdN z>j~`N?Oqb~;LM83GnXj25ON`@3Y@xxksQ?Jr1{suQi2Z~xs#M6ukWs17g3GPjCJL1 zK(%RmcNb{_#Yk{I=a50+nEK@I8s?6l#z52 zSlSf&m~k|y8A0G_d?rMA?QYj?DGYI=sUmt=rKcp%$K))NqlDydD?xP|ykL_Pk>@yx zfCy|cG4q5@uz?%H(Te^k`6=3fE_zGUb~qUwUco@Ktv_w0u# zMx~LQ+o|-;(`#h(eI=s779QT7=^#V>o;5GpjSKATW4)Xj^pNc=_!k*evadcN1lI>D z?t{$Fxab~Bic5C)SmEs?$c&#@>Oqypq6HI4vgO;y(8Md5YPuYGUD|8=Ijdbw-^*fG z5^p`iBCe^-$-e2`r9?(uFy)!~R*tep$QeZqC3{u?A)w8JlD&}+>!HATr>cEcA+Vz4 zA8^XzQ?BNAMe~6ONZ{)&K2{0VWdWmJN?3p(Fv`Z&HnI~0Zk`tuR=G8#$q$ZRVvAWi zYzSdEio4bzNVcJg!ta?uPw#aRdQMGQ5u{o%z6NkyVKFBdmikv4U~IisN8Ay;8JL`& z$?6lQ{7$sj46!Y5Wgn6bq*su?E>n~3CwMouTJZl_$#Z%y zcLz6`+5Z*U(aN&_iEI4$!(1Z3D5_H`*?s5AvYs_~CZ^fsMe{kDnX(!KFG{F{G!Lhk zwJd`)_uKl6N7;xmpCXdj`lB%$EUV5|PkZg?X6`q}|A)5(w=RlnqFG6%t(vCDxr$yB zm&C-=T1f&8+hlMv1``vPQ$n&$z{dj1R`UNQzojKuCVaR2+wmpge zY%Cy1aUh9W@Df7%t_Au&0g2>Zl2O>x+3wFLMpD{Ls!fWYZyjPx)Vbz~_*1jUlleVUq;z1o*R<)X zKrHsFZr}fWZ#noTYH;#NeZCoN6RA+|px=E>at60$Ye!|1s$@aQ#(Mjp3qUy7_k=!b zn{&_A5*O0ywq$d?gbNqFEM)kWYTbWFjs&X`I73z`ehQ{p9_gK9OG9~>w6lmcE3D_$ zqu|alAD|KNT?YhBg5|^ylbhtIS(KOOJc7Pavvc};tvobG!L@?7gnBXC`I1qBCn&$p zVlUDH_t8C9^;5g=!*PU&&vHjmc7?@e1>g{%xuiC=7g6b^U>tCJVf02>A;FhoVZ?2m zzT%!{HjugE2b&c`XQPV|BFFP+pqQa}gDebyyT??UW^rlduARrnrZQnK)$sR`k|*W0 zw{cP!eUOV{=N@4{%&}m9f3|hqa^%J&F?s?Ds?BQp!-?!AxzZPtKm2yT{wBZw4QKy^ z6aT^I|KWfC@W=AZoL|W}xrHR`{|}&7iXTIoZAC>NtgDPqHd{}EHDsV0Xiq^xLMMz8 z0*v)uFM+0Hc8DI!y2I87-t9bxL#Tvb*jAaA>PRhzg211|{h zFzm>JT@WA;tfbLrT$69KH0%vRZe;)&Gz{TVa2@w#BfcVVTDo9Aq=xB{8CA_&56f zJL0}e`XWS?HNMwk@!lv)V?3!6mUpKTohN2v(88h22p5{L(;mJK_j$!SneZx0^eeUl z@==jI@v~QzILQcwLjfAy!E2GIE-e450>zrYTdL0pzR^*)jWKdoFU_#plQ~ARtYVj5 zs8dSUD=61TeT#9qS~Eilh6~Yf{SAKF5egV!gpJY+#lQ z@e6@t6ok0&X_fIOo8*?E!7Q@Pdfu{EFq+ubCbHjlmnbp`rQm>p&1|^PNL(>ots_Xm zb}~&O-C7V1pZCzE^;;F?VFb$)P@+8k?|+75vV43sOIMLc>PK|mToSk>DLc%Ig(Q~z zc1KQ+HVF*UyjXhk`7qfB<=I^7JY}OqS~+Rb!km1n%5^#iHR&nh#3s#kB98ofWpWhX z`E=Q@g~DiLa_zDO3WP&HcZMO3nbHbWt-`gH>BIC#yZ#?2q17(@I*QvQgB-p#x`iiF zm*LvUDJSg@4wuX+mRrSVK=v>`$e4~7X~11a2#4bSxd}3`5e7oj(P+bLwjlP#k@<<9 zWY|H|;d`YUl4_KWJ5Ib*IJgGVNW(%d8zx{Yx|ajNS|!!cglL!?%lid@S&u)3i@bFc zg&rjkZ@9{gG@vXVS*)lM{-{STiJ;VKH0>rvp0Tl(pg0MWZ#+Sl{MOW2&?+)bKLa*` z72lIh>Q<}C#b6|lEgK5Qu1b=V%aa9~b&{)g?IW2Jb2>H6xis$}nr$GjK3XcALUH6V zg$7Q)*u!R|ubV`KQ6enGNZUU9k`&}sC||iuF|)v6Bm~oX)gu4jh{Om(TIjOv9rS53 ztcp^3&?d$C+ElGrGF{Fz+}N{Kqn2qxYtM4Yew@s%649~lAr6ao9VmnNP{T@r4!_q4 z*7AtKUOB-Bf_Rc5gRz)1lB=0B{?yKD4B@HKO8YTNG;3*`vMZD$rQ#f;x4NfrPIIJ$_* zNCoYvuqR6ZiV!{-f!8H0eUntgD+#(~sULl1vR*`nXU8+GQVU3Z z3L1Urwh3ZpYT?VS@9?Q6Be(8a3;z#Boe<%>u`?LE@6d8$3j!F0{KL)NfxMl z6-gBGMjKFTIx{8X5%U+#*XEOQv|0N6D0>UkAEjR^J7=ehrkRB#?(XHnbd~*%II%d5 zk75SthejW^hLcR#dJnl5m};xLE&)X9sB$0g_=cHW5XfQph!A&Y*yUzZWCWyYXM`n1 zT79y>d?pAlmw#B7^L#-bLrR4maBbDvcaV>a#boL|GTG(bt3pJ??=8^sFPr_!^;3(9 zji`9(>>Q-lV4ftij_emoIj)%$kYB>-!ikj%KU$>dpnFA_&(qJiU&Mxmn9v0wrk+j% z6_`p9;Kmls5wN8U6(O$A2V6PP_o;#a$J z#PC616bh7$po7auD_4X!{?rkZ^Ol`KHyw^f7g86FNrI#{vkk)ew*`(f=QzDiCW3ar zg&J%2kDIY%xK3%O$#$+RAv|L^i;G2VBbThAm~#qbOm)yw-O3+jqY#ldlA!{E6C!-t za><3G&al!5xJ7W|Bnk((?o}2h^Hl302>vll>BMUUUvFq4cqIw(l|)dqgcw8W95|zD zsqA|-@pjJpZ0`X;l)~0NTs@{baq(75Mhdlw?mXb4X$&Ma9k~WcwQcf~52q9rR{K^a zo|%b(tu*J`T%0?dY*QH~LXv_6Op!xmXqJd>Y)_`LAaDo-?KzsVsbE9774s%sL$Ww$9-#KN||jE z)crtJNQ}rv>X=2Hs|Jlw^o>y≥F?bI_**4!x5hUvXk@YdZ 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%0A`i8)lci98?Yo*HQuYh4fF#VAE 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ם+JECieW)2)WG@VgT`KiwC7#Oi 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 a&;mz0DB(H@zld-IGrOK)0{;7K^ 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

R#bro>s-oP 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⪙bIFo#}L<~m$0Lp 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 z

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

rcY~ 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�03hvlRJ4~RdK3fS}ijJGdxRLd%ctvC= zu>P%qA;AsiOiHmNxO4<#xhi;tnInkt*9z-Shenygs%$Lo)#?Frh?8h=-pyc$zQ41b z%;*{HE*U9mT1)CjYIqh=*ofF@2?7vafC+WZCb%X?2M+Cunrt13ZlQ%167A<;J+KY+1tor`aP52u@D!P&7cdP`I0lS`qdzKg4+W zwe=q*sH{0YrvG+fle88)_a$b!uQxGa%do-%ZfWr8V^)(;e$J9-d_>#W14pHg-~Dzy z4>ERyU<Krb zS~(7a6YRI#r!$0^qzM>F^PNZ5VD^C2z0<3#C*HJUf)L=^OnkjKJESo1GAf3v1pG2wwRLM%w6v$;uLx|n z=w_l8C39f^$5#7aa9@a^L9;U8JaOByc=Iu3Beb;b9-UZ9VHT%aK#7#CMlB$F;j zdUyD0R&(Q{!VrE*b{6$r#O9(T8$h~jAtD<$?Mm(r6g8z=F`NqH%%vPp(~Fl_igwoE zoThSC22q~ftypV8OZiBQh_3Y_H#=snhVqF`Dbs=fC8}w?-~J+&&%*M`Vd(c%%Yz2z zaFNCUNwe_paWssLeJMw;uflsE@m$j9a%WE0@c7H~U^Yqg^RmU}p5aJbZYfi; z74u@+W$_M323R1al*B}s4Czk{y2&>C>TD#3-dI#C(wg!{Z){Ebc)G4H_H$GwEn*#i z*-uC+^7d8(IhucSAITk?ry;X9j<9$|#K~)5oDm5|ogs~yzf|V`q8noD`$U;6X6A(8 zCP}kNIb^%b`Juhd?}L`HWeLCQFR%x~TvB9Il9% zt6uq@oP)UN?rJ*z_$TKp!dywN0r4jl%5%OK~paD&?g@*2^( zWpKtx$k!U~fb`Av%@WJIs*6Sbdeo*D0LY>_ z#3r1a%kSvg3J3*(fAb_^=fTKx0!>{hin*kz*d(AYCh}nup6-_pYJ?{9?;>Bp@{`r9 z-tn!GbL33EIR;hA3B!UTKT0@^3O*R+a{lBvTT`l)zWm4GC2o#RDb zO}d0cX@MQ(T`^YZ0N(2`Z7srt{o(_wo|Engg-%gNK%sg!*&6RKZ6}dIe=vtx{bSaC z7O)j;scQU$P&t^C;kbON#!PQBO8!4D&{(@Qs7l_z4^d<3Y92?F)2)cXlhI!7FXYU? zxl*Siykk(xLbo+;67cv!qZ;udSY|10cD5jVI4O9RI6#xRHKmi`00CqqCE3g0!_n5y5S^9RV$qi$&!vr;0 zzFZ+duzJxzty94}Mh^US-^#v~)63Ngl&%>wU@CHuNK&zbJgie9U3GJ6@|awk^lI;7 zw7(Vc52ocRt0Wl2t2~Jn(jTg(GFejP12rf$Jm$`?u{Gb*sSOB{MN~1dr4iFoo9XcH zLiFrbkdYz2Bu_JVvU<_1^$RI-&o`pKO2CzT25Cs&rxCaGdyk|0uN$(2?80UuYN$3< z;qoG41xAUJ9Y~15AIzH3@qBU{>uKNqmZ4s)T71!=!~Dp3d<;{8#FzyXG!PJ2ELkCf z8hD0OmFHB``>VRSm;O|)NVf-XTJ=O!)EL$bPsgsK(xIo?K{4Zo)E<_Sk?<_|8g3FA zFeRb{_=myCLD`33%M`^hW5T+OQb$dZQ01$)6w{uJ#)=S8sD%bNjRfL15uykvYskZi zJ<5u>l$ zcNId=^iq}1;=u4K0^{{uSydNG)tVCx41*a0AVUl@1{h(CF$gIF5JQ|+9LF<+QIzFj z5=65lQqM%qL~KslXYD$eFNCOG6RS?OKd+KtLi4M?Ej?ALx!C4(l59m1L7Wh9A;5Wgf`E=1 z7}<;{B8ghJXCvjl7oYI7=W*02MHJ$W6r+wP=LJyi6jukpsufD-?@N6AmJcyjRheQS z>Nx@sVFz&K4nRW&Fe?M75rLdJLlkj{2(ZZ!&UZ7J9En$>^JD4qog07Jl4MPvW~A1Z zx03d~WvrF8WcfBqQk#7fNu}2ERsCp`SzgIvB&#w=LKr3ugdoHqgc}Bd5W_*BZW0Lz zjRsS5Qg)#Cm}RP^+`v}=XJ;k>%trK4v0`_(nl?RAj~#w(P9C`6r%aty*8r7_Gyn1ZOPE4YXt zP$vlnaGXn|C%d6h{djs+QmOe=JQdD(4uG^*Q0N{9!_i??SE|*5S?!uqD9$HhW=0a2 zgCPhJ2qB0egdqc9LcVr0#YM2Ki?WML?f zLO~$eiHwYf#)QVm*oldoiO7*7a#oFGClYqED1wMqR91OVq*-F59B1<1N@M(ADCTpJ zIUWkd@cjz;T?a7Jb2l;lcT+XZcG5Sp`4)zHo$PmzsAHt1Qw~__!Qs?v>P5qoT(n)S#^oOvZ>TRYH=rhP6G{Sef#{H0pdRZ5s{!OdE!Ybo0MP`C zzR`9p>l9_FcMUrAFQztv+gS>6h-id#=A=^4F=!UGPGaPiR7x^wX2l?h>N0B$mhqAa zRdSPU=@TJ0+KD!*O(_{;ZQ6x#LinpOi#tfYc<#_6zb4y^x?qZLD%m96G?@`5T^3#O zFOZ1BB|;3YG^?ZEA{1p2Y`QttM?R>gdV1=JSEZtfi2Rf?qD!@`(WLyAVo915US*ZJ z=W1zGMhO4L2=r*L!iVlKRS>|A!h$y(Gw`APgoNr47m(1tga!0)+>RZPfVzqk;)L{} zb)ucD`dR)ms*Bdkl6rPJE|%H01lq|IB7CB6tyAX{DqH479c0q2oK2I9^x~+BH$oY> zf^a$g6xswXqmfXB$R&pf?GVCGVJ0w>k!j76N(c~Cm%jHCA%xh|ZbdaF%N<<`lSviy zy7JjOJO1t4v((c?y1$pn18_zC#N9GxlP`&z^$(?k?O;sg3cSFmhYFlX*ykSDoJuxElVYVbug@s_Ltt${ntvgi_~s6TH{w`3RjRP zZsU@2rS3?Ww|u09@=`Kp?A|eE@ryT$)UZO$!rw?y*UJ-RqOX!y>TkZCX-nGC$pm7= z5ivB*RdH#N9D$J1ZHB&F5V`P6rooSjfkw@N2h4+wnFpOV4?aWSV7uU>nh5-mnx=&MP*y57sUo{U^wM`|cWQ2_PwGSZhy6>Src`RaTG1gy zbmd@rdKjLEzOa^PFh|FmKxtD4ve1ze3L^Et&pK-9IJ92wDy}ZS5l0E-iCn|C*)uXe zPvPFd-P^TG?&Rpt%#%|oyt`~W_mOo$ae5Bym&gexhJ|^CRDdAmE~ZKu$%S zV6pZK*X@cSG;G}^r#f8Z(Grs>ou65#vjwjmouF>CQCa9(bDN zGWz%K`|-v_%VhOR7wyYYdOh4IiipNDK zAAU(<-x#^I&9TOkj)*B#(aOmMXulJXPV{{~VTW*pPvIyQ_Mjy;EErV~D7izC^6bq@ zHY>8=qui+rVPd?OLR>^*e$ItGmI>~fGKd+jzbviXBIDqz!$NqP&|!0?~NP%$MOn zs6H8OV$;-nq3Y6I%t_ZU5Cefjr-HEwC{B$67K(}2O?X^Nz^qknsZLq|}~kYc6vF3V`v5J(nP`59j7KJqO=Dm*x$b1bV} zLnbt@*cAS=CYpszr062-$FmtrU9z!tcVLge`BZQ&68jWGZHIJm!kq$8hK`_g+JVtN z&Ws~!TqIf=KM=l=hw35ccD32eZe8SQ!)B@HC_EEI2QK_6PXpmr((Yc#xJw#Hoq-&N z=236xCf;V>9`aC@ zc|g&y-@0-h#8RSbWT7!y{k{buH1^g&x!#aU!kTN?D!=vgcRwqi;-MjeIZ#Xzf_xWr3 zykixFMk?Hw6R3IRLHt@{Ko_U0RX5miJf7MFdCD?uo!D>@M>p~vqI#9`dWF&aw7^)1 zl7+8UP8S3to6M7cmig?00MsKW*QUApzkzih#Zxk5nI_-qRiP9@5YsQRWdCIJHU!Xw zOF=(G8{IoxUmeuI;vz}b%e??<7D9P^oo9+M3%4NT63NeNGj&gsR(>4IN0L-rkfL>} z@N6D}!s5@&@yE8-Kbg@nq6Ap~u+{+(R|%p?wy|=E8$>q2%}}e`InL!$a0_6#RV^#R z6on&67sFu~W>U4`-SY>708tsrKQml7(0lSBG*}`G#EBE6kGK?_-q^?Kx>uOH&x6n@ zpNI6)rO~+D22k=LlUTyMMUx^f zjG9LmMRn{PH(DT=V>qxGW{8(MRd>bF{3DC(OPp1lngYw3asEM&C9L5DTm(?eQeu0X zZZrP!<9Zu%JRLO%es3zmn#n zhQh6*Z#o*nnbng%M2`1*U2eP~HyVq}+{Ja$dBqBkLY(3SBabj>84Ut46}u%7t7(Ld z?@pAfxLOAx#F0qqXj`WASZUhqe!9g9Z*n8JP&<}HwoAucvnJxLZ_|?(dfL=XVr$C- zk)mw;lVv)Rvkda^NRivj5|Je7Lr{(Xo-@vQ{T|3WBjwjdWjL2Sgw)5dFTk=qjDecy zUI>SI)Pp;XAqDQCbc0&NK?rKl%%mfl(Zb0#p%=o%ugiRYen%&3Ku5ug)uQr?$ zWj}k7z^YR*1X&@a!|l0S;*7|pyqphlYKHXqqCUZ6+CgyAOSB-rv~OiZQxxRcXLKY_ zp#Vh@Qrys67A4Y_b$zs#1mR8A3B)SEiHukh4`T^JO|alLWY|_onXR&hu|;C)Di=I* z;~ba#M9d1P&9!2@n@VJ5m>&*4mAKPX@oI2Zt3 zm;gi)03~$g5N^c0h-&QW8jRlpxBmYsf?GkGxO&ZIz2I!7WGPHs_>;4HJ+hnorb(|< zoJ0loC0{l1U?MTN;F|Gfg~o{DCaqyK!l}e5SI!`YMsqbjxLdJ5Po1sFEHBCnn3xrO zWx!i&P!T=VcM0hbCrKH?Cr$fJq7s``3{~wZ6;7t^bUqYR@3o!Ws(|QQABJUi#W6aY z;}ZM)q)fK;`LsnUg(B+xOz#k}VwAH+HTy+pj-XUl{smI+WSR-HS3oY?<>UXLEIe6* z$&!jHi`qQEgH7R$dBsvZNgI+!qN#3z$v5vCC7fdAil&74KNHRZW3D4i_t{aII~1&v zI{TbeDm6ybQ9|4*$Bz*fZ<`Z4<5HUhO;_TQi1khJdP2#Ob8HAgAdrw}U>$d{!vEx0 zyZRXaZHPPJbphB$K78_pt={2(wAL$HjzSc^C5aOg_;04U!fho~dx`}k#9e9@C&Z~j zW_^~eXUTp+G3>?_j$poR)sTX=#jqn|2_a9QLmR9i6!#|#V_zHaDQPedl_O!^`dBqx zNMx&Hfy=?(WY8heY|N~}D@q+LyGu2rivppy)9u=%{vnWB48%Wt&NzfrdLQ>48SCzT~ zgzMyT^@)nC!LcDmYa`LBhWUGVWS9Qm^I|DbloVK2zsnLH2#0CVp;##>`(tsOkT|Nk zV;?7OtDNtii5&7Xx>&?&PnS__Jhk}U-A%k!NUH+bxjfwR)7Y(e=8z?CnnvAFz+W86 z1Q%9MUE6IEi&v;>a`2UlVzI!)=ATFWlgH98{hvkaha&-itg&50qH8UaX7A9knIX|BD6zUd!kMk)^5Y9~ zF|pw}>Q4zU@$XlCUn|v@_`FyoJt1|Lw%e!rZ2jo@At*T+6A96Bt-qA_v(lxIz7Tk* zRG8tU1f`)UW>ei~f`TLB2yW{8@_HjqFFWIB!4Pqw^u~XyCsXlW^D)Gs;b6d3$|6yD z@8z|VN1~((#oTCUnAbPqt2mOd9(0IEuGL!h%W`{Ooy3D9Vb@78^h1-BeoNhUJ4E3V zp(OI|LM@@)nGs%_FLYIQ4-@*NfiQ|Bq6!T;VnaRm?!u(q$l8HO0fr0+und*GQz)af zcp?~3gMx*~mnsj6cR_`v<%Coy+71l1N8wOYBZ5OurP0~Yj*v{rfma#`TnIA*xd(XO zq$%Gio)p7UpsVD@(&&RiOKTV1uUi?J^c?K4C!ch_htM z$RjngagV9Pi=MJ*&@Xmt@hdDzZLJO0M_|e?r4*bL>F9rT=`*pvS}CW6+e*6^l{u>&e>4CdTVDY^-=i?%Js)rD>gIbeXa(E94bQQO~crV95@mF<;bYq;b9= z`&5=z8WA-@jYA8nV0ai7L^zPUa7Yu`(YcIL42k08|&gUS|SdTB(W zOB5DyYJYEe2~;HnxYm5vHQqrY9__7&@&>j~MKPsbX1J3c?2?iokq;u#R$(SyD^9g! zB07;-+1*^K?#`Tbt1yq^9vl)JNuJ2=lD$gt280?Z;=y;&9g~9}tB$yI0``@_KE-qo_27-XFK;FZ| z4>|E08#J6wB@5;C?-D31aF>6$(F4XF%iNi-Qrua_l)6DDJs6XOiY(^io@3dXh=pn> zaIbffyu`Y+P>{pM8W=1n(Hqm;}$vQd5JM+dOI=|((e#SmO{+eTOkq1v?Iz{ zQzb2!k%C1hn9&b(!yad%VT&;F$A~4{ogSv z?J_$Rj`0f+w?i#ZjE?A1L>e%%CrCUXBq%g6c}b2W!6yqiX~($L65*!{MV3%WPxwyD z*j#My!uj~RN)0HyxtJv3BQUooFTbyn#+Z15N_h(?(LzKu9JhKQCK6NGF{*^Q=N?ne z8Ys11_t}AAfnx-a)h!nm!puD7)b;X6Tb4pq%Me)N%8trqk)~@Yn2u5~jXFw_Anju) z<(b@5!;ctoBCXIym1?C+tcz4 zobn#e$gk*01(d#xLR@&@@Ng0wEs(TY%pNfHOH39ZEGR5N>iLx7V}G87+%-k;+JaPR z2GpiM+%&`7*xLHLjfiNc)EO1y*vJ(n<28t3WNWo_ktRx^b7z+*_FtQeitnwOR zW;}(v)oMwEiX@3vk>z`%kqUyP9E5Wq?GY*}9IQGBV-Q6O@TG?qFjB=)3M#~Mh~*I? ztp<4InUq(V=4lBc`y-;*yomFG;BXckH6I(a-hF(p8ycP#C^W;+xmtvjEz7x;pq#&n z{`k1}4ICB}5+F!{XBtE5HwhrN5~arN62kGYxme|Q>w;nSojpR(CSL~H*9O}E-zA`F zfRKP_!MvH;B%{b}cH#*P3kL>+gpAiClb@9g;b$i*rCyFnMu=*+HA_NyZam?l2b5mnJn+L&m@=rU%sy>EmP4hfO<;fz4D%5nvNI_&gr=@el`3Q#J+$Ov#^gNedOF!zaIrk+V9mg87SiISR{k`lzBjG&G)S}%ui?3tST zS1s+nvWsc1x^Dgck;xpkioIs-!F=ZXu3;PE6GCcBOi^zn_6vE!NE}dsW(yKnazPFX zH1LOppgmy^6iBs;ScR7L7O514)MbJxKf-cNe3SYl*JQ86`fao5pM05?PLo_u;whO- z&ra0rO~AZas<*D|oqZ3jesW4#Of|+h;~pfVO)&FKI7`Q2zJv1_w(p84i{A-x@ySg(XdH;HvKTTY_ajG`u0@peT>oibF(*UgdhZIOJ7EwZm&gE6WpD&j6wgh^Poix5ar zA!nWlBs@US@&JVaYz7t@c=01m96 z!^R5`5MW6`1O&Jt!wNa0k|l-^;u=ayOO(YS&alf#;IMfAv`MG`+F^0PNGf7zZ2tS-Uy2-z9lIl(jSp)E;imPA5t zd1jVzhnR52g)A-ON*>|liZs*CAX>z@k_k!gl<%vJ>C_15oeI$XgyJJJ>5(RguEf(a z>*U#N&dufbWG1lbL2?B3E1@1S5}ax02ofMH(4k?2LPLgw1cO2Y!hu6Tfq;PVAXy+$ zp~UaD_>Z{U-?}EhyC2WIY(KslZ@-N%z?%L@pbZb`)-1!HCEQvWd-sY8O+x~RR|siD z+~=WY-Rxs8^iOodRUK?*=HWWOzL}A?vlbokR+Z-;lpPm_0vu?2%xHx7Gf-mZSM31^x3A(XVpWKYAy7`bmzaK5iA0U;_K9Z6K$X1Q<@tTqjh^V*Fe_Kl1hc2U zflHDe?3@MZ@37xOcZf3U zMU0MnU)Zue`|}jC>a96?*ar0Xyy0XGgWp{_%EU*+X_l+9HQv_6LNiwDZdQMPtCOAvAKUDXJYdhu6iArx;N#%zqs`N(y&Mxk7S#s~senJovl zBDbEsLz%f?<`j)m)9pLp!z$K6q$o;E5n*~nXR0pzq~czdm_UtdWFykbknrP)0%UaY zGf*X_FAovj>Rk$u2$2y{wN-7E${a1aA^HrKY@jaZlN1EHYcW z;;3>!AO8Vp?c*icpkkE76VyZcgIJNRPCUct=-L8aIfhNGvRgF$MD*`rm9M#-l#{5L z$-vrct_gY6L23F6#&Ax)`|4Vbw_M@Ss2YZgfgB{jw5uRh@4qukpqv92tk@aYa9~KE z8U*rnq{S+t`!7#^46ttj2VIOCkU6nK{%QFo?#&3~Awh&#BxPc|9M|QABrmg+-Qz)X zml-#3f(>Sc=WhQYxG2 z<0Pb`)8}pZDs1z5d60{i+^N){c!W|Z-hNfgJB77q-^<<>so~c2uhU?|>U2OtAK)_4 zTDC=drBD%+T)!wQ5<)s3i}n93(V{q(*09i-Qpc(c$$9D>H15dg5%ZsdgTRz|r1~OF( zOKPAh0MhJGpZYFNZFwCm2qvAd_>*KSUXLi3HaAX^eJFL5>LS=i_9HgpdJyGRXuZMT zmNZ$ln<@vEFFS~@kr_3t#}aV`hV}hp6@7!TqRa6VtDH_rjWCeVidGcicc5zx!27ge zo2?o+?PhR+$Xh)wx9yzIxdqV$qzF8eWLdKI5}erJ|E&d~lzZXAn#Z|;9i77el zE1*%CCUD8NoWN64v8FeY$oXP8B+k8pQ0IoWEx$oz)qT`BRfQYaHY3*TTqW1rmY)4< zV4v=LKfR)|?s7?1!?2{9G)5F(&2n!NVym}T=_K9rtXj>wu!Po}nXttlsIODQcHUdw zmMhOYvR$;wPr`rg#gsIvv?V<#;xVrN1->2hn>G>Ek%jBg(MlIJrsB=wg!HBYPie=Y z5i>&emt?v^=&H!+Ul~C6Deak}K;kXXjOudOOH}!^^d`|G3ZFP2OL@E)!}Uonk^6Vo6HBg z3T1Xw6-tZDMi2Sh>|5y&61{9)R{H{3e~~2XfdaOdkbsMxodz(-t|JYt6{*^HUu|1* zwcep}FG6YYi!t}`#R^qh-^ZX`9g8)xq->d1)DXn2GGWA<{Bw2l=!)5AK5DPS_hJys z!4ciCZ=%2?!Xl1>NY*0IMkgzZKOQPq2vfI6Q8t*LDaRd{yc5sO?n+YeG-mKH2~gF4 zsyOqXjt}C8m{3gz{!+)`B%!uWzJUqZ;LsNx%^mjBaaBkb1d@4TsxX|~?Z}f%RzfN^ zT87A()u~Vx+yCAcu$}1caHs-n7_pwnXQ3sDCuhNqKH^pBzC~9)-G`wc2J*IL@MXG> zU5Xk?cBB$|l`KA#e+o8?w@QUY@)t1yJ;)NVp0I+e9=3^Oi?mqWO40<`D;Tv(oK{t< zyv=B6L;^!Nq+lWuFG!Jah1nZ~{J#T=6~0g(d)tP$EI;%{L{)Lzv{>Z|_)K-CrL;97 zYk9O1ojU3^@MFtP+iSjLnGba?5s#7wyGrPb7kdF)oM zj2{VjAxDIt2E`h`S0{gxMV?+`PG|qd$?MLby8%vjLAI zw!|hR2~IthSCJNF@-aB+oq5nuK~$JbNWE|;(VLc$F~&8*Cj5?tfrddG6+`su(hy3o zWLtto$2f13@TQ*=dQyYwotdZ_2qO{67fr-=Mts~DQV5A*tv=Az0 zI*r6MD5bF{8jj4GmoRT9!dAwT8%&GCG(nW0Jl zT;97#Iim50R#fR3BmA1_%7?;F??2;aWo+Kf@v=7hn*?q)asZTl<=dy~<0;sRIH+94 z#w7~`xK=_a8a%BAPWbbN@j|@})>HXRX#%Dy$?Em2w>*id@f5B<+h)FoMjBK`+%4v? ztml`hD>nnc8*HHy6-nOQ#n*M3HpR? zlYguHm;!E-YXOsjneyw%pMG;Scq^ZAha7%7L zcG#G2Z)PZb?PZCtF`LUMz4>y3b!G6xcVVX*@Iw{Evy%M5!Pga0ll(&p8&q2qPE(=; z&Om2@(VBkojL2H*T91T6#rxIRzY#{!wa&8wh4x11_q-fMLryW?I}98@VsDn8Pdg%e zC)QBSzUW~>bs_rT#u)<$JtW6{TvFLo0cusCeMQiEHD{PpHp<@@FDGs~AcX;Z6#sH@ zAo&SSFazlJ0GJGLLjOjAkutmgsFdcp%?G||;L?_yeH6ik>_Q{IEn}^+Tbm3`Okjt@ zO)Kz*`a~>Y6o-Km@1a)0QLZlQYS;{VhOAE5VwO!ZV3!s)gM+cCV8G-A(+J-|GZ5V4 z0?%j^T@`)vmg4pkskuGvwERq(#c~esM|H{XSHmH9KW3FM3=4Ejj(xjS2o*BEOi#<_Eb!bB)+6sBoEjO9;8e*~oZ- z>o+0?%mF5zobIP>xbcdi-YK0-42}E=pH9a-oqWFb|nf3BLQOF1qAXCfnKR*#XX2_8BH6O>ZB{vLok=&zOmvx1+|Y zYcykGC7CBoxuM69Ie^@z4`c<5KL4@k;-M-#-TBs7{q3g zD^H=baq?w1NT}$_(3RO~V8T>lA2QELc>StgW&+10j}vYxoKV^#T||(S?11u?zP1jG zdI!fp>}XV}trqMmLEQln7-*<#X8D^SP0}X5gR&<3M%tW~mF3j&!~HRbVu9Vx z`5m#m2`(pXUqgsbAzetcNy$egj~%}FiW3oh$ai&R62gb6%1QoVJ`9z&Ta6low9ZHS zFF%AZBEwy))qcH0*Dg|%pGVuXa$jK^d)FC3-ke)ERK2{h)9sV&u}G7nEyI5ar#W}< z?Izb`J>`@jT1+hkMT4ejD(BfsSEMVI8|?Ut0ZeC3Bb4R5XZ#tP(1lZevKb0s>er+3w_A<<>Q8eIfY~X)3WtQpK7l;lnHvzw_j^#&Fl-1-BelzUqRU zXpdD&U@O!&24X!HX)uYhu(T5oMmL=w-dGJNuoiZg5VE z%V$zbHJOdz@Fd7Xxm)ifT66c<^Xv%0#UGzZjtniK7)MW(?41l~68~(G8+jX{Qxkuy zoo<12gXc7eM3PVRD)Ra?N6cE8Lg*C;=xUZ*h8Q7BIr}c+Y*lwbbp#SIx}Yw)M`#xu6vBoT6vXl1t_GA*Ym2gORlY^^ z>4F-ZfHAOILN_}5`uL}C-w`|1k`FDLEoXNGlSxEDyd^W#$^M5U=vuE4yl zrcv!EU3>Qx>`rIiG}8+R#EP57v4{Vx27jLQb0Uuy2!!hNUESPY^Na(&KyvHk@ZME-4f0 z``S$5K+9694^fgl1s-%9ZY48N3W8vgy)!|(QY3O=o-#6S7g{Sftg*w{y)Uh$RvQd} z#sXS!g#70K5`A+?VRVP(d9iK`EVH(i+M|L4trDje1f#8tQi_LK@t4~{5=$>&(4m#@ zLEE?V=KGsfO3Iixg}pjmC?o`kjw<~JTmP>K9lMkFfp)@blzkr&(J^Ez-$?fLO5@E+ zcW<9oA;J1;W-i!S^#K)twini!(rUv*lKbP9e$W;(pPK(O8IwPn&xOL*4`7&0)5<5u z{?yRm_)jy~)F8YBe{I*Oa)TX-N5q~ZQLWAoMgOv($Q()+!E56vU+h8al3fhWv?=0w zk$vKbw6L|T#!}>Io051WEeSJiOs!bjbrzNa^P|zeI*6mQ5Re(}V+Ytj;KOAJ4j>Vj zqo#vY#tgT`rhW&pUQ?<)VNm?3pQu5!aZ&xR%O{(N`^1HGQH>73RhESkNZ4G(1$0Z! z*bx-J0N~|)E%PE0#j9q!->=CUk$7{m&4iX45RB`37;t{4Ju8c+9{XQJDE((~(L27< zHpPcJs^p8)Ld_u)|M50VNGoK5*ojQim&qr@>6~c|*1bAs=|WsLbYMrGtF0Isco5v} z>CfjGqX#IJT90f|;gI-ti=t%m21{;XD}v{HmWR=AOfs2zEQe2rt|6Ci0Mk53w> z7-vl5t2LIQ;<^UIP3XBJiqkNgh1d`jaCm;a5=A;xHL}+pAw+U|h!)3s{c%bXP?rm* zvM`&Ws!YbNpV05)YxcfbWPL3iv1%%Xu-XQv8^!xuy=b)`51dlt-11z-rmOuevQ&S* z=jPnF-8VQD_?^jpD?NTuYvRO2*L9pL6^^JJHl}IYz-KyIqCYZ0PcP2Y(HFGb6HTb= z@K|8HR`q#TAf*?j8&a7vvz&GL*8L{=0)$01W*8Hh9o~Wm@H&w%S%r2L@fivY(TK+< zMF^V#hDZ)+Zs1!uj$mnmL>L`T9FDn@`lX03(E=7>u(Ppbd95&3INzU~7bu)FMGPjD zqXQ9%(bBi&NJwPk+X$-Y8cok^_At~*Ci3F-Mk^=%b8t(%%PF`P`8@Xw5PSaVVl7x@iJ8lhHQ6Q@poRMA1SUrW5#VT8mQg}z&Hd|q<0k8S9z&YA+5E}J z8)8~mqJFFLxfaqw<=s@hnD9w8cGs6?s^iyKGQaqBeAWuW(m6&fdj))%uVbCwDIytN zYyab)Vkza_k;AZR$bB$u0u5?Rxx^|wvg@VZhJ@R{jZ zqy$oiFn$kz_jSOksZk$AxZ!LM*RiIFWt!rWUBf)XQbRPSn{F9av;@*)qz^w@sI6jQ zZ$H29-!%=$XiMf{H=ZmmRd`AmcW$l?{h!(+iFeEj+xzcVN;QG{Y07o7mEvyP-{m*L zHyyqIeeS9)wz0pp7T{`CPnH|91zg^pMsjE5N2;atcjY6#XYM}q8>Fc5i>H3J`ks|# z;SMUJ+*06l*!n}{26vr?LW+PQ?`s#3vI_25Fn1fX9J5cq{0fSD4f})-TV9<;Qi-dR>u*jYg?CgQA z2P{ZUm?8j*TYFv_sF&0WB`svC$Eoy4%87gSG7MwIP)-EfYjSA?)Lng)FX6>0xX>QzRKB@$|g4H)&*+us-|_O$0;mfWrUKcEyiVZ?@nYY zbu$Uaeqra(%?cP&8{eC9igJwL#SA3r@kp2%X(AX#_7k~l5i6=7yAP>Rp`==*s{t3a zOF@_UOG}(S=zJ4kjr}t{?gjJ*(2KEM!e@Vlo+K^K>>T zc|U7>yIL~&mM=`ba;zPkJIZsFy9W(M67@9nVhPp~S|GO{iL&_Rcf9$rLC?d4D@ zg4dWPm1}3wGdg;&IVk>*4S(O)@;T^)b9i9r-V&!BkJ!o2_XSCTtf~B57+nq-to)BMr3_*&7;WRy+q1Z6VaYFsr=8D?!&7ci5{$Eg3BN+l^%0is6^yY;STiX35@d&X%;llxWX+| zdd_-*aFkJn1aL6pFB^DE23TXGb~^>yF#t|RmdvZ%bgE?!MT{AV7?T|;s`dv45_;s93eZ~2_kf*10 z8nvMw++>uOaZO}Yk_qz1g!j`G3slds2t3bCij3u!5@Uo^nsHyRx1N}qB02JlG3uhn(D_{dqSOLVq0PsKne$?h44d3E{8X_^*jo4?Rc-bm6S}!7V z`TU+-t{YFc!tC385|;W&@^qVO+Pi+f*0ioyuY|+lb@Lk=(^iR9qLheb5hY=i?qi(f zFoGP$6vC8picyYo2yqHy5JCv1Q?XIdKFICQi@Et-wp*|DwXJ%~^U`mYvf66elHUI% zZTI`-vY1TlTvc=;S%p)m#1x{4QBD+tm??rBa)l5fhB$*9Qi^d7V!4WDDjlx&UTxGu zZ)aN#$LoH?elHh_r=qFp`@+)aN1pE1XiKvqLRLZ~BN8JK60|iXZe=k@sX9A8@;oX& ze_tII(&OjP{_wD*I~;{a-8}8`JE^HodOD9nlod))p~X4Ehd8GSD5V^s_;?nNN~QjL zZb;N>G#E@H5MhKNh8Q7+5I}+iFoFmXgkgwe6B`MW#>wpW`dBP2KP#2pUCLK4FD)%P z9~B=-mASbiLqRCeW-+Kn4UEVTgE0ugAed?hp(vRRLUSmbLOWK8CPgBrM`vYX%BC!N z>J^WLFr+&j4~mD69v9W?N`-qL7wGmnU1GCxO^HHMVW>ty2*LzFgkZx!fe0W>Ajk;B zz}!M18k!{|u$2~N(CSO5&cK)c@Dd_M74vLMSB-1Q0-sAcG7L zK!PwJVFWNE13*ETfds=1156<~47ocH`#!aK_kN|vs^f?qP!Fog_GLQ@e?kp?<6 z?J7Koat@DSR=d0%4lUtur9KY@QMOa^Os|`JyH>v2==$ZAKB|l%Btj6$c5zh#p^U^G z96NAUNL1KXz{ET%e0CBk4Hb^1odYtFapx#)z0f3oD}pG^|7j zx-u2c*D9E(Q>T^ERMV#s6p6^qA?}p-H4?mF*HJW~tU>a@B4!Un0_Go8a|dX0KO>Y7 zqK({V6b2~Zhz?Vk!-z51Ly#DD8{#63xf4P}C5V>@QfrY4H@kQSIKUQgA^EqL z?Q=7&ffka)nYLbn|-_6H|CJ#BGTmhEj`AojYTF{Y#}zH z;a{k2R_`qvHiAlAUlvuZTXup(5VWHOGRM(meQJ@)OXChFTrjxG#&rbnCE(J7NHCls zm}bKmY#IhZv4&(31RA3enqpIVyI47ks+9%w{rS)?3)h~Vc~=k6S>ensM?qFO6_Rvn zjaZH{N-;xXm_nGrMlno4tPtcNu*;e2VadKj;fG-bgjyKzA#oJRb3vkwATa176bO17 z@R;F05<3|DMN>PIoZKkI6|i)H%vH7Uk0mP~LZTNl!c_>cL~#TtgA_o8APg%33xJmZ z$DxEM3Gl??31G@XF~OEIDG;J%+i7%{p$(N2L5^$U)4CA{2%cMpiMzOO45R z*K0&pu|(=(w1p&gMRGqFFBo!$IY|S72T?erhY?p0Lbl4;N#hXa%ETy=lA}!0i0C26 zLa>9e9}!_6BGO~RJV%2I#3@0C&CUc14fz)l^K?$u5h%|Qkuzqbp^hRs204r=f(}7~ zh!86bSTLq#5{4^sK1|#~#Kd4AF)4)9ESkkv711k9qR7QDOo~{+7ZI$mgAt2J zFe4F!F;*>1s}n4l3@lxX35YO85iMaEF<^m$Fd$$+z?eaTGC>-UoJFx>WW|)Ep=&6l zrHU1JN-#(vs1=ee6$yd{0vJXXQdfk8(-5&35XA{uVKBA9A~2Ln38qA)3lPFYn2aHU z#MD(H^pjAXNr=Q^X_Z$(a}y9?L4pPqY)IA-*=dQ|5Xizc7a1^_3??AJfrtTsA%P6D zNJhsKmlIVpIdQpB5z%)UDceT&s*zWT!s+q(C_|CRnqIwaOv^as70jJmNhLo}$2iU^ z@RYqNyTmaJ#CQ$xQf(Bn>BEOboi zHPfYTW>#L`+ZPrIg+n7I)plAZ9gW1~e)vQ(2m;}-fuu@KMxiMDM&}+11q0#0sIfF@ z$xhGANYKyHgCcxJ(VBEhb?MQECPWER<=3}nym;2TA}J*#Xq9ypb>+>i9_^&3!oi@Z z@P!NexrFL@E3w(s;9?Mk1A#FDDw!Npyjr?)q#+4cs8vb$bmkURxW~U{tk+`y!%qMbtJ-tLDfXFrq1*EF4C)1JSnHJ6Y ziP&Wl#bIdnR#{rF-(}f?OtSfB5)J~exF(#Q(VQ#NB$*@I|8=)XhO>d-viP#8=}oNF z|NC^c*j-***%XpCw|cg6@Ko8F8JG|gIp{QsC+_AeDKyS(HWeEX{4JO1T2i@C^S;gE z$m}_aW<~bvJk1f}MCTyXN5@29ZPW@Z*=IJ;? z7sk zEKHl^S{@(y%u^RdW056cU*@G)tk8Tj@f6q9)X@+UF>(}J-HAk()-(pH#p+O%FEx!` z!=Ts|NNt%!(8tu*%$Z?$e=+|fU{j0nf*^`z z^9H57Wb~uGD;}P8)L$IuiIh?@kSuC0pQ$M?MxO|~aX6dobfPC)&E@VixfSDW?)qu1 z5j{EACaPa7@{0cALg^M7yY{CG})T+a--Dwk8!dQK{z%Jw`&U4nXx?=Gx}H5 zn4)Y|gn(ECnA$N{4g1z@L^MWB%WKQ6F9dMWAlMX!i^(zgvt&eyx2oP&O}0>%rEw(S zMXS6tNBhOW!?7L2fgIynqdgSGk89Q+T?ZtsJHSzeV#A6`I~rrDZLJJd>kciZ>a+++3(rn6yUgVTBFsxpG_VRr;+sCpg7(E^!d zQ-JlGfY5e;tm6QQ&j4w{KvAkdK;D2d8Mz`|HCZ2HydZ72fVf|u-6$y~x*uozT~Z4p zLqAm%(X&y=)FMl#8(NOqef|#A;w`BBcR3hMm0_FX8-^wEJNfv_wv~p&l}`4rIff<2 z=OqY+VKflL$2m343X=p2`|aSBNt5N%;E@C>BcTzB)Fu&^BxH&cXdvq+YI; zl6HxWA2FUea*PO6F0noyVC9A8TL#`TL#{9JsSPDS+J8e6Y6d&JO(6otQm)<5yBdr1 zf-7aY=YYS8Iqh`Ebi8080f~g_$>-m$M827hJp-MNgM_}7CEHBgWSZkBAztd4!G2(Ey)xS`;;AsJMK*+nHH z-GM}MyO50nVZx6Uu-peYc5G?J z5p<^Btj~_CsLV;H)_Dm$R+cYu##SR~)@!qgOB89eGyi+xz7qLw@~W=RS6|Z*-HhM3 z5BMXQkLoE5l3`}Vrx{GQ{5?{~-3xBqvMcGAJhW`5aPicJQ}scOS`z9oF_s*v_+)IM z2<)<%SwvH@J^}~qwGcRx_*iD*G?T9_BmNSI??Fh0$Vr3-5%8MJV!LD@G1RH;yxB@O zNASm3$Il6*@61}9=$)vv8Z8o{Yp=gZO$Wg*eq3A4W0!fCTQ=SaASEVyT6VjfVvBd^ zBDb}NnIjn-{4j`{@hNVi=zFqM?O33Uy$3jI=vNr42?fX_sEKr;SL3W~>9~xjEg9 z^qS~m#O&8cVEPgFpK$fXs6WBdpJ>=Z$X~%1S5%acU_RLOJW@x8rwgMXX(4gmCvhNl zmMb8Giv*McObc8^qO%-xd}I4%fd+#e*IDT>(jkrOf(#xE&o1~{PGgq=ECDc*vaTIC zVy9}ok$S@{ViYvl?tJ`VG#Hz*F{6-$z)|_G9t1|YV*AVhsSlimzJ)M_7M2N^k0?4}S13jvc|u$iP}PZ$!oP&Sg;pgG6deaLNUhUdw9{ec3S^6SnzF|oJ*2qHKR1>-97EWC244R-q2Li^4|YR z5N>KIiQXnJd}e`&z=3GqX>UFVW+dI{pBk0}>T)0ZbUeq`#$|iyc{7Q~`^#cWf0#^c zJ*?+k>qT^Vpm63&%;U*jP2T(s zr`0uP2~}Ieh?HWUmnYt$#seiTm;X}P)=nDCdj9arEFrLnEMhN#8}kA(a!KIlABos!h*0EnaZG$GJ^+O2NemsaQ9F}^YYaaIm<`5DH-SY z{y&)H(>sYcjiixwhE(qHP=-J$SXQ-pDsu}A+8`+_moobBP0q|~RAA3kz2X?>S@1%9 zD1UDw`a}R$1}S5<2cq=rw8JfaWYk^~i9yxepjqH=9#V(5&YM7^oOo^vUg5}f+HTP8pI65)mKFkSLH82^4HnR7KJW4Ea@>hnDKr@fC!D_{;13m%5f3+|0zrj#*g+ z4oD!?Kexnu6N+z?;7!T+27B>HEiM7Uh9JPG}#WhqO0<|IeH88v87 zE5oGxk?0N8!oIIxlC$e9ZZ^k9fj%#1%PH}+r_2}p(N>4Ue&Fz3;miWhe?wsCLlsDM zJx|qZ-?vXXVq`7Ehnn{meQ%n&TK`=%Z6s>fzgnsYwp^S#Ml=4T`~{X%2YAC@;i5g# zC38AhZ-hk(v!IV8t|K6rCeay->5D((3aJaDTP3M!{s=hyJYB|%2$uv^E}ZShJjV0A z2^09e|C5l9z|WhN&%f6(N&X$-uLm@QJLR(1Cxd76eB~YfVjT0K*uFk>8AdSjUro^l zEi-eWr{iQ&2Y<1Lb}t+Md93w#v`6SNm+DoOC7KHm^kt~5Aks5au4c|&L1uXSN0Lsd zE3;-2!x%3R@JR@)G$T$-?lnqL&vr8<&X|nIP))3*`J95x%%8O+NFq{72L=#>#BBPE zlwDl$?iWEjH%8M~!b8^}7QEO#+|8Ce9+66;N{_T229p*O~VWURn9suY9GV3y=6m?7F8nMHJCj zix3fRpbR{vFma{}LXot{u=w%X5%m@?aFH>;0T`YkZ3P=z;%9RLI$x$kk~22k$ijV_ z77lG_=!xV@lPU2O^iI#+jogr|3Rl9oYn5@MMl6v*YfYk#p{+x5lLb2Rq&;W!tRO}L zMvRNoM`9sSqUqCuGCN$QT0z|N+?@o83J7Ss2FKfYjf>dw@El-fqD@ z(hi7^2!g~LAsAM352zQjE1n%o0FVYGIja_vQ3R5NfvVRHY-0X&2kH0z!hL3Lek$7p zu4z+u77KMThlXNGG!Zgro-8#!4tk^picqFnQB@f)l}!>JC*8`=;w|TjU8rLtzLYtt zT7w9}>0xL1QbSjXPPP0EIHH!nO(pK4RFat@g4Jo4os3VksY&W68?~;I3lo*7NK7y> zF(QKS&teB5WUs5Kh)|s@$f{7_$UdZ7vEZ>g29W3|Lzt7FaZY$MMRbpv+fRq^eG)TJVx8m$dncQm?5dR%>On z5H5E|A>A0a&;>J=ISMrrfZozDb4YM zSR-kX&LRkye}W|vK$^*2=TR*iiKwR>c#KC(NXvoAqD87vi9VEVMLbD zJ(4@_vUDZY)|vgSx)`yAb-nV$e`LsF8}{{aQ2)G=i2cwt%E}aJIP}jJPZ^gP9;Rpi z5W?o&uJsAEk-Vd)tP*l-K1$fhvTG)Y0>RDg#!Z&8$>jSB<61ZYEx#axlr6FebjkV~Ph z;6q{nFOdQ*Mv|mRbMY<*j~8e{#yv^{iftn@e#3&sUqLC#k^L17-HHJXm4ba#9-7Fm zgX)!9%UAD!zEXjIROPu^78ymp-(_-am8z~AV|}N^>bK)vs|r!+cHk_92w~GxlEx;Y z7=h|ifuAW480%$fnO4xsdBRvOw;KSSn4|Y%YD$$=;qh$Iv5`~II59trb%?h8abff{v+iQ|R z3FXc_Yy$<5_Qar{a~jk{F$RsyUHqk3ZhD#LzUQ(}Ek#WbKd(If=xBFi#7X*gVwVwtG^Em$_#YF!JWyHm3Ln9Gq9~j{)@bQ;DQ2>%D5Z~~lNk>f_GQX&O<3!?veZ(Yt z7jc~pOgS4<6K|MxsD*;* z?mO41Gy9M(!ul-MT3c5%7XIRH;S@?L)mre5CCQyDS*}RuAD~-C&*gBKdPYap_cbi8 z(e`9rci@>?BwAgb!&T_;<=-bWwm08X#q3Y_5vYS{_uRRa4ApT>l_I)^_R2i$xW6wf znH6HH%~@+Zm=xvS>;hgc*T_= zmTBVY7ozIMcPwg!pw^`IawW$Si8tJT{i;8opj0KKaBIgdR^_s*iJp>{#PP^MCvFB8 zaX4ynCkSS>!%qa(lW`&zuR5^zg?^rM<;aP-m!J&K_&l~_Mg10_f`AbUtt<&~ z`F-{Is_8q`E7Is>veqLOYb?vfbfknJDs`{NxM}|e=^kO8`KttWFZZQrWW$z>Y4{OG z?az-gYE*Mp?v3+j>N`yZ8PE7ywNd$h5$26GPWn^-RsOfhtjBomG=klBj;pd6A#aZ0jr^WcBIfj{?b>}a8QRnc z<5UU}_f(mLwfS*OGmzP-MI;rH+g1%A_dI0j%z&7f?SlHu>B2Y`6vm5y(#|~sv+5K+ z6>!Zot2ja%Ak7@(Jv2nGgxD(P)~2@Mlo=vUHaq&v6o|SJof(OkQKuUD95k`5 z%c`3rM@WU7Bj>1T6St-c&N{``ym12(SXVWC^~uG085*$#5YQKbt)dUDq_#(0?|dgs zmiomKOyIR;p{fKJd+b?F0&YQ^%HpQNgRRN1ew5S~U3$b(&%%y5ls0*mMCqbR2`ZYFatNPK0 zu~)Na)FqYI(+{Gph4_qd-KGSjnLI|x<8B=UF8&sLrZksWpQQF)*S%w&ROuZ8GGy6Q zfslPnQBE(_2vb>*IwOAhfq56qp7pm+e$#@cZ??PSQTOrZ!tHoTHC5z`fBsaWO2hWv zt1dV)ubfAd@5rQ=@g?IYVX^a>Wg%#-=Q+2A7F~@OSMb;DYfJ=+bXl&F^YUpZJ+TtO zxQZ#~DqeoT5&3)^xzY__T*hu~gz#3UIILE!f7lZS(67c@~oxs|*{M zkzx!$$<^h`LWlHED((UZDV5&Wt2B%>dvMs@v#j(G}7gM!&pS7SP_Rd$*v`=1}hZkJL(zo+;t z;|-$SJ;||Eg%t^+%Ma|BvButw9+6UwGm$4RE9LdVd^Kp=cOb19MIF%jdc9JRcm=(7 zSC9~ql1`GmMW%-sS%13emGN+2qEr(mnVG^NJH6S75No#}WHDKu>#nHo*MCN+W;Ef< z*G*Q`2bkT|J;(iGzG1|Qc@YrzvW5`!r*Z%2Kz7@{F$kEdnv|Xes`^AkRX9U@y?Ol0 z_P~x{NK~CiMMqzMIZv_fIY2wVdg^;YPe+?WhJ+$otnw_Q_h5INZAevCUJ_tF-kzoe z`^7xkTYQRKhQBqSWLXoknGW|=_?hkPaFiYtMaV0nrCxMp3%e?oxzx(augbMluhwIy zxbumiic+r`1cg9NR}oc3lF`%c&tkjAdTNav^L_H*WtUF^{4u*wwX+RnH%;H{eV~v= znoRW{MgG$ zqor#p2)Pdl`zRGN>=_Bpx4&34hpyQ^e_a-XIB`_I-Pk2szQ+Z&nEEIZ*p`(0c(QFM z6T;>(>cu!DQkK9t$N%_QtYk}00mYR8vkU>CM*))I0f0CGTonNrblxrS1DJAIgve6(H7YAtwe5eOo40~qj=9T{km zaC5_ocF9R#Cg~@~Q9)xLqiG2Q;>euz&Mn@SSy8((A+iUQ&{Y_PZj9J7Yf zS}OribgH%=+&b&0hbf>;L=af@xNgES7?^T$dIVy$xMTqBi5U&C^asA}IIp041%#ML z*C*G=4UEH^xhCjmsC!A-8%~C{dT?KfBg}f#3YTp52-2DX(HQkjWWpADK~gXDWM#Ug znDHZ&du9+f1&q#)S^YGJX#ESwQOgtEvXFDLH#(t8O}%<MVj7a-PYki2nD1dj5YEm+4v4nH`_N0;a8?1H8#Q^Q zP1_qkC3RA@aCY_5htAAgcK%NNACyQHg6l{-8te^$yBK}ytuK{=k>fU;OoYVID%{q& zC6iy0+L0t8Xa;Pu{@Z`|>(@rKiDL?-9`T;!=;jHjZbG3gj?KL*LLp_1C zXwCx4ItT6<=vnzWg+xwVfQJ!TgdS+|kuVKwHleLtU*bjZxo{xoL!;nNhhLU;~dLR>-Ci z9`>l8#k~fms4%o|{(&s*X{r?d^!1Le)Vb)D{fk&63j#?J@JU*E8?ojv%gAS{77`eF z@*%NRMH-V@fy`ddEspO>)A1=32gG^s?M zPOC`@gwF+-;*kCX9yT!n91+s)W4;M-<1jvT`auNO-1+spGF4QB#W*kxiv{wKM+70H zP>$IqSoch@3n&!vaVwiJX~Go(2R;B;)#u-Yy*WDQcZ^#97MuJ_PMg5*`?!rHZoPL{QS#DYkx<8 zgbw4GA>Yzgt_u>o`r5@9f=Hyk$Jhv_zJTp+}jQ)WW^T!eTypPK5bc~hptp>(RYl$w2Pc4&)_bi;cO78ebC?toqb(CmJtSyD z4M_C?2`LMdeE}&|?H3W1>bfAxgysB;epM7V%!y2>C`9fOPP*M5^Cs<;hk%Op0?tdU z9I}a0SM1b`ZW2Qvp@wW=+yI83ZWmW@L-m2>Rp+A*3iv$0ogh)znvCB><@AC8PLgYFRxE`=>PaGmenEi} zVL`tC!82wL=tNx7b)$BVG^ccwHCU2L<>E+um{M!IMCCwX%A9~?q$Q?G5C?5sV)2Le zZ!DlXNFbnF5h6U1gpzt}G3m~x9^yO}`k7%|1aXtZhVAkQEl{roR#7StMMemW2oy!{ zboxlF!HXI-DTAi2Z$v%T-|F?g)}KYp4zSJ=YX?9Y!;^Pu1t#YL6hWD5O0I-EYKoXy%W#az)R~}QuG(IHYH&JZ z1V{hB#bUvBo2bvZ@%)Kk6XsIT;Ton>GZ4#V0R%}-S6LGE8cIi zEeKqqV4eMOBju}L&P<%G!Yz4W2^ZInbOO%DB9d)A{0p9{yYBAnIK^f@(swnMx`e{R zH#yOfpcTA>i32PnQo;&b6PMB@`p*afBhU4#Ggc0&uwV%^Q2BJ+XfshMInG%o)J>?b zO4jKNgc7dr=N94aUXC$BGa8A2X zT|!?g_m+r7>=2=47Bj~=60-4y%c=*gFvh`b zV7*F5VQEc+1z2uaGDuELV1zCPJW5CI`$zfJ6dOoONI*7j5K?H3=aAl#2%IsphE0Gb z*;ez#TayzVTN_L=F)KVFWf?(Zrru$g!zM(mimCz{eZ*YTdg7puJ%lx*m45>O5PPF^ z*Sf8A_o#M;$Taj`$%^z*?rl8fjiPp``Xd~_1xhCu3UwyNlw&R^t3rRW>L$07yfIg& zhR}5Cgt(2NY7i-Uo3X+x^hY^IIkKS+BoxR}B02VZq{URQx!I~)C3p6bF%%*i%-@^T zH0ccix>=_JK=oSZ#13O>VXAjQWXaH&`8Ul1wVVCy9@UV8&7gK(6h#4`v%ei5TfaX% zoJ0w!lbQKXrUjI3G*CuHcqiwm#>xOF?Y_zyad8`!lr7Ylb_{+t5J4jcKsp0^EE%CT zt5l;MjQHG&B~I@it4Zcm`!`Z-O=!t`!VoHB6o?SaudxVN$iLZ$ffR36at)zv8W z#a~D+_I4A~%@aw8dn)8k3nWYEnW4VTpZGgHuu;Hca>R(hq=oJma=tt$Yl ztAqWi_l}MMUQL-XM^I|x94fP%5A6n(1m|m);x^J2R(I^LFzrZm;SuA$6=U;$W4^+J z%iG1gXpCb76W|l&a5{gR%o99{;hS1NrX!y#feHm9)zFkcCA?xf&F8xl7ljkuB4%%-IbGRnGC<3|lFy4y+S*olF| zz`~G!i$SX>havkT_J|fy)pv{JD3cSh*ScBWZ#G}p9n#dXNUh3rW5sH?0&*=a(J2Wy zq(PR%DF~^`BxR!4WsAb^BIMExMkme1f_gpTiAxgjuwlGtrF!C)F7(RD6qFW$H zrsJb7@_plL4mmTEJz>jCCzKkZ(Bg%}Xj32PIARoKWD|17N;F$7rQS;1X~sT}M$Yrf z?Nuc&q(ebIGNYE*1V4)5&_I$DFtPMDG|*P8{VbtUiJrpV7?%X0Ge;&cvF(sP>YD8~ zUN2ZS76rx_JU<@II$!(WOKK$jirUuWUW}36m zIu}H473%_Y)gZ8*K7!d;1Z9Xn0vKhN(H7z^5g5zoG^@h{DRN~sRQh8jW3(MW)i2?> z(MMbs$ThQ84)McU8^GO!=u&0dk%VecqIhy%Ar&;V=JW}CsZY?gDeOG0+c2JnHqS#6 zQegDNVnT3bDjPp8E`+L~XCh@v$rO@-#*tuDX*Arnn(lIcb$PIcIuO7==sj~X0%Myt z1QQjGe0V0rg`L`vO(gcrANjA2xKspCKZp*0D#kXEfmJq=aSS;aHknB|=SR7Aq2iW! z_t2FUKg;epI((j)frTX`A67VVSf|EK*P!bartYo%$YE?B5+Vd5HN@F;M20$~x z5)uRod;N>r+9Vm%Vj0jYhg`(`v@N7K{e;h$RO-YZHJVXnn;J5R4NRsKhn1L!Zsbak zI8@H_jj0Mk@+KXnx78#PR;2o5OVWpZNb#M(zBod)If_blzUTRlz}p3um4;*xX~Unw zP;8peOt07m9R!w+kcj^R@GMvian!?D4B-Nwx&V*~ZNTyaQf5Nk5pA=Is`}iSAJ+Q? zIqJd@RAP~doiPlP>Om6g!Rt-0WmQV3Y7T&SV#$~0W&l79C`F994d=Hv5nY~s-X*4B zkXbM0l&Hgn!1-d`A{&0Qe-1ew(v-JCXGdsNrsLent1|wb{38Vi652`e*iO%M)X+V9 zU}X_2<~NK%Jn}kLY)*^4xklneWcW$n$|h_PA@l^<%4c!23|tI_9)+kCWiGbw-?iwQ z2#QJ1k`d*#>WzRZ?;KyRof{pdMB@W&*F&AN(%4WiF>XlH0)1xpF&T^9*Edh~zY~=P z{h5*2lf9M5au9W!4IXGX_R8j{H_w_?!r-dFov% zu1{m<2~YPoVJvVFgn)zTqHJyqDfv}ct*cGrtsRM>LOxuEWL3x+qh7@_aDAF2LQl1J zy=>6D%LK_uw^ATw=5Y1Z|Crdwwp4bW|6&$i<$pN`at6f`2Xi z5W*1Ew&jj%VnM4d9@My8&}K!IO`3Eq zE=6x-0v&1M*frw6Jv^)S=WaStT))=lP2}E#g^Xd2n9dQgU8(lzK!>6!b+C?!ePE;r zz?Mp6qstx}5kaL|W@Zowo>qV>MvNF)xQmFt23j%|B=QOb@8CD6q$l!MI#jD2eW=YD zGwtnM=NreL3P11jQ$m6SOv(Qfrj{vj*yKF%tL`@!&d8K~E@W=Nt+Yc8y9D|8UFEPT z%Z133ko&7MZAa&IPcc>Zk}OZtD!jK08q$wcS%#sLhzt<2tEH5L1+oP$EmcrUROE++ zY!bVBJQUt=0RtxO+=7ExHBPUSV3| ziWE3gKG|^-7shk#HV4;R%s5*_kzQ!aGO={FK$Ht2l0!#?L6d5Sd*>!VXqej)gzM4~ z6ig@ao8K#Qu{>Mcc*8cs-(574IKITchG80yoLIjq13GrLXMxfn0FlBS=Zv#;J(wtk z7Fad$Zn-0Lw|La`bREfJr~(qQdVtl0{34LBTks|CP)_Q7PibR)JG#fy7?{Rvfo&}z zB#VZ4B&~6a2b2i$OG2vBj3c_7a0P(P(@6_2Y`=Um$b^03zm!+w3`HICN^n<#iH;AM zs}d`TC?oKK`xK;ShCuu%hVF^r0x=7{vXYWWB$gYQbZ2UwTw@Tjd-{p+7h*=k=C$3z zX0j@x-Y|VUg2zr@tssS#sn_Q=1h>8)kcN{YBN`}U?rqc)ml^dW$wuqUBpCXp1exwBkU9 zRB8|GVm8&82t-G#2;@5Vl4=03n9#XKgUzKRZQ0%aN<$jKhdMbthN$r3;-VuBe8o?M zM&*k|8j*-@kjH7;C)1zXM9dhWg&;*?9T(a7(T>_hBUqUAolzr%DsOHCs@BT{FE6Z9 zdlt{m#ooer z)xQdob5_XHOu*)mV!a}Na3sNy6tJuVa1vxLuwp-=KEh)N9eeTWTV^`-;76j+hXE1S90kirozX_ErfAx_=~TY^BkA zg&5rQH|8*^?R;y~C>A21hX5ce;l6}&4^%&rk+K}Z43Hhs+Ei-2;u7;~W#+6rLP}c> z!DLTrmU1Db1@0X|4HB5^NJfAVBAa93p2hA}EaYVipc>;&p-qRMIAtbOF8|jGo zLuLOc)sq*s>z`<-9(9~V^lcZY40D-5=7?V=`;Nrf8Ggp5r%pt_-*+Sx^MMwZBddV~ zBDV>SUW}t_6CmbHm4p-~j@EydWmD;+F*LeJ$G};tnV*^ZmJ)SgS}4BFcQJ-H6_wVu7j?)fLTw#kM}t#5W#*<4T3+8p*j8kSx!E6I#;q#pzn`ypN}`}C zhbM8w9i;_IGj(3ayu)j8zk3{IgdT@=9yQAqCr-L$h`Oo^?bR3KdwB+nvI7kOIY=jB zD#zlA1r=b24&i8LOeSi6tx6!{$H-19^Ssezi+QZ0i>MGy?Xa#Z?vATgq5IZ|5YUuT zw@$o4f}`3&#%QR^q}^Zo&&m-N;MqTo5P|v~fRhD;ckU&Qm+@zy&e_m?pC9IZ%y5lbiixk$E8rH$J3J2_Gadiys|U zO1>8GTF&ECjN-?*l8p&i9MFEEixyCG;oAt1vUyXjsrg~KG32k3DnUy^QJeGq2U9p0 zk);-RHLq-9_nK*>Qir+T!hV4sLn=%or|EEGud2A)umE-wpnR+_=!uM&?aPMph+qbs75d)QYq)gNs%1Q zWtt%4+@_f@Eybl41UsQ5-VNH&1hF^Yvn3b*(t@8at6Q4eh#kloor|@onyy8!T=73h z_fh2rr6I#n7pR+wlkIwif}2H%wsr{KIvHP_OpLDfzcwAOWmRk~M(nhH_lIk?B}#e& zQ+B%&_QaBuW6|-PnS(oxjPxs=l9*>&Sy<`Qu8la%#hVa(jOus5IsDEI7)!AV1p_oo z?XS8uE6+=C{x)dpNwVd8q&Sy0YuR6}6L&%<8by5uKj?|?qAko%uX0@kF9?H3u1f)KpR~JcI&9nB@2*KHzHAnz|s|$?m!X^kP zfm@xtD*>NQIx=b!`X{-ug4}O4i(PS{@*=Vum5qoLZdiG2#r@@)`La+=9{5mA7Wqog3ORZg_OlpD^16}cC?HDV(vzt7fe zn?e3GGrTQSrO-4tl3(v2Hvp=O<)(>S?aO@#Z)r)O&xg_@Z@12hFKcDs7vrDPNgK)? z!-NN4l}WsVfGG%*rNCcIGm|&Ev>q_eA|2ZVXGp#3>vQv?X_kyC2S!fFcOS{+`y%KpWFk(@9q6W_xS5*kWK zKX@+(15LWSB$T-(*DRgu8rQP==rVRML>XSjnf+j*(PWq2X`jEb*V5 zdG`spxe(D+->s~C?{`?A3tliXpP3!(b<9n{j<{QftnJ=A(O&N$QiEqFfB8fI{T~1R z-XH&v&;QiB|L>ju_;dgJnexpZR{LYIG%+&na7 z5Yo5Es}v&2VWdG>5Dv(N@{_d*q@6``d7ap*pD&qGrLqHcy+prZO{`tfT_#nfne`2B zVQBWg5h=1sQ?^?o>CS2-<;ulg$zVs-yPLRWbKVS)b<@9!+O@$)hS(=yT~69ZBi=b6 zQQw&`6w-YL8ZQEG!qK17;IEcwYiUgj>G#ibwkz=X-vd~N(oY;o<|Uugd9u=*wkK8f z_p`Z#qARqJ-O%=>`IbXfK@mynV_7Gdp4NFr4;_=8Cvah?=L^;<0L?(H3p6_@-)63c z3;j?yUhZLTtuKTMEbbR*^dS%5@4#7md_q%;NS;^%jN-?Tf0C%zP`0~elWP5w1YDMr1x@=e3d+J}5me0OE2vqDY|s*@ z?eI$i99oH%ZDYm}$D9*~{zyd$?cw-+`GnEEN{LfOiayVu@rZtX3=@u{Gl|O#-W}q7 zAA}h+Oc?LQljMkPdz!?=xPW|2m|vQ%N>A6+G`VTTXd)1mf*R2DrDX~Pgr`cGQ=4LY zn>r*M=9*-lO#Bw3JAw@!zf zjJhMq_K}KO_7r5^lP$xzwMEdTSE%;7Rl(!^)42snQTa#r9=F&HWhBsHP+J<9_@2u?e1Q8uFA;Y^8&*Zip`r5 zvX`-0ov=cFT|PgpS=?W%1p<7OX<3uTmMK_Zf3%+rKZgSk=;W5FfIvsGkH{}#%LX~6 zvtHVzgDa^vj83Z}`g%p(U#ix>*DEXHP`pCn4OIR<&SY!s#{T~AaqFbRXG<&F+i6fys-z#sZIK;A!*vJbieXX_)0ZKF6)M zZyvF=%1iQ~8mfN6HP84=w^_*c~qLKRpw}&{ug4O_F|p1nbuKL$HUk5X*xH=+aDH1qp{=zSq4B6 z=+0RSb>2YJYG))vJ6|#v`{~}c^v|{}Gd`L1Wb;;6F%plg;uQm4P2@GPD-!n1u7~U3ymQLZIt+RyKJ%B}wK2!iou}5up@=m!goj z3091sMlSKc2*O%UMDe%ZiEcJZ_8}nFuCvNJE98_@G%u&SBeZS@tB67cPDJbS(iQRY|0tpejD<=XG~e9KbwgW5hJgWKh@#7HF)#e* zo-<6snr|~p3#m+=p8Iy38k(od&Wf_0O;bf0CG4YBcwJ6M45Bb(Dy+O-p!8PbzIj7c^rNR@9LExo5_Wf6giSdUn*nNLFNeNQSLH zUWr&(m?jyCQA||$pTfB|1h%v#spkU+AtxLnGty zNQC&*%#^^4*tkM^A`%!Evq-1)1q`J+=BR+!bR-=b7&=6oCP@wp1%rVQ__+CV2`cYZ zu3MVFDf`nu*5x4*qs7G?X%0Ep56X-SjtmuSm8jHC7>h-(*Om)-rUWWnvrk$Fg5l6? z5D$o(Yl(W-cF583!9$KaW-BPHW-D5SaX`2(34&o!RvycYykOhp zMv)gj^c{F+Y;Jru7L5+e*d<-thP|vx3XEQ~Q|87&Ai1%d4L+QaJ7kNsB~3q_e#pdP z5lN{EPPcMdw2u0NmZuFHrPU~eAq_+Dv}#j5&L0#usw6zO&YqAaVWsWPMxl6aFFRMS zhRLctv{n+CE&X?#1twxc1=t^Aax?Z<;S5;IQ zN(6$zkhJA<8wnite=WMD?~zK8ujNN~C6w7zP-b3G)zcJ?dr)iXk=bD}8&t(IS^Q5N z#}ljRlX1}Dl3kzc z`KqQjF+5uy!a0Yr6^%|zg$>pxP>)V35>p`nm{Rp(%6Vk+S1Plv#;JmAG}AtfBIx2a zh%a$F5RSvppl)_#t$)55y6INoH##dh^idJdCNjTMRgWI5rnS=ATi*CYBXFP+0$|)i z{TZC%Fo-8}X&;WeYI#~pVp)!5lRJ+-8j~3v6r*CPVkg28NSs_MG-+8mj3BBBVVr3a z-s=3!Zk)1fZBJS5nuOGTEDeJ3`D4avt0+3;`NJ2BxQ2BcLa1XA-At1xfGP?{!?C-B zelvFG*itne1OaHBTdN72B!uxa@?9j?k8k}`XGd?7Sz%aO2!TjI77Pko(NNUf_T*gX zu&L2}t}2V5ni)nnH8^H`5D&#;QF$7LB+iJALV@w{h^2wtrd2YC<$m*^*hJNx{q&w7 znDV2MXw(yd&=LZm#3UkXQ$tH%zL2P)$#PQ3jJj_X&T8~7BNdGXcGcq)ytW>U2Y{dm z0ia4HFIz8(r}LT3#-d`f^U}oD756fU7Mb&rilRuaBo@j91!Rm}hBfjTc%0Qe@@E#w*96{*C63eYfTG${HWC)mbLqB=4~C`*1pFz8XJhFl zSb7*(soKOtsc^_|rMmRA_jr`UVMRN%?zJXA%>_~j%&fdnx-;97D3PS*57?m)vlFS&x2;TDFr69cV z(^Q2~EZ*|giy+(}9u5Lg0xBvBnH83DvZB!l{}BEu_6aUpym@luH=ax-S2v_TIzMM8 z9w7wARw)+VP+xv3mP>TcdFa64!86+W>0I{zf5H*qb_+-1!=>etc?E^9|0gEqC512M zB8ca4)MYa7m66!kSX$NUPQJQ7Gd&9eL7@pFD>$ZWHk8P=_8A+3X6TLYF65WApH2p$ zFf9*)Aqk77zWm}%q>QQZ4Zh^F_=aMMEoQ1?v#~&+^?Jlgk+2aB!ce?A3#*MqrO_%00tP|z(rGj#bvr{(4)rNwF-+1<|P(~F<8S=>M<7m5Z# za|07WS@7f~4uar}a4veKpDLQlO`104NuZaF9NBx#N^Cm_L8vbVVc>KYiO$Oo2#E_z zirA`5{;EY!^0z+y3Q0zMBoP^kj)Z4}G4M_nhQat)Jr){0K%gQP7Cx z(`76kl!(Ow@ljC=nn|qkBZ%bJo%d%(gLMr(tw-E5d~`bq!r*{bYOxUqpS*D_aOGMW z8)=N%B$7#Y?T zg24gO4zq(jkP#Fb5H5ikn6xJdM`vWY+s4&l?JMo%yC^CZ4hF;G<1DsAdmK>5oS)w9 z>8bh2xe9)Epo5|$1!$QAkBk-_HYiw%%WrB=x|;1PVs4!(oBghecDqFcSn(o4Ob94Z zB{`_VXw6k}PGocSW~?(9!a5Z(pd*7(4FqHwgu`MoGAC!sTnfOWg5mO;jbhBQ5yWu_ zS9KvI9znFb`|$u66o8;OXaY$jl1U_zNhFdv{cz6Eq(_=yQ*${{ zC9F0ZZWEf&%i7#su@h332(=v;dzlkY#+-vwqB56u3q_6_!&Q#zyeH9&XWi+HpK6oV zMjCO{QrelxR~apcVD>^6hP9B;!X}7|!(Ri#>p8MPM{xn4>xJjqk4P;fO~txLN7r+- zp&RH0bJm2UN43d}BRvRC7x||L8ERE}8Fz)F|Gc8PbF?rTxXR|4rX3yJMo%V^qfpD> z;8R61QzEutU-3iEj|3D(f{pPD`TRyq7)^jueJF{R4=Rg_JJ~Pa%@-JRWMzqeLJF&ng+B&6)5noTt!LL5gBP{Ny~PMXFj&ymgcyK)pkMqQNMHwd!ST#8I9 z1Q-Zm3>6kD3qmjWlvy!iFe4KpvV>xxYoc`dif1zEbwwiNQaLP2mL%kH7bdLZ zITfipD2E7fl@UfMgs4J{;(XjeLJ9~dg&EpSlwrdauDhAq>YT?UtmbyQN{V8&gcKm6 z2%-uxP2yOTg&Eq$Qdr!@5lpSQl$UL^N}42$=5r||OSDO-qIObrlEk{CsOBk^I&u__ zlBI~G`4o;$iQ9;9fkB51R@6r(m{GLI)f<^vIE4sNiX(_qbW0nN^pZrERCXwbWh1fM z^iVoJ6ML zLPAqkY-VI7Cc-FcBw{I?Wh6%I+- z8JLC`f(b(uG$RN>kj=3wA`GKALCiV|`4yGmbPt8k>G{r}s61YRpz7kVt{S#DrlOZV^NkKDuOhelHz`A!@nQ3czv&+_dLVw0f*_ z7Eq=Tb2%WCO3KuT!3bfHPBjsP2tYyzOlmX?!H^~s?MT>a5nHYt#WiI@_YwJf9AM)D z!B54?j)lYVV7jKY1z@B(Au%?+EM_qn)wD`v34>813PeE&fw*UG+>k?IoI;|~aVfSb zgP18NBxSVd6ooRG(N#}<(XybTLD1qkqBEgo>6wuwwnRn|l5O^cV=^{WR^lL-+E2VD z8XDnzaRnM6j&88*sgRmg{y7xM2vmFOQ7J9QL$_=qjBy7-x5k95BE_C#L}xpgrDcp2?Wg)hZEq`B^t7lp%5euL`6vw4Fp6_Dx}S`6$ z>^X|Uu2AIY;&HH;eiEmt zq&-4AJn~pVnEEkIPwQ2|5+AgFwO-Y@F6se_0xagLQ!rirscL?#hIAdG<^f-r;-LKwp^ z$Pj}dBn-w81Y{saU5cpLJpw{ID!r! z;tDXM4shaz7+`{qFl8Jm!PpAGhZsT56v0I}R}7<+cNoJ8!CE@X$0({5<20!vh{ZFC z&PFnbq7;K9$WZy5LJZ=t$SR0p3ULlWNGpm#$QXkhqo76*b`HUuW);jjg;1qA)Kr-# zSnU<&M+;+PVIed{+O>po4nc6F6+f@t%BA#H9|gf*i`TDyE(`nKhl|Jicl8$#nqCsG z-oNW=37q2?${ws4lJSlC%|UOez28FiNk?4&Q`wa5gW+7{ye*Bq$6gC|cb zDi>0y7DZq_Ww3elYM`f2YPnpi3S?F5F$e0+I&NG@q=@QQ zM~WI56+9#)GWSi-+bcaGQDS8Jh~36%JC9Z<%>FYn&q`DG(pzpz3V7B(qOMSDN>pXM zNfG^}n|~5kpQLGy zrH@6DGf`DItU-)+U*2HGlFgBI6L&eUd|AJ!5I(S)Ctgvbg;1>8&@>fjKtSokL`N26 zN%IoRi1<_`#c;Kr#Uh*=iR(sMhm@=7w9;*6HUy3C6lkjJ8Qw>a=v0yotdp<9Ce@jTcNrDRE8Jzk0q5TrQTbKG7}Z?s$Jj9Civf=^FY*-QJH4@ zrnc7TWnWzk@KK0rX-gzrm`kwcwyDxfXRjr%`F|bT#sw4N23@5e?oD*6Et_1Y{)Bze#~U&?hunv1RLrSx#T) zc&G~h+!IvZJf^EtK}NGz#=9Psd?q{GFCSGvJbYA`)iA$lVh|+Qcv-PQbn^{4zQP<=;6V%1} zNu4{$Q5(RAIq)^ieazQN1264yi%Lmh%O4i?!)l($A|YggdKwsXR8NYC>N6UimCgGu zl5orN7dATrCk*?-zc*k+^!*(8y@Xufa~~^<yDQ+HaPiER;trq>yZj`575`4NbO z$;E7=C!<;97kkaTu_;-Nb4`@CGC2~zkF3OBXxplndEjb;f2IIZ; zp!;rL6K&|CHEj$|sv-*2R2ed&gdkHBP88J=)j?Q^R2$P7`|?P!>uA7@v~2BEMnuwd z=!Q_h#_fG)22yMkvtAZ+>I(>_g0>-|?>O;}=a1*O4pB825RwEYXCWE+_(pbmJ0mz0 zB_kiI?rOXCo;jDNQv3JGB~y6idOrNO=eZ>DVcv3gjT;W+&eHJ)q-bKS2rE`06;{X~ zuGkJtw9_F1gheUyHykt6#B8i7XO}i>^xT_hGkFGAAK~rBs9i^-FyBfq*dr~bAF8wt z(}}?=Ay^`Aej6z&A1%}&E$jSKcc?0t?~Wo5ain9eKb-A}rdft}-jC_&%RAb?*?Q{~ z&m3hnX^i3Cqi3B9Tly_=Kvepr;SCFenj}(QZRa>i;~bY!-pk1QE#zaAu6yy--9loC z8x>MgMH2Q4OV_2OX_Ey%E|lOPO#meLASk+^EA^l+R3I{mP&Dg+Ho(3}m7QGY>a4rfPbTj1!xHr3Wz#X zCbzLBX1(Nmq`%M2H=^%(MZMqP>Z&lZ&dr|`zS**o!!7SmF za}_2no@NyGG{6c;YFWyV`yCR6DH=6Vb{c9!pA9fxR#3=CH|f{p-c5c1yd*^a&t_hN z4}GS_ocpBBt1myMk3z-dT**3`=GhDgRtd>>)#aJO3Vct0TZa^CEg^$85^zHdNQ!te`ilon zw9oN$KKqRuHZ!@_8xe_*7XfS^ff^1V@E_8!E08GhC#*+C1RraIo`XP0D0Bq?M*!bm zXkyv`F_CD)#V!+>RH=1J@(1*UKKJ-X1eZW(aw~oy+?Z=_91h zX-AQeC~Is479?@?GJ#^wppy*w@x)@F>xXVKI*?YvnC3Aw#CcV5Z^#1DLPrLIO4HK( zTY%srK_}{^2^xykH;Q;OrC9vljeXZQa#11RK+K?AXGd8;4**_3p})SV6T$3IZ4RN^ zFQ|i3WuGx1f+HP`fa|6E)IvkplZ+P7IE}hqx{N@9wV@21zjZk>c_`na^P$0Q53oV?P%=p6#BSFNfo2~4udE7nnIVDT_=HO{6%#gpi-2UXvc zzw)qC+Vp&o26SZ62*T=7*)7Al-$V@jCLEbvR2NdVs-zYWy*OBKQ$;+-{gQ%KO2&I| zNZAq|Ggkcbffz#qkEVw;#WzzH4MG?%)zC0Y@gvq~9@P+WxjvP83Sc=wfP&B03|V}~ z0J(!`(An-@P$k;Zf{+rR+=I$~$j>+-#;$qph$GFist!;;OIqJrojB{%>fZmz+{c6T zR+^a|AH@ARqDw;B7C`W}a~Vp9-Bz{zyS{QHI|9$$T6=lq8jgj1e1fqkkg0+wK&2n-s?j?k(Dgo#py8hQJXkfS*hQ@IJkXlXQ!P5Y+Z z(K~i8-BzCEq^qKSiyXvkpakfNAU=uFTHR9}3=5|e{7VKAW#0@#?6BVdzrQ=L}6Ae&E zPLl+Qq73$T@UTp8eh~4zN6D9MqBBbwKe;;@#3XTP_CVDNxCl%Swu%JAB*|I11pqdy zpqa})v0#f5vrm64SA%0xRdlx#wzkJ%EjgfwbsD5=a@C!XV?+_)r@e6Z#KM`JWOrjN zPV`JJrfREPYY9WX%wsK(IV0f+$iK$ZmY{}GalBt5UhMbGyF^M>G5MvPAm+QYX zacBLi!6b_Iy**@1gYpD)PGU`zk|W(LwA^LW^94$*jhn+>2R5DTZ4tGS4jjR$HB-b1 zFmu9UXj|EHvpWe2UKoRRd6Y@`ia)Pj$XoO9%Ft+4s9`S*Gr-rr(_tn+sv~24p9~XP zWj_2dyceLWj_6VW*&Pv5kEcOjk%wzZF+&_u0e#aq?bA*YQDoc~_ASyuzI;egukn_p zhclAx-1CrfQm^B2Ni-L=vc`qbOAWTu7QD(yBLr^5#)Zl#Lv3ZtWjYmDL_bH|b9QT9 z=vi%5Mc}o>$S>B)_1Qip_anG?in&{R^W}^5|B>I_qv4sk0XxaJGkYY|!>G=Y@O=uV z;&SxcWj-T+yj`lI>L|ZFE0oZklMC+@S#XlITiW37ml5Av^(geb1kh!ZT^E=t^-ruN z;wt9<{Hd6n7Oru~Pg`nPYcCu;{HTUO5wcQ@s)RCG0ZxR5#xA?DtU;B@!_p>Up-Bg# zNb)x62W4Dg;RL2rO1#v~tmR@;%VGd)Qd!3|NRDy_+(nxM3k#|b`WU8GOCepvO_-NU ztn3417r0d*SfH5}`USLfoe|WBbb5`gK2JcCMv)hUm_&RPFTDiC5LKK5DjCVR1}Hj& zYEL1K9J1lO(CW)&5{lEDLkLAEZmEsx7#U)myO}Vb z?<*jfiZtQ@h5|ez6xoHJHS1c;!lBAT(wr=qC$V&!jeaDL%q3^s+Px-m(?yUi@fMo6 z#>Z5_&rZ92nVK)OQ}j-8);8}mfxf&zv6M-YrfU1o`f)`BED}(kiXgg-T{$)_-b?Ie z((qj%t9nQ4XE>J%QjmQIxXXKa1#6rui*Tp!ek65Lb07k>36cMf+#2Aa9kT}-*k>94EnylnUWm0Kg*91gpr{X5v; z<14v+QI&fDyTuli>SrMC_LWte4v~jC2>>(Kwn04yM64Rj`}g{alQvlf%1!@7c8lY#PhiNNq4M1u*+lr!G7<~hr1exKd)DqF13`>QMK zG3`WN_dX6-u|JOclz}~#DG^Bkk7QrQYezC?ryAd4jgYj3soV)sPn%yf0Y-NpjMFM_y+A$ipAEc`?jEM z#PIK{)dg~AB&X4EMbu7`qq1ftu0ffr(WXhmqSpk;EPE{CUK?0b-OGk9K92 z;^@mvd)i0N2(I(F1pLLyP(Oxh!o?^1>dqymZX-E7B;4plw94}0FRiUeWX~~uLO27bZ|iL+?4fHHkcn(weZ$HSOt+-KIkUct6ChbSwn&?i zM=Dli`X`4WOah-!qr_8YRp4)opUe+!PN@IPR@W#r+mmxL=K6nusN#aGk4!~G_iMZL z5=HR|%Izq!{3rvJqQ+^dR5@l?rg!?W6vQiBT;~yIB2kc6@(;Q5f}Vt%(Kxi@<$c4p zj^fcQR!Y@pu$8yns z;N#g_4|b9uL%&<59Rigi-Jl|8Dr8s5xctj2CwY8&de~TkzO*{;FM^e*+B76`F!^>Y zCSnP3B$As;3ckI?8(rPxbp4!~%_DGFr+;=OvbA+2cEfT9ZcEs`?^I2duNjHg!Xrg1(7 zsG-5_oh()%&_bpffm__fcFFs>_KDFJij@P*j^%N<2EDcFgN9(n@@+ByDr@DAuh0*e zOV;?%$TbX5rDsMCfS8|%#!8hS!BN`*a&iya6-Z*WUT7haXsdw)c_Igtio0Y_e$7`@ z(|>A3jvdm<(Yood&c=$KZHuq3kx=Q~k}aDC*AF3YBX*NNt8oMHW&OJsstF)ZnE=Hb za>ok9aa*!8LgfWQGr#I>8J`a#)63GG9Yr#%1V&o{qiHA{?d=i-w zMGFk*jQlb8vJ?t$zpR?Yg1_JKgf|f7UrA~> zM-onG45LR9Cb9uvw2w_;LN3ttBjFf|RJ_a;U8g6kLW4vIYK9cKAO2BU*^rD)1i&eq zsLt@5<;4GJV@_5&St2_m_VRynU20?40-)&xpHZzsB@E5Un{ZfykVc8;D)wo&q?(I@Ep6ua_z8 zf|OG=2`YdY6p$RVL&b{;&0FFl! z7jUP(q=V2SB-3Bn*uQ47-=CZKS7**c=QtmeNO^Ml+#u2R-{>;ce87>PQA&`_`?34E ze?V2laZi82tnU={#@x0&DjH2EsWX-Xcs$aJgI6?YOB2{`S z@V7gWH~zsCcTDw;qtwiM8+5c(90IqX8>1$%PTdKNxT_SprSMmeQmWRE9FwGi`XZy_ z8JQ9SG-bhpiySgwC5qHxB~PTE(7&9SbI=XkXS17tS(ryMM#0^Bp!L`dWJmCkCnGY) z+Vr|l31F~;l*5#{g^MjAv&9Kg`uiM({kZqc7DAR^CPq1x=Dj1oEx1z8x8+8puR*}M z%Th870t-jtT_G4{8@xK5ITdk7WZw&AJQ2r^P1>qL9C1P{&i#Wonh1=e}F^IIdZrQ7qvM7@j-j++>P=~}@`?~V2*yIF&>@$vMm?^9X z@{_L@@wjUFcO$P>9CwIcTJO2(fOaJ{#+jvxc3W9skhNXX)%>t}rm zBB7K)91sR-liY?hAR5ps0t~pGgO)skfGAQkXB?CiL`W-sIEyUg+fgzonPZS@{=CUw zx6W{8{$%}vk7@&nwb;^Su7kL5H{mjpsS-&N`$jfuV#xP zf&xuK<9`95P~`%rdUF&J7h?#QvU++2aB3&cX##fZJFwSAN-QCGJh#K-Kf(ZRVV0Av zSlEx^_EROS+z4*|!vpD4AgUih7}Ytp_^Ui2Le&tXwB?w>DXcm5!2f_aiE%6K5_mX*LB1(FY&9gvdp*xmRVtedx=$CrCK}e)k zDkT$S(3vq85j>5?_-hpkWJ$-GPGV#;fh&hk;k^pzpV$wiNQmpzEOoCjBN3zd;f4zc zl+u6DC6L1Z1>8sZAb0}2?K&~Cg#t_WJq1hAVMGB0T81}{M`lZu5Fp4f`8bKp+Wj`TppAzi76K59A3|>tza!MhFR~rvX~NletqEAO z5j`x>DH@5y3VwtOp>$CRj3*Sg*b_aTybtdG1##35H2}C#jnc}qZM3~AOT_BhuhkN6 zB`w5(o|buwLsuJU-6K@mT`^3O6q~0Jrjj5&;|y_ebW}lck2W9qRFUTCz}z+Y?F@J+ zaB6ZXd50&<99N8|naP~9gpLTOn}O9R{f+=%ZkG=j9l9vOo+8TH5f`Pn7>*7q1o)7f zav-@zB66vsWKsZ+7ISSP4`PPE3{zz81GIt;T&a8~E{pW?p3m zOmG0fq<#!b90^p7Qv3y%X`Dty!?7a=Su`RJ5$OioS z$L>LcU?H(1{)OnmIwkTkv(bz8iU!~|P8cEaPpC_9QZBt_F9a6Yd;avF(EoW{s6RmI z1cc@av=j)%+DSjUb?kLgL0*zxx1~k~KmMLVNF_2`I`@k)JeI~%l_tt33A_+&x?+~6 z;6&i6$p&(A!*($+C!d*34F?t`r1Pe+QDe(%oFX_I2`iU9DuOdY@fb)pc`hiGV;m*J zpl#rmV+XkRQNJcb{zF13^7Eun(|VsIwYgrFyI&Pq(w$IkUNyeU4H5_(`{g%er9G@j z2M-s}iNCs7;*eFhTq6v!V-XM<0}2TB7y_2S%_PJH>8B)PN;W6FPQxB6rGI)3NlAHR zaA`|OLPi~p%PQ-s5gJYb3j`P}vjGTjA@7W9WG}fVqH+=is`XBQ(+m|t#A-a)1L#QN z&85{hY+>ed8&E8x`!E%_f>Dj^Z)9R5A=7RI%ArPxM?#sa#t8`HFLSuH=zcEg`TQWv znh|iXXZ$*sswId0^BSPP-gWPoiFOAJeG-b9e zv<`!jFY;0pv)5eDN)|MyY#_4hRRpmvEHah@Fm^(9^zBJ(f5C1Mm~OP#f*F!yffr_G zg4~y&;Boi(&;Yjz|M*#)X1q@S?(hHi!+-n+AN~$+{|iU|h!6k7cmLu|<|2R4*p8A6 zQcN9=hZy-MtT9l*+F{b=JI5s*DEVzR#K~_xBg=S^@C3B8McA=?HFWl?;nM@8Q3f~^ z;t(W}f{PF_;)FP{38apj;GptGhZ-yrNnz_C zTaa6OB`#NMomA;jcf`a`pA|M-OxmW|O;+{$X-bGJ@q)73a3G36K~=g#m~P1~@1RKu}|h4Cf5A9K2H3#{3C?Z+h(6+Y$O;$llj&bYRFG^yq)3`4kOl*J%L zP^(b*iz~It@**u?6ub13?X&oP`O&3ObEd8P>x zBgUL)fdz>zrLC%IJ9lXH+_tOxFLzOHt<5~%JEUztO>7}#5J8VLlUgO`kZ}bV;}}Xq zJJoo&YSzM~vZytG~ zT1hWzQxhE|aZGWH9N`Qw!yH12=3^tTHy1nWUbQ5(sWm6*{#sTwr)KZDl;Q<2K!AgW z2nq-kAV9;4DFJlvWsmh(nZ8nao0mh^19+T<`l**P2L_iFor(EU_(m z+rJu&L%2eQC}Bd1C}Bo%ggdcSlTq`r{yj|UeK)I@(_y=_7ZcQqa*T0>94O(34iGTm zMF?TYBN0(MKdttyEw|}6vm^MLs6}LQJGmUJA_PI0K^%4&%y&Avsy#NnKhH?Fnpr2b zwY9#RpU-uw1Y$9eNF+2QN^7J?PT$$pd%K-O@TwG3l&2N?*CjU)fdn990T>v;1|Wkl zkZcjsLlln7mAtVXoW&!zPa-60Xfh0g5HW)Y5W@^G#1KLg8)*fq)y}`ywnarDNy&S2 z6{1-ilbJ+OIV&V?%FmuEyQ`hvilG6Zah?ny!W!9ReUM=RgbbS@p@r@inw2tW_RetL z(-D>-WFj&lv9Yp7$tqSzp%MdU0VxTq_JASMlgpl%sNWi`t# zU3H{{n2P{T%yS8L7|bMbaqpAj7-SQjSUSnMrf6(vs5M#LntDIqC+7w5wl`ZDWMS=7)*x3 zRzngtWJJ)M+)mV$sg&GjB{jMwkx1ke#UrJbrQc7L0&9bf292AZaidPX^}(Uga56k` zkTC`j!3Gdum`zbKc7uMj%PAH_K|w(Y1i(xaMG8fdBEevwK|qiZ5F!MGK?N2PDiR`X ziHSs`aT%Hrkcw(zPtE?M+grBaA7IAyB&S{txCx5Yr{;rjf%ZxAu9E!UG5~ z9ZfW4FC!6=$&g@x!Wjkm7={qa3nK_%CKEw0j4nuyg{>4UN(%zQArvACNfs{%;d1BDHoNxh>nf1t|62~*@b8S z59w!KN8ucsP(@6|2zMCgv{MTI9GFoiAvO?~LKv;DDb5wVZ&=Q@h5Z?G*HaZ(1HeKc z0O$Y}C;|X-3O0r}4|d$l{W|I}mAB|qX?9$m#5Lq#p(#W`SU{ph#8lrk%F`q$lj0qi zsAWbi8FF6CR?3Df5#Yxc3|J>|KM6`F+VF)XQ%=#w!-UQy6f_hxIM0OB`Ldx4ge({<2&Vx7Xd{$oEh8Ug zZWDM1TNS`c(GXpaUCW-2kwDsLnp?lV!3DpG_Vb*+pVX9*c3yN@XZtDdg< z&qUSMZL(HXt_#kgSjb3@B*m?k{`RWsdYq+q9SAGo4W2b9c_s-`zmAp+w+vE9 z-u2A!L>yy+1URg!(Xu@A%@}+dC7UH3LB=pB z#1MqfiScHI_|?8O7*mE6V57L|s97&aOHDn04J*bl<{T*pb2kS0dc~zAr)SK!9Ht5? z2V&Jv#E;1rEIm$?1$oD24B1z%EZj*`o z5^-u*ZjmM~Z26NZw+nc&K_MWJfU&}kA)CHj$jF#X=)14(C>Mo{A3k{Tvcy?KFDq7& zyCrf)txk2>f~HQ12#B0ES+*A9t&8$k<)p3K^Q{srL`?9Yh^fNl*F$<0?@3C^N?z@{ zo^8t}$_yJEIx0}w>@&ry68ENLC9T`{Y+N8{NJL0bNa=@HMXFWgB&8)J?b)TqJd!AI zNK{Nn;c{%IE&i)=LPkbH{fb=Up#p~w2?>duHbuo1=B0c0Z{N3KhdAo#Lnnk!iJLM( z)lur5TQ@Gb3_|WJ}FeUX4Sg%y5AMZ6*747^JdEvRVFy2TGhJt z=~CBym?m7|(F39eN;;w~bymIWc5KwAmYuFECIpX&h@Lh@wG!r~o42mWS+VaqGDOM2 zQ8AGrGeuiTKk3O{mywmdW|rA_K=_cj*vR0a^3Eu~_N3-!ref5OlqE#r7+tzx~nb9%Gh*B;U3{G}rK*cXAds!5!Su&JKZ7RCs-auR+6O2Xz z(Lu?Bm5TjW-O45NNtCWZYfH-$QgfjgxMVaKi4Mu#Bws~qA(qW1(@9KdD6rHN>iayyGlbQ(yqd};^T6ARTdY=`y5=msjT`0e^ zq2Gy_F}Q$ME*c92$0d&yr}0&5B9ltw@;jREyYe$8F+UNDMPi`<%}SjsOX8Bw=0hWq z-P}`AX2sp#3G+%&iBRTIht@p}=kcod@?AHEU~K}-qGL&%`h0F(q{jLXoU=@lu%dNR{_KFXj& zZlH+@bzq?<1#bu)DU)?JwCrOq72j-(pLpF$OwI&g^U2>6kK8tL7;kJmB(hqtZM6sw z0_RPLSkGm>cq z#3Es}=6t0ZLYoX;xsH{Yvcyq{76WA;#EQOP?cP1 zJVzqw5$P=vhxa-43|YHl>30hvTL5wtp>QM0HhVv z0T@3)nk=df9U*Vzu~aIcKSp(kegw2>%}E-g)?Af{kC_mVtSw^iVe5%bZ2FYc_hA%# zSaLXQN9lXxwBaymf;nZhL+4BZ&>%3#XO^k1K@)K?30g)aQ4_7O!~rrHYK^-{)LK)p z+-Y3M`%I7EsSO{U<)s4qQ6^ue)#k=Z%&;iVOzfI$pS6bw_ttPcvsQ0{MS4Zt@k`hg zOfRZ{70EZ$H8^3AqTXPkxz17M0xUI|_ehlR&hO)ET6E$qRpZ?B(Xi}Ds{Rs%1JpXt zwma$eUxCG;+{dc&D>_`96FVkdv8bpXfwU1rurY$g8;O58v0d#dgfeJn;W_=$fGrBV zH4k1l(-mvVqWdgLnv4x)*_D;-L-t2f;HEkuFBB+ueMW%VO_TWIfnZvrx<2;o%vQQv zVOn_5(Fy$H8CM=V0zp2@no@~ttnZD}i-9g2Jcc`enHLyXWQJV0Z~h!~{WA3*t=`We zko-;*r70UuG`Hl*RW~j@#&-YqWm_3503cBpBY7j?g9}W!^g>1WfEIqlHPaAJiP2>? zu$#u5E~rygQ|ksU9>IYXU$(>i!lo$}nL?O3rFzE58bZ4{$(M~ob!L!+QPwhG$SH&5 z>}jd+x6`Q#wFrI48i&&AtCjT7&ZQ?}ctNM1Wfm%SWM?pMxAlt++@n<&ioFRiH@ZRx zMQ)*B6yWWjWzvv7l+^BE=dFqnrD8k{hG1Qi3W(rqaCM6tRf z6Dxj}OkxPBV4~8FGloJKni*l4T7y87JMH#umfA-v*O6DUU_!}o35;DKg$f}M)i@M$ zP(R@fNg)yKw_pC{Dd1#~JPh{OsJ;31+n0@HTm!V$t}UO88xK;E*{7_Hmhvo7%boX5hj z8ui5~q9Yv(cgnbOQPzS2S_2m{aUmYC#=#+=pogu4O(Ir?Ot<>>mCel#s>mr3*^xtG z&06Q(s_-NNA9$QjkWhj>4sMy9il!+S)yfP6mBr=I3tqJsmUu^Yo#?2^l`ADez^to9 z1vV*hc|X8Vp@KhaY~x!A=?An!5R&j~RP4}Q)Sz487RF0s;x<7LO}Xesz_zGx z>&45|>IE>Si>#+Yk{jY62o17B!?81R-1q_)Y-8*zA$EjnzSfGVL&?DjtQgvvN;4nU zEcZ$+;i@TdmAE~AwOtUPY+*m0;9Wx%0P)0v8&Gps2*1gVDjFg@n3>X+ED4j2aQl~%^F@FX94C^(Vf3)NpyO2%ZwMBk z;yKAiO^cz{G7`wndO?9Yr1W|YaFopELZ?sFx3>cfB*8{GVGWKwYYzJ@3iRn>|G_Ov zM4qwDP-|8BbkqE1`Vo|Fm5TSDNEN2rML4NcJn6%2xm(f zGjK_v`6uZ^FG{UFRK=T>=B)LbJ@1~11@11fIG7KtHG`I?8QOdC$aFz?3FQ}6sOSR7 za);`0_r+Fca>zzQie3e<6M=6~`CN;e8IjUoCDyEh;8DbXK@O1Nw5B|54x;|q%tWny z;~bI}e_T*m#E9LTRDTZq)ro+=dsYd(5>-}9J?ef!Aq%GwEpI@`!H3>Ol>S3;b0vT! zB%$rHnpvt&;Vp(bo%bvKPt!}2uz+Cf2^Ud^>I+mrs?AqxSlnXmF@hHR{|VYNd}&Bd z-509GV4l6$yo{x^WebWa6*ZC=VT(+c5gfuzv<@sS5^Y?)yYPG8L|)CBUfuGCl0CEi ztjw7eW9(BZyKb$s_KVWKI>KbBY?VrmwUZEHfu5{#Hr=C9>yGN4Y)8f#YUOW5cZrCkK29F?@`nXWluc61C{l9G9^o{@Vey$af~jdR;F>I5>{k( z8SQirORfw2vYbXyX`G2L1vf1@by)91N~I-0IWZC`?F{1-8sOzRBZND3o{KaR%auH} zw5XNa>ZA3CDCv}X)oV9TudZ;bl`cQ3lK~->?6j!kQTjJt%bs(ER0N!M%JAK&tJ{?^ z0GVmyGIu9Wp9?}6aT>0i*lr5IW%mzP#v*~~2!F-)(s2(pCdnsp4>(1_uNC8~h##8Q zyQ!a*B=%(0synJYv79exsg}h47>9^TSiksKt(sMOG`y34l*8Rq*;}a7*Ic>xK<^)^ z5@E9MpHPfcoFIUH4TYohot7(Wy(?I%!QKmN_0GiK_*blx~D`c<6aTV@_M^= z*UF@}uS%hUPIrPEyxl_a6xCw!ss%S?fL@X?`WU-z|BX|v)fyYd`r3efw&MIYozm$s z#}fUF;l`A*K8}~r2(drX6_M;pv}C7MCmpCV0V|=x)~25Xb?LxHmB~qGuAxbGu_zyE zVg^4&xKfXi0eTLN6-2~0 z+(<(gJ1P5q@RA6*P-wI($3FSOFq;nZ#{TE6_dvM@hl7j_ACR?Rj|G!O_uVDVP2_*v zbdw4oCM+dbh&8y@6Es1UGuK^Yi18hBBA1(iIKmc1YE4#W&tBr5PlK#+Ij|;cmWgX>C4j}G9hkcqgCyT~kl$36F7atVx=Y%e58{8I0wDkfTp$ckDq zC6!V}0ZD%Y9V8Kx2W2=ry~wa3_6K+zF-&rof`)KOnKCT~P1}k5@bDb+3(#%4CkkZ> zQna;|B}#rH4_SR_or)$+ACPU^aMebKYVOZ`axPs!6QiJ46#1r zTLTd^(xePnqOexR)=;g%-ci0^#4ACTI?95!P0M=pgl|Nlv{$^^tw*$T45Jo}fs#5=ZQ z4`&I=WeD^oxSXFNhl*_HL(i5+@TXOM_?-nDeJOvIc2~6zs;ZVleM45hJ7v@Skp{8q zJdg#L?Pq|6a(7Pg3C`>m|NIBa2^1Wi-|MENnAN_q9@V?XbE@tKT zDzNa|(JP9qrBdGRZv`zV+%;3nB1d$EzQ}=;y9q8-C2$ClCoh_hI2aq46DDi~HURn| z_!XY{R5_Wkp?ce>?FY&%K9@Fr5kYCI2qvrdleR4*^ruIiUye1RR+kNR-5r3mAxH$zLTDJnh* z_u`K7((z`eO#L2#_g**P-hAPT&S4)&jY?M-n72t@|1vp9t~Yg|t@%Wr<|$)@?`I)^ zC5l0l$Ymn7Xc z@{NfPX|MmbuaL!|^mWkf6Qcd4qEs{@Swz|cD4D>V9{jrTZx}1-5PWKB7Qn#-tdDlxeh5qb)Q^%3o6|!K zZFU1HIFDxQqpwu+3oO*ch&n)k$G{Sq&9#F8CRP*aez$(w_HpdwX7xz7G;)n=N~$|% z%YaG4*#p}QyyC%&0vHh~l^7Mz{NrbjVpq`uNDFWsz2w}2nJcbByA{<K^FBxTONvKN34%XZIe4$PJz?i@tO$&Ep(D1IF2qHh7?e&EjrDXDF&@ed04=wE z=FQ2Lh0_J0IsY41H0RgGk`nl@#%L08(Wq;Zk-ZYwTwL=;)h3=&FL~mX`n@jeko}mH zx=C4;^-kPGOpYjsy0itcKLTwTKvv;P3Rs{NDgQ-Lan}zBc6<6T{z4BLV}>l!i^UGb zJq`X5eWzM1B7b`gU?(_42# zjbfbwYzc7e#x_<9FQv(95#&z12FJuQEuim%QCtTlDCtGgesHd8;j9plPZ{6Ja=>ZU zOqLto>W>(Uh=&XEjyGW$qVvJJpTamX1h=`3X)x!-^1RNpSSU~+s353W^6RxsIWF+R zf^;*tL;b#F_~e=VNM#EWAj*aa!s6k#oTgDWm0goku5HS-Ij+@ij0DOf*oXp7vgFu1 zdA?BSqgz~7F?QQ--LpC-lfb+m5j-_m_G4nJoxYmsrb3`f;-g|zlXu&PUmev!Qwb*t z#6)V3*F4kx)E?~bHVE($D+@OC$^D&ZKvjkM-50?%W1ikMfPft%R|$f)eBtrQ^Hb(5 z3SN@))>RfejmfLj z)uKlvA9gjAZin}2Ab=i0gSa-3jn1z* zQ*u(JG1938M?*|CP@~_ko~q+$=H<-oi24GYV=pYZ=Iyi8^~`k#ER71_%qBxq=oHE; z1$Rv25m6+JI5DlxF3gUx5ajc-?=y!a_)2}JZ=zhPA^I$r>D?GH)SU--Hr9X%&ISn9G9k7o5 zT3@$%Bo!WIjm0k+^5wGHd3GzW6}y`M0$M!wrD7yP^9fu=7nkv#4r`Hn;irism162w zCQq%hoN$+UY3ze?OBno1d7==P2V*85gj@kL@gHXP#d2`O zmYt35lRI5dqe)PHOu^mw)OlLiMM%HX6Lgh*NhExhSxW*ULl0LrhtD&&2;}`o2;lOf zu6*pZ%?j}?R2tCT33mRlAEGpf;AFAM$UK)=Sw_rb8uNjS#0e*Y5fIznB!&Je-1PX9 zax)V^Axbj93fRmPGBPCfGA~2qV@O+57tjqu6c-N}U$`Hjtj6}Xi#s%CSn`MxgCiN6 zq<2EK5SoS`(C0X-cxC|~X3Rzo^*@%oA2JItdEm*@5ExU8JpBwt0oEX(>qa`R5_GX` zvwsNd5vEBEC|^;PJJK84-MPVw4fL-{4C}~_z{Bg-CEovk6*eLhlG;61A@6oc8-$$hn0@S4$BrJf~F>Qg1m$l5D&pG zigzs`1;v}Mo>;rC_o~JO=#;*1O4UNbZp%U+sMv9WhqgJ`R8JnE6XWfS(@qHG1?EE{ z^a`L>9x{8?xy_}xLEz8NbhjyE+iR(aC!q}{*Sl>dtOI*_KE5I%@pkb8swC!sc+1Mk zMU3k4A@&y0g=m|?_xRd$n(*2?6dv+T14BKvk$<^A5x+;~GWq)nVCh*(oY%&ZH4|gd zSM&*-hI9eA0+vr_lL6QOlt6hSll4d)T<+QGTM~`j?MitnNAT{op7-7PHaF{BY`FOYId`PgIr9^ItRQ!D56Usa&b!Amh2Q&N-*o>S#f4=Z9a( zkr}6Yl6aX(1*I^rsiEKVU1>gIzfaMVc0ZP!vDke8+zUMhlmRyrR8|s1?~c(j7_p=- z$KHvCKpnN5OZ#KeJAP;Ap$%)NoBL{LfQVby_X=3>g#76=mCr9Sexe-*;@#rR#r{>A zQl0^H7nhiM3UbLBPswC8ypT4U(_oqR50sT?J0|44gi1tE=t0r%w?<}pw6LEf(g z(cxhG0ri=yXkbDuDMRnQZ+y`SPgV?7i3#V2OfB}Demj?Wfxp28HiG)jV)bmM zQH#W+#tP?<4BeX=XSmXIjaKzvV>*Oq#sc>8^q5V-igj;fnMubknVK_s-B^y(Z#U?7 zS1{|nqMAgY{on4S`z#O9@@}@7az2E`S;(;_fB$-u6_*W8-yras0U&HXfT0|ef#iok z|Kdz?#IUO&s{9G|7rI};oZx_nOvq2Ly+>D&5;Ces!^CJkmN!eIiTnBy9iqS)6CPOaKN2fCE)P z0q@`eCXfKHZ~$j?06YiFHvwuf6h*4kHry#eF)TI;F@l(5kSa%b?G=V8$8bdkP(lYN zXAox!@!EAM(qxs+Lm1^b%5oH{B6xWU{)tjieUp*aiOGuiyG_1Yxi3=rE>gA0d?wOP znzCAHFA}jOmU~C|PLC)4$||aguH@X!{nt}2iIm8)*7KUl-}`E&%B!MBB=$$|{j^nGH2#lQ zn)%;qtWl~tzSD!aBZgyt*<4(A-_x0riAD7HqH=l&vJiu?#}s1(aPA!fuvH473Ngh9 zC_zDl1qcch5GXjn=K>fAQBqioFF5O@TQ1R;3y>&Ok!Q=eVX}^pBH;%~I!)pw2^Sz_ znSzE2lrBiA0;CDFU7~IgB>5sli@tDylL8VNXkf8I#hx&sMu;>?Lkk#qk-`Uw3OF#p z;zEoTd1D?Rlb$8Q$T&%Y&KM+Fqs$m-!k#697IO+|L9pTl7*OF#0hj`;IN<~cV~C(dWD^Bwr+K?$Mc8u1{o1u1(QBin3i(YOeaJqN@94 zG*@kv$-enB=$|gh67Hr|x|L>ZyH#CHG9+>`$r~mj+akR#$vl%ssjK_5-M7_WO}0s{ zMYT)Z&ezI$-BaT*?SH+m)zwASQE$C&Kcgsas1q`kvosrw07pQ$zmfgs?BJe#yrtw` zLUKLXETM9eZY~$u2GMFwwBs%ks*W|Y3S}j*tCm)eHzE|?#k8n_=FJTvpi}S~qo=q< zWgF66_bsH9YQ&sFM>PUW6FG)xcwHtD_@Xg6DH=irn5j%fVqrHEMF^K_tm$WLalRC+ zOjerr=~#UkbiXJvqQd+*O%Spt!K6h;6a-5!6GU(mRj0+sF8!msz_?edF7?BL{F5Kr z*yY)C@d;KVQZ0r*J~x!1nzD}ErnOyjEAIK&>o;w9ke5Y;F%mgac)Wy~lhs)Zc(}gu zI5J$pm3_YZzN~G>{GhrC3jDy!Vg|{Y*>hBU*O@KuwZ_hEm!u~5gJ+To_#hXcB!A$- zZH5v$C{U>OMXgg|e415`P88&E?juQ_kCYdk937+yjrR|TSB$h2iU|a-j~B=;rt?P0 z+Z9VG#vHnIj-g^(M4K}@FexM`!9BqgI*yeqG%{A8L5!!`AtZ6`f{*Ap(^A3(Erdz@ zJeE_A5}tY6#uuU_)q^P%IH?g8+!#giMo}D95dwnl$>%7XK^b&(C|NoM)>OOb5h|K8 zX)j>GPemKzZF6&`MD7Jda0OVGf@zOMM}a#)(08~lGdl#8g=1nM{D^QE1HV8GWNpzV_~mBP^)~v+O}TqODb7+RLnz0;K!4K zJa38gtap>4J`M!(!_!ox1Uis2okVKO5Xy=*WJ~u@jk_+yB^-)fSUqH@Yk|P8u~A7G z=OU6wgmNY|O1W$OuqUO*t;{q#A~+fJJTkA6#}{%G!#c82uCtLRNdXO&;wy?qrI{=V zckV$)kFj4*3@tq>J=jWT#!grA`2qZuVXxw|y=JDGYn^=`%m6dktu3UV@(&ww6JHWwx=9Rk=O3k9;9Ue1>>Yt#|pY6RXadDaK zB{t zEUPOM<`SI!m=)DX*4^DF6jJzz-4Vn6jgjc`yO?8AL~|l<7R-HEN2k6bRahoQh8`8mwS1khskmqH?lK-rMn- zSP-AfiK`kAi&<88VK9dglhSaTaY@pk%^-jn)KDus6&H<6J07%%OJ_wT?Ey?^byxda zTav)k-i<~mAP~$%Cy+pEIDtx$7i`NTDI}fB3=LmSQsSdF+Z&{4E69*vxv;F1@bp4R z%zDcZg#lqN&5^BFTYl8t3|v5rgB9&GJ7Bm+aiRYsBDP}AOH2|8+dMa*kcXAtx6%i6 z$-3MoR|5}gEROP~$;pinkU$zwfYx>tgCv$Bl1b~5#w1Z663t0v@*fq%1GkV>nh3k+ z?*t~Ij4+XBq9Eqb6hEWKQ#i@f#E|L4iqL58iDe!jUUXF66A}Lpmk=ss@&n^Xhs*_v zO>hP(E@DH86tqln4p<69XfIb$waTLW{@Xa z6PiZ+;XV>hBG1H!Ut6Lv$X+BwN$hX#s>(OcXG|(cliLV0=ZMg=`8cX&BuuvnpyPWm z{q0xfBXz52|L^bVk+W^aZr;iDO6^3p;VhOQmjo2CVT3#!s40`;5|ayOkXhc0q-jrf z+)m8Sn72EMIKcr%1qBQY5C~8e!;UDTj=-omgSdf&yVlfHh0@OT4BJ&fEimB)gc=ff z;UdHeDB_E0jUbYUC}YJ75+EQ+p-mW}D&lwj(MNskrbHxJ0|kTx4+{wh94J}lxYZJ; zHN+ZO<3$97jSdJ295_M6ki;RGr(MTubma>?U`WuBp<;pz6j>z+1R;z`OFW~5jSv=a zSb@chK!$B}cDib!jQ_~2gTxLjB%p(gFoigemzU!>^A9L+kg>tTgN2AJ(X2VWBAw0L zul}vC&m?I9Cyf|r#~ie%j9Evk-Q6OTxJv?DJfUS8=c@#EyQ{wIZL_vVY4m!cs$=w( zl3PZk*XbzrViaW+oZ?(32hdN6YDA&fK74Fs_^Gm_*Jmop8fF=;c2B07tFF60ZtTHZ zc63LH&uHDhztU*G_WI1zyn7GNdtYT!I_R6DE{&=pyR6k(W*EN{!58c8oe>=u3WdWV zFv#ds`1@zqWqCJc@V!}0_e+x(GIaT~Wm|tM>+DHnl38ytBrC4Xd%v=2w%a6K-j%7C z8LR(Aey_Cu-`yMjsofM&e`VP!U6B?~k!+f*qK@->-oM)aCi$~sqavfCA9!UGC0L=coNJ`#;0j9`KW7%+j15lX6Fy{#LeEfUg6H`8<+)zeKo@6~&Tm=Y*(;bRXl`N2my>Bq$#hMr-C}3dVK)}#|fYA*L77#R8(}kE>%q1&ceXI2+ay?Z%{yU^?Dq0Yx z*&Q!DIxS5+qo&#I)fjsnOhcGt6tUZ-At)6=T3>A7dA3fT#5Ed zw!434Z1r_rWwS4ezCKiJ=%|S?bj{tlWoZ;Z@d6k$VB^FXTT+Ma>TZ#3_FGqZoH3-3 zQ_6c-%$d2hMPC^@CMIn8c1i7W@)f-%Nn3G;ouOTHm>LL`t=5mTOj{-`J(YP8T3-iIN1{ zSw(cK-T$U3%G1B~PBYq)7nl(yB24iDj;gyk(e}kFbuIl@PxnTPM+7hy6a)f6AkffA z;DKir=B-;JZ|gXs9#H~H7IC8s7SU-K%U45m{1>`?RWwuDv}c#cuu&pJ3=s3ji54V- zmc)N|S9FW7vab87FyTP~Az+Ze(Ba`?4<+frp0=y%vwrF%k`^cy7zqV}0b!wI#Smh5 zC&{iNRO#~OStC?+B?=@kkWt}c1{xvcp*f?q)m>k+bZLHRj}mCHB87+$anC7eOSw%} z)hqJkO>}!ctAs)Sp65x^QA=rM22vx!^L-L8vQ^SLiiD;PhF&O7)lT7>KrbbDG*9fL{7V{bEjd@12^%9hCc2OefGOv>+yLVO)vQjBx zoLMeYACVrA%Vx}*5i%k~%Pyb=iLZT{cr~c2`XlbK1^wl(`RD*p?>? z-mKNVwz}gae~Z<%%^3y=AUL4of*cT{Gqy?d$s}1Mlk1v-kRij26e)oeBKi|1`Ku+b z)#RXr5QYwL;>H|kg3f7*HMF;^)?1BdNYfVLT5&}Ra8m*tHR2voNg*3XF^uJOs`Z#C z!Qw(kh$MJW(Wi?bmZ1#yE9G3)_|i`}c+ilb(4c_9VI`hJSgiNl?{umZal*y~1%!tW z4;U0ENu&~_vYZC*ga@aD2gZ~KRz?PmCdDg_Fcl)3~9_Y>)CQ zFIEfe9=OTa`mb2Tl|4wqXPd;Y*j?x3KVV7&=uSj8tZ-FR$lGaM;Nykjbmt5pHy%>; zlaY|4U-?7lG{Xd{yX+g|fQUoBN+9q^wj?LrUBt};fPSrmdNFcoM^6j5J9Vs(I9F!!2$^7y? zRK7*9u_|8>yVab9)5$&yoY%^R^1hMSH;Z5E4%q3h4)TW*NKpymVNnqhn=ChAML)a2 zmMbq-h*ijOI!r643r*FCJM(64ufYz9R(tj-j2-ck2)Ow~hCZXKDe1-0mKAPPACidF z$VK2Wq@p7~!A0uAcCumFg%Ma_qTpRQ8} zr;H!SP!1Pl=;2*R49p(J;eL1sF)RgN6xI#+NAeIy1jF_L>TdPz8g;FBOG-kaYZ3ixl#4VgQTDt zVOb?MifpUXb6=+q8_sMB@A~$2tt-5s%H^Cjx_0&IX;lY7#3)MEYsj~aMbn?`q%L`r z+a2=l`8AqpOX!`oFP z9p8M4fHiC0+4^VJWAhVz=?syM#fUDQD6w8wuGs0J%jr`&YHfdapE(pK-c3)|MWY1p zQE5;>!6PU`!P+9nPevJq8?h)=9VD5eW!1DoOE0WUf*W?Tmb(fZwaPER9iF877?~K8 zVCy90$UmPGUnE&W`Wye!--d8PGnR!S3Ndt6N2W4V#g7nMhJY6dGmhJz0IO z{9ff#x3R?PpqQw4$&n)FB+}wXXkLX^wCHc6#F3Jj&a|#_B+PM6n$^hn8Xr0Z=;r+dbR=1(IsbW zp;wn-Gz@wC2?xYXAz}reS(Dil2~6DftIIwgSmw2$R0PmilMviz=Kb&6U?o8gdD?D$ zu>@{O{S2|=7)X^+7@s8hE_k9ZV?zDPSk9zF2BBH%6H)^F!zkRlK%NlbwQ|-QD(syCBiT#$-dizv+O2)wJxEhW^L zD3dG+Cb(oA(P27l5n+{U^&tWo?_6(X~k(QN(cp$U`*Fphc zQT5-sq6x1K{~6{@NugPJBd%Na=$#{8MG-K7=TL(cvU?!gV(6Mr)@v zkxR8#Nr?h2{avqL`b8lBTY&hx$y6kv!sTW<d!QP(y&P$_$t$dI_?>0b zDe#sO74=`g<;8XbB7FKnj9VvaqG+!F*(W(3Vtifyxi^5L%t*D~M1pOj2^#`als6!? z*87NFRzHi-!x50S+N z`lF2}60&ouGrt`QXCQ1|OGNUEWalBT#2^P3I=%HV^E_vnK;0aZd+ghor5W0I_GgaV zn1%}X#a_hPU+4*FnT!7%)&j4wPY1WR{uv_^hBgP>!${!65_#!V)q@DNSlVtGlW~L- z#nf_Ny+wK5=uW2t#h{{dZ(_S{<*dX%3|$)w^6$$YPYh6su$Itb%Y z5XQa8>cu%Lk(-Lmr_+W!`qK5eeX36>-8mbnrK2=y&83MVPLW(T!+;PIr{(81%hOk2 zJQwhIla9-Dwvl;ek<=V;!yJ-FOJrFniY>@4w!}5af|TLLsF;wA_J=9BV4+46$jzm3y&gkdyqYSzp%@ZHV(&6btxgDG(UAV2_sDr|?KL&w zFzN?$R!O?%x<3^J?8W};QgF3|WYENnFcgnm{6POy9O^f`kQTi##jFX%HYCBd3S@~_ z6B@@osRU7-8*qS_1c+UDDtim!EL@{SW^vo5J}+fFr0QgH?u5bkw_-t9$yQ$%W2{Bv zcsp2*S7yT!u?Ut;igFh?V>P)Tk>fab5+l<6Ovoi*qyr|2X8Aio5)cflbJW#baNxQW zL&Sn`UNzu<%?nmg5uZ#GCnOnsNi$M)VghC) zC~nFHrB&%vCf2o{naqF#lE|4mbm;sHE5(&`!*{+ zCuYucv=Jm0-dA31!5&wQ@oc36@LYN!A{#I~+-gP-KcCT-Jc)2SO3AvTh$-73#!sCJ z4zg?3?k(meiVodbxX!a?R>?{Buj(ji)TOoNj=AnB&H2|DFb~y)bE1vgz8m6g4uwOw z)P&c2Wu)#Qlu|!lq&bjJ{-85rW=-ifk2;T%a7GCwY7Pu#zZ~xoFrpoiWkXu6fpff= zMP-D{TO#0Fn}G{W+L*45eeqZb5IEY2SIC6COz@z49{L;fz4Sc|RFz!|)kKh`OA8Q>B7oyq7F!f$SFG+> zm{ie_!2@nG!BwBz@upCvX9JepUEFB$bwbJ5fSn3XLa)rUR$D3+vFtHbn)PHslnXea zBysEpJXZ+gk&FyUEL_OO4w_3Bdh=T1{Q0y=p+uGg5BbORs|^<1@(rvP`lx7bVTIBk zQ|+{sKpD6870UlaPi&D8ZtdyswE*diY=UXTZI3+7xo{`pznld7alMDJ8bYi0z*QYo zD#zSM{4j`JA#$U^n@g_(u%i};LrvFcrz4}O!B-6?e3+DrC4H!ILCRR4st_`&ny&e! zf|ROSc5?H+<)$<-8Lf7O#CUc+qrCjxZVThDXo>SUN=>?H)Kp!k=TsFm*b1V*JDnTf z&;HQ_6V4(L8K|u%Me5mUzj%t+DQ19Y0+3S&{Ym{@26k@fOp3p|ZGv+mMxvKeb4(~g zV_K<8G_BWUe}Z0Fj`ww$B4U)*_li7nMK>hspBTc!Z#(#a(0cR4DX9%(C0>s6IUljN zyP-EBQ%YY;iPdcsUUsSy0!P0hwkrz;(4s0u13ewnF{lg564LtaU405to~QaLw4eCD zr$TfujIpg4WObG?DVEkT;}Zo}RT;&hF7NGIF&$yi!n2Wk?JCD0i-grlAe??KWos(& z4CYpt;ZBsEi?a~I108$Y-y+diF%w=ELRO8#8A;?*>NSj!lKk8LU4~P)5PuE)MFQ7s ziVGu^;~07{LK^$vdVU+5OU}MEBCePG;HYN7jTyJ&fUU(O>T$J~5-O)-lxKvny>Qbe zG|3!bl-$`cH^cF%873{@h5CGKK^`aS@X;nPPtihoOcW#UnQUlc_5QcUtQs1SB$Dtk z4k1oEuBbR_Z$049iTSE?3K3COS0tcMEJ!oD+!ipTqxGHbeU;NJ^@i|8ryxxvi9#QK zRi7f`o#G_7kd{U~l1(!vl4zu2!^dnyw|RbXZ#xiBA|WO<;PwK34Ls1BTO3O}tuh~t zZ&K6IRKjXKM)gu&Naxo9(S7R^ehPM-1Uf(lswY(zLRohyr)>+hP3T*vFjlQ?JwS;miI6m^$Yxlo=gCn->7mL+)>TV+@CUX(%X@jF~vL?;pThsC-aDOk@x z9~u)@A&K2jYUAjqMv&`;u)SJGsjbON9@?$YkH=dg`@0f?`?ub0Vx$;oR^81#UX(dy zC40DuU5j2P`2v6LSBO8M=ee|JctYe;wTHGGV-mV)UsLabZH-G`Ssg=mqXr3*d=sU)G3`y2bT+S^2g^TQeG#9HJ5_IU9#k zAT=nceI`@RmS<00h5HbQod@wT2lUr!~aps@lQ-MEMW74lg`@L-a4bo^(omfmdm18{I%FO zyrn{EsIjs{enBOK{|E}ZO}zf^r{P;KXrP7!%RTp#+QV5Q#$IR+K<7)i4cJn$Xg6=R8fNe{F^&H ztq9@ry%7$V!C6-HhJz0*H<4BQ?l0ninw$3GG*&ZGYnJP~&Ou)s@qj z*OcOmGZe&2u;KT-lG6A}_LnPRJ3p<>E%(TGG*yCr?<4;$X_s<-0Z-pswwJf{#nF`h?N_ye)3E!CCU|HFa%@> z1#=0jR{CcXBdgHPlxW9T(QU2O#j_A!+ve71DlD>S>zs%LfV;_Bo%(Kjr;G5)WZ>Zb zrWq&-ZRCmB9a-_q5-G$#!bYqOkT6j;ktHE7sk(^jDUyaMad}G%+zU!@_QqB505a8 zB)-qH!_I`ih0$I#Jj!M*t84ss#HJ>M_D#^g;Te3Cdd z+a}Dv}UtPVUF->wxVxcdRRoXDYax!s9jx<#WSQOa0sb zjGUEP|7tfUGdXzU2wCj*)^N@^)2w)(=e>1fy27R_nXJ{h0ByPyWD1K;1oD)d$F+ zO$v~-6^PrNdmnpZpg=^%IAXp(5b+L6R#Mx=EF{$`@DE)>e1nuYvVe$c|S zlq+#ph?GU9#OqLwCuob=9Wum-Q#4i8%dOzD#>kYL&MhKl?>q|Uv>|9215;+N0i8B7 zopvfX(nSpc^N?)4}H;Yux2|IlHW#DX4;#%FfBMETg`m1tsK(|UitvHTn zn}k%k@zGZ(X4gd}`}@!wptvLpxHr9WS}kSMRgB0-9{;0-i`(1h!;F%*2-HYVbMU>@ zK>RC`AP1|{L?Bm<*@-5-OTQK>jMyS-6VmZZ#=K4nV|@yILPbuRLQ( zkuP57uAqIsLrvo!%6v~KhK>KQP)qzFjCnp6Q6F_kXYwYl%up*qw=aaPPU=`eb1j(1 zo|Jj4zFdqj0YMW&???K@4ce`f?A&JXm2n_-K)6q2iN8LIQtZp_?=zlbXr89YsiFTo zp}$<>*R@Yw_Q*jLD0(&*31F+Qyft%nOPOPQs035(4mGEfr6yyY=Oiegq=BAU84y=* zCOT%C@G-GhHkmVYpqhxi$9VFZTxYm*xxg4 zKr&V1O52W_R9O^Mpk?P~&%d_>_S1NUM-m_Z@ne%7A}Lf7ffMNjPd4HI;O4|&(bs*i z&lW1Ug#Gn_6Nr)5Jqt>-YXg^Jga)K(W6vG}P>ntxtmgK9C`B9wO_46% zmwBX_EXmfXw&u~=nX?n&bW7QA;g6&MqGyNw#5}ZqT}ZppSIn1_yEC1zi983YEVY|Q~pWcZ{kx3K@+#|yP z&eR&PP}}`bhA-K_^Ang=IAn%Bgl`q`2Zgg#J^Q4`w)8WBRY!of-cjuPu=BLg!K$e78EUi0iH^M$BBXY$qk zNU&7E?xioSlEAE_$@0Vyr)4iibhxty(;TIO2&DQ_hL1t(N; zkIAfvQ%lAtH(PYNFx`_KU~mH%MeY+deK{1=M^BSf@OAZZDFTe3Zp)r)j8$$Wtl*U< z!*GYsA;^NeJg`VKsB}5{s(A)}V8RLP?Ct}Za?SP6E|&gs$Jt0|Ib|7CbduS}&Zcg5 z8=xEQE75VVKlw9gvt?62J z&+?J7zd{bZoPoN(YoMm}dph1@1L|3`tghX3l@hB;B$&!&^~{NxIQ{w~tgN?OtAw@v z2F`KzJ-;7#q@#JD?*{HIUX(AAWf_P@i+W<2R__+VY807Bo`nNT;71AQTcD?0ah*#b z3c!ZKOsK!UrFRV*Fi4v}RN5FQaFcD<>4xPq&c!injN-{aWHQCdUKUrBIZ>}c4C;iN zbv75Uki+ErL1m3`_8d@11F#dx`7kDzUEr8PT}Tv{g#G!PoJum4?1K0OVNZHTT_&b2 zQzULOxVJpI?$;L(-{>Zzcy$%6(nsi&%wOyIM|&!@{J2YktMwnX`=S3DB4Sy41^+qa z5QCNm%+S3~t@r;UTNPo7o+ISHEibePwuCF>>K3klm2u1vw$Qc}v~~OIoxTrKCy`x~ zT#l#!$q4?FKN_ngI(iAku9-If6XUB$t_{0PFNfBFRL7%8R&@KPEuBl?V^F}iz40n5 z<<#6AHasGmN*vR*w1O$~^*Tu^FKm??o0C>A_#_`gw4%@qpSc6v1wNz^Y=_Iy6jg}I zm)h&U0E)_mJ*f#VO&c|pG5AJjn5)oCP^RuLTwF~T)1wJSRSP9#Z-taEj88hUI1>c+ab1oHu;}}IPex^!8%dPYw+1CESCMF1Ex>>RSvG~6 zbpfEFxZZIbDn-f+3g4-$L=gZ~dFTn+W?Tr$Tbn;n*XK%-J{o&NBa!N{rB7@?v2lc+PB~!t&3x-f?9h`$hd3azOXd0Y%YKr+ z@gFfS0#9t4`kAAf-b@yM0cVyHeBBF1EPWV7s%!x`P@7hs`2s#`Msl_hk1JRTABVXH*CJ#raLZ~hfsK4Z1 z12Zz2d|cQrlSpTxB2D(+{}LP{`n8BzG6X-Vrz}F}N#-f!lVVBQ0xlLje^~C=N~BfB zz+|#}*hBzAf9|l=aM`#NX~Ii)`V7DlJ}etht;@b-mD(KHFDQS~k{xC^AqbHy14k5n z>~_y7EXlKS?$RLmovYwC1YWqMx=V!%rMBt;d}Ruu$;w}LS<)El(C-p4vCGhR;bzce zFtCozT0(Y68xgk~O+DL4&&`1i*iM`sF(|w0X%>P-Ee=b^3QWI9%#qFfV*^kimD7x} ztLv;vSF*a=r`e@NrA)e5`=JBem-1iJ5aQQ-x5`sdQi5uf(>0KB08LDFYSu@*kY6gG z+d7ZUF`*=p`MpoWOVB~(M8}|oAfj)tb@#LgTi18iy?T2PTw<5s_FAC`|FEx0*-g|5 zV4@AU?~r~Qt`V$=g-W8gbi6s-n+d>tmMMQqyv^{QauK{pGIpbDshZz*$RgYEkeEb} z8kDsptU0oKU)FP_Bx*DA++f;9?nuO*tTa4VAq)3>MgUa8xRnRj5h8o4J-kw+5dW03 z4apcOQsB%khB2a=qM8@Mh6+}W*TBtO5a~9P-s6*)!XJ(O$$Ty1uZ3SY%3r%kwkHwB zZ3YqbEnfK)#UIc-59CS~ds88jgO!MvONu_T31 zAp)Zo1tp{0ZidRJ&9g+6I73Lo2KPq z3t7<~XSKwr`;RbJF;bf67rE-cz>OT(q5ml&bejbcCB_NJGi=G}g zC!@XzL`_6Z;qMs236``nqIWq;`?urVDbE*7Z#Lkh6fpC8l|7S$h^PtX*uvFJ1~b=H zDf8%yWlXH+oH)kzX$6kOZtI1GR?I`dyqkPe(y7J$jNjt;lXmTOg9L@mA+DNNsIJ2J zh*IrH^{z}3@f19qpyywsi$e>`@N7T$RWR};n`h1fG#N_ytx{xY$h9fVGh@(dITn0_ zW*;0aISPVvT0ZW7qgX|WT$f5Q$1xIsDHTP=Jfo6WC_cWr6htHOJJ?>as!g7&u+7&& zF~LyV{S+?8*R8U;_0wWa6nQ}$=!(c}n#Lp|avVETAaA$?B$IXsHchjb91!v0cVW8m z69nS;f`l^;U?+96%qKJ~L&}-9^DXAJLO;bsic*1W2`9U%<|5G>(2T~+{IL4f!ab+O z%Z5;3)@;$tj=z zQRVDXkK0tjCYY@r;BsbYM)hU(0yg$?9p<&BsB>i@;C<2(1aLqyk$ zER={@R%wtKArsz@6xzj+Z=OCT9emO?n(UN!#(a=O;54mflHBg~CZZIre&2wL8Ua?H zD9~Q;X6My z$D~2Xx>L0&Il!d}b|uX=G8D@Gt6WJ*x4PH>o7j0FTlHshPEjm4vZW3f+@aUEitg)1Ps?nsk$sXsLBPMA(la!oU}0X-eSk~e_NQDwzwf4dlzvvmuolCSa= z`L0AMP=YxU{Omc(NE&SpUZR{ezMv&^Sd0erMl25S>%31<7k2@z_c$A>9q8sm)p(TnRs5t@A4sGPPvVzhxYpemfBI`Mq zh#Kgli~pnt3MbXclB@vTig|1#Z(Me4N-PSbp83iSW@+|nEw;fXU%!!Y0zj^)fv$fc zun7_PY7<0HQl5eiE5~P(gcxzif{Ol*knK0IWpof~=EDfWoMs|qg$Yhrf!JRCKxpo# zxOWqPPUdRO1>#a-xLO1W(1$D{R*F#NUaYeG_=0{-f0Ake>ynB;ruG(P8kRzcl0O)t zF(!eZbW8UaCy2sleeUubCXg)4IlZ_)19zzlfGyDkTjc41C8UuCb^%PU*?a5?%vW<2 zl65iD^)PVnrBWG;OD9^enjvwylyX!@9)tb|=M}jOr}wVR*MG$tTMrg5Lz^?T8r@x8 z>rp)Z8CuDLpC}C1$qvh?d5R6j!{Q?wDf`5`+V@4nbnj@1ktOOKYvikMj;e*@E8K~& zt>>6`wO>K|!Htxsyto4=esnec2^EMQQ1uIKjSK)VlWSWfF6tj`g?BE8n5=A4$|&2q z)rlU;=q5_F!z2RKXAxXi5O=N5zRR>QrZ_7Z2Y0GfpEOjqk))Ml#rpe@y^J)9kqInP zFB86qNAtqjNH%V@a}ZGF3wpXFx_$y4y7``mDyWEN^1+q@KNIOuh~#&sHc>T;MPy7Z zNSXAh(7(AZVyp)|+3(=|t~wdJ@KftMod}9A~n5RxD*zlt`h`*cS(s* zAf+S^vyfbzDCV^q0rB32UCaVUeyEg8Uan?(8$*F5&)aM_pQH-`R^}*=!yHc%hNeqn zoLWB!hqhZ9m$4`OZESp3Nne>w#x`!O01gl}d`9;9&^*78`2=zLU&AVqa zK4?h%o(U1X)Ezsz6q{ii2RP9o|W|yhMayM2M)` zE#8PyNVK{wn7BEyVN#tq>D}SH0V@xwB9yfgo~enM9EG33r>4V2(NdXC$gk_Ieu}i( z1VmzX=PW?b+1%wf@Yt2Z^q?^pmqFhPDGdmdQC$LNj1Fc!SneAlyn3%CH6=Vcz)75& zm12yeu%yDUA;syc%Z;BsS{cUr1rDm7AL?jU(l1KSuMRJ$yfoNS>ts0lwTet@BJz$~ zuJ0!?tv><~{M?+(5=vjGcg{fB#zbUTN}42oqDxR24=5TWsj>wyARTK&1dfUJ&Q{== zf|cFM+Q~u_C%2=orIHyK?!*#BiiwD^a%`Bt?D*>O_1TQeA}Re8Y~SK+weG^wrgc_& zqT6I6k8(|rW*amm=vMXZHe#+31priPOCzsJG*hpAgu+_FsSBt<4lA_-DhouyDVjE5 zN(|0f_}}#)eCDxA0!mx=%&2r^%+`7(dxjQKWY+oDUstG-mOZ)NXY#}oJSKb5=5sfX zVtum9mb{6j^FCkSc21!2d?d!lVpHX0Sj&u*M(RX`@sLC?r6fuJOyD*+Devwdv2sH8 z#dG~`sxpYvU9-|ye5Z()sY&ser6=jJG{?uzgMMVuL|iy~CiNOTvw;1`<5N@!;_v3b5oZ3R23r7Iq>VFVbVKDbMb&vWP>1wAw~b5*D4z!!m9IG(U}>v z4t?kt!b?5ijSZx|gWe3qJyax=2GaOsd1}D4LWnb>;4Rk@zGWXcT%&v~?3%G3*l{v7 z`=2Id_vhtSIJITPyZsF|TcB@F2>kWPQb!`_d{&QoqN5#~Tqs@7fJ8ApZ0DcU6w+il z6r6ifciSE7zx+ZXa#f$!slK4C%occQ1KtKU8=)OX`+xZvqzL>yf$*0>XEW#}I5R)_ zN$Rr9z}NzP526W6)@1<;?AWpcIyDyL^J0j*DqLFTcXJ?gLp=z(Y4}{UC*!X|4rQh5 zo>xerC!N|b?*!qhHW{1mCS}L2(_o80Rix}(#zwnT(G3ynkU6m;A{yhzWUlArarpOL%3a-fL}v=N z)hGo!TBdP-MWt`GBz9|Nfq;NPbUZ_62JjK0^40?%D0J)NlyK`OgO_+L-6&S3ovgsj8SDKc0Eh;z+$oA>V3t=1+hiqSQ4~Zg zWb;&lz=e~`aiFg!^{HubHd(g%qZDj0X=httC30sNM#qKT2<_ZNZIWgRe$MUrlN%s= zBD)*u0xvK06!@md=C-5)5^}qhZuS^L;)zzFDvc|jb-eOCY;Yk-39SP8P=$sF_vf`Q zN}l3;j=OI6zT{#ynGOF|)SBgA@H?cO>!%mz{Z+J+2q*(1aPk@l9B$5_Gki3I)=GNq(*{6O;A zlGa1;sbL;Cr<|~vnVcQ#=R%2xP44XS>Qm&QB;}Yg82zBgAsx_$@OzRpsqO^E3kvb@p;)5A3CtQv zzR@3_9$bJm{}k9)=oERC6mWC^y2QL+0RWGb<%MuKw?5snOB0+8qe`6MhF>T7kdJU! zaGkh4*r|(C!XDZE#6dg@4y7rP#yKIpEJ{#}BR10}h{5Q>zl8{yXr}N3>pn&c*uz-_ za(2l>q4lh9{hRUhM{8{L5UYjrC@jXdnvY@&xHq9m8sM-7pfU40RXl#X_3zWbhit$?Hpf$L!*oOex=%u{pp~ z^}P`zpzI?dV1x@*A|W(5lJtRW4VD~*L))T%o%Fx7AB|3um7Pz6s`A?@Qy@uY7fRsU z2i{pZrWH_e?^e2Me5gnwfHLyXsc$#->j%B+s|Wb94>U1P&%^kh%h|5|>bzv3t$UE= z;VADRd5H28Coc#vh+oT-it!j>ZCKB3OZg6$5du%b(d%6ZnSNOJgzgVXi$5RgX-_fb5_T15jD zCaW`)cy+Hdp>SdM4J;!duR?Jb312Eoruke{y-q-f(Z^`h-RAkFv1vCgDEoQYn_Hn~ z2~{YIdz~Cjr`_{Xd-fMH*$7j#Qt+A_C}p1XCozJcK6hVrdV!vYXI$oB@1~`0dyl(Zjp<%Q zn`cKlE4=KTe%$0sXi^iH53{UNBumHa9;v~YJprve)y>Bi%qXe^koysv&lB)P zE9B{^-bVFI7oxNy623j$+-f($H#423ir9Qkw*giokX5ZgqFy}$0-Y@J*z{Px1+Kxo zkGr$nh!xT84gy(Hs2pM>PlV{IH{8JiHww*Ry~7eNaX*NB(PxEa0IQO~V6sES3aXZ? z3MFW2_d7RJ3J5kOY{}4@Ri|A=y?dTCN=elxY%&}z=kBX)>d~l{j(%2ONS4!QSyZph z>1q|XxUJ#z3RyW(zb-F_VyLTExlBM5A)Si;WdEq|h$diGrBw9UhNwxyCbhtoN|xbu z&J(;?fGaEOk%~390j3>U;(1c-uULKPX5|Er;We9E!tulBs zxk=VofZ|i7(F!TzU6}SHi-6*da#6cNkx4EIY8QqHt)~?&YNXyaUoL4OT||{5hmUJY zg%G9Uz}hi}22fe5P|m`X58xZ{`kIA^0g)e2=yv3zQYop}JRD+VOOZK(1vP{?t1DtA`7Zwz zm~c8U6d)Y{_RBlC-orO9$djQX%RLwB?u_btr=5U1KWt=e$5Uz`fvcq*Z@=h z03&$-4|aeDAAkde00G=(v`c*~Uq4r zPAF$-5kc8Ee!suFQZzELcu#aiCfp*TwXvKgs%fG|dh0M;tA^>gcPkxhEbF%-p|yM? z`M%B7BL!cq#85=O6GeO*!u>{U=0#^Ve=<3c`vQC)AUD*4D1Rl~`LP4@!fE%xl@SW* zaT2Qndy#Q5fZf*l(FR#@7G2Ajw;$s$n>#T-rn%k}p zXxN`?7T%%H?uqx)ay7`^@o=^EZeUnvLV^@!f;gn4<>FG+mzS!oYzqi<>h$Ig?1yLl zLQChpt59oV90|}uw=EIGxxJZ6N?7eon*}btEu}!`WX5xqJ+z90(8-!IE`%i<3)*-y zLE+6TiuH-{Po@Y~nsSYmPEcr5^(k~E7- zk!xZC(uXy}I=L`!(njlzsQ;$NioL? zmdcpFsz1slACpv~qUo0@#jmv68GDt{5 zTv)Ww%65Y6avXQ)!*Y}8#v%%ZB!oh8Az@m;_?=*Jnvf`a$lBAB=?5+q2IUHc>x_xe z47<24zJRj&3qEE+WAzcSqeXEn*M%G?Sh0c!h%#YC5a^FtxglUfZWT8yy4MAWO9{DT zSl8wY^-$HzXRX~S?E;3T3a=t5CGQ(cb3|}tznO_@A?U?Yoif$+Um#h61rosc*Jz0O zkfmNsNJ*RO>ZPJgy(nx(NSLO^19_%7Z{eH2k&}{UjGLt=mqpWL`E=e4*Q>>b$$trm#bR<>6_}OGfq36wcUlAz@A9hdzV~N=KiMMasfdg{U8>kR?ta4&*p9 zP97Kb86$Eogi1+ZVzSVuyMq<~4pgEgPLc=Y7cXfJ_wY)FLiFV`1+cGj?GY@uSiR!n zbL)}h_)AAcXD0!>JD!B62BfLJ2u_d~q!J5!5d^w;UTVB^owN z>tW~OPPUn{ThGCcMm3Zj7^`1hdbHC^0wz^GY_@9N;$Wc{d$zXhtmBdCl zSaTM{zLPzSfc1C+grouIoCkx*g2IK$935kcvLsid7rHt;o5o1>5%^F#B%JILTr@OjupXe0bBhWfir5R9JN@|Dj5a`qiVIl5 z2qA=MM`ODPjj}dXa=ps9wzO-MB`uKzQJUs&$$Rw!-bluPNO|gH7<^At6v=;In zgAR}tEFBF1bQ%DNXcd&|*2Shh;iAKj1h_pD6I!>i9t*H{owT*?X{!>;m(AHGsM=vS z!X-sXk!SO*u|pdc$c6Z^62oK&=CAp*CfAA*ENI}7L1qNFrmr<_N1|lO-;qbQRZIF5 z#O1!eh^t{&N6v_fkvh9OmD;n4BuOQhiyf6!kJIs2KDHikL1E%T1_&6@N~}hOetoiK zSu%@%%zE?Y71jF}dSy}d$4|aD@ffokt*41NP{9U>AVNISXccfwp%O@xh?YZ93KK(E zH6$>v3ia~pimrDhyndRi-^7Su?#|zS|U*kU@1ULfTBUChrC&CId?OJXkDZ46=|C* zB*}aA7S}RFRdM!3NQ$;Iu{{yz(ApG1PC2Bpgf+#eTIQf(BSeNc5Q z2TBB58KFJZc{c`8MHTwnX`fWd9Ej*;VI6i!NyR+kO%Pz>g$a1p1&|ha;evw11_Xr% z3=9-3Vd76Pq>!9T1vPV$7&W$><;FCzwV9py|7v|v=j^z_B{WHe2`+7t?vp4$*%QRe zmwdr9MNAhaWkDq*lCpy+AkN!))laI)Qj@QK^@&?@l2VrB?%t5KFIrNqx-{y2L*Db} z_v|a{N*8pg69h~=MC(W2E>ZVMHt7_MFY=PtSM+~hY0;}%dg<0X@3&bw%k@jzEmg{B5mb{+kyy**52_^5BZ&*DebQuDCZS|XAR|<@<1E?P zGX6$v5s<&fQy+X)v0sI5ANbif$hJkb#cFQrt7A0yToMD|P!EKg%+a0i3g)Pz{@<&> zuS}x*sF7XsCCHm}%4SHLd}pHhGKyn13bYkz`NbXKmLiuDiG;;2Ex{!UC{ZB>jv9DL z0*oF&gIpewWRf9cnsE+AZIZiC$CFuhtK>f7?<%5^IEaVb_JtxWgTyQ`5amOjJWE5` z6mZa&j(DRDIAcYsW2L%#q_&s!)Gv$MHQwzWsM`;q+f9)DvAO9|3i}GE?XTP;CuCnM zk%9h^E=1W>`rnthnBrsGR=$w({R?$}v(0b$v868XDXaQ9iyO1T33$J0Wa1jOy*2n< zUeLqBRZM*DnHTil1V84tb-u6Erjt3$ae9?n^ej~Rw-vOhfo+OrzDS{(i=$0j%19w` zV{M2wGT^CmXU?4&Fg|2l*rCB=Vj{kD$wH-FD6xfVKlweJ)1JNl$2T~)#X5i5)V`H8t1 z`?EK1%iEN(A^nXhYsVO^;zd?A!H3M1IBZCui9;p{2sUt&gcGdXsIR)bchx^@`Cg6M zvQ_12%F@4X#HBeeZu4haop;7M_?;C9IQfGnkBOfX6cHU193B=33XBVj3XF&gA3S8? zLnR6nAlWr;yK#C6ET50ib3|Cx{5t1;_v_wkYjXp+n!u*JR;VK`BQiqL&YWOO*-^qH zMude01VxVu3h~0?Z;7-dxh7j-RhLIb^nV(99OPpmFRa?BUhP?0Q&ulYP?qjzN!-Qr zDUO_Mw`e#)!f%s&fg(j5CgSR-hB|50Iu(-&nV5)8L}sVtVp4P0=Wb5(IfSlN z^!F?~M!J`gFie1fb0*9b5l|qE`cg=gN}E;+$Rs-OM*)2TsR3I9WI|)&AyZ)yaH-H; zkTZZM(UT-Z{%%FoWeLfS7*JS{!J$KeqX4j=Ks-niAQU((EZ|cigvB7%9!2GS4XJjK zW9?6gCC8xU7C}T9_pF4}yNI>8QMU-Joxc_6+!5~6Wiz!E$gf7(2);H!naR&iYG1#v{bHoY0j$L7 zc}~A(XkHNRP}j1BeEq6icT$|M*Q33}qjFLZ?fGc*$*Jip_txPfYd_omvX;ytmzlk74SbDiyrj^?kwX>QwUSma%Eq@t262^C8M4W7iWL-<$a;&#+Q zByq9|7Br*vC5-p{qDJUc%Hk(3paMky_<4+J1nWFb@cxAIKKb*^7WIT__bp)fp2zv*Ui%pB zYDU3S+= z)wBLiG^%Kg%K%<`A?wBoRzBxgfp~Ef7ny!Dv)n6hfjzvAD=0|9>nKp=@v7+)ru35M z?Cr%;_ZktE8*rqhQfQ<*DLIo4RX%3m3U=p^W80F9qeHz zcGp(a3N7E!SPU4lX^f9!#~H9?EZPk~R2P7= z1vh=YStDXfd%!D?d2Z>TAz+t{z-kXvo3c`-fsEQ&O!Tcv=_WR@br+AG_=r#FTgM+- z^#f*lc1S@ml`ubkB?Lh{xX5#qTfd1t_W z4BB;bxTpDL{x`R9Jm;=bz5iNI_Kafp_M>GtUkRNtcS^f+Q*L2+>KZt4A@^nM6#AIr zcp7Bb>5*!W3rkS(@QPGfkft9j zt)YBZB+oh{bMqw=5Rt?(chnPF(!`g*XOZXKElZneCVH;!(uy|)g4(UW6vy1MJvXy z`E25b$Vrq!Br(BmHxi13YdRsrR#(q{X}{Z^rIkDEtY<-Qj^EEooDB=G&t)pgX%%E7 zFLd#0KdlGnICI>T-i z2e$4OO45hVTQn}7>=iHnAh+#eYviLNR(iPiPjF#Ut9%A*J2EZ`qJ>TksUiYbuW%oy zrZ-*K#jRJvz0>5E2R^~p$u0gO+<>I-&YPA}-|yaImu$6tN->|&SXqzg)5G2#N#t`- zeh;)q23BnY3qi=kC>BwZA{RY1OgF8*uwJW;R&e47?u0$l^xF)`*TKj_oj=8d_Q;@a zl%H9SJV4&J{FK>xW>n@0+-sdW!mmp*2mO=2Lm>MW4Q(hFlnbNjmR2$;rM zME)3z+=T>^;$M$~wqlekEg+S55pTG$x6GqBrV5&i!nc_qQz~rAj6-5TZ|y-u?Pb=? z`i9O_{Yh?XROxprRG&em)n8wU%REp1Kj?Vg zP(8>s43D0)a(Y;k1XO%%+pe;Zg!FQr$)x1Dh6vU~dX&y6`gR!G!5*oV!YImQ)>US4 zfJo)Va6$)|7}7}hrZ~OG4tD$dtXGD4+38=;IClzHOWf;coybkLDd(JYTgRl;9BpYHfTN3s0^DiJ3o zt{yop5DN+lh>FjC+#kBo`IOW>nfs z(+q)gG3EiK%|9+L#UeAo+vx)@Y2`#FS^v1C1_#tqNZP@wSl*j`v$SZnYETZnGo9!A z#hix$?4LqINULP22Y&J?`_W4nU4?KcK1^mo)eJYz?0v>M?;OOdmt?Ggx0(wvY+_LY z7is06bV!nnANYo*BUO#4KzizrVF=0q4=^i8ViQYgQT<~QNr+PAfpNPKnyg||D&Roa z^+{g#zS5IciJ$U%gzmD)bqO7C_S>c~i3wnsPW3hul~nDk>K53=2l8)II=SXz>LdtE z$CbuoHV6bfn6e=)iIlHq3dqD`G4UX^uiEMD_wlFe6``3vXt_rec~rF%2&+k?N4s1* zYwBjgi8Q3Eb$fYE#n+!D5JJ?=%2c;6=>@z^IV z5;#~0uE&V&2y#CN?g`;RB^C&RAk_m8%##N8!klm(?c9{JW$h$$3(-d625w1=8V3aG8Iy((o%~vZ}cb>uIv% z{*zu-$k#jdGjoK@N=H(pUA=Z%%%`WtO|NSut(CzhPOD$=H=p0m3P@&UB$%_jJijS8xeAxjrq(~e=3P>|W z61t{$ElXjF6gW|xi6ODZR1jc^0TPEEE^)I59%xvKCWV#sB$5+i1ull4;s`M-4n7D+ z%7PQF1nn<=x$3tE__Q^ZBD$vJK>pys#^5X~m<^QR3@%V8Od{DZWTwFBijd&F8FO_! zInHvuKPg2?YHFGJW_?8}O67I=wzgkWc!=`-iPf*PsV3|NbvjgC;{1KOsJrcZ&b*K|){!=?j5ll~ z<-|cnL_e~v2xeC!2$r;xC)%W6OJ-d$x{NR z!jtaIJ*YBy@z-Z>d^11o-L*Hw*PGIeHfabcF@!gH2UnWJ)>KA4Yf|vNY^v0aHZx*B zzX;}g2;%&m;@udf*u55ZBv1UMQ=$Y461L~wmd2X4>X+qzOsuh(Bq0$HzZ1M>Ub&w2 zcD!gL)jcFLC&EG6or0-3dgDQgFG2>MdlJm_+{E>jQ4Jo;Tz-|3iOvMWkvOziZdh88 zBAQydIUV&qy{;D8Y{;I3gA<^{Co3Hc(z_!pvDq|=YA2DUZG8Ip zQ9+1_Q;tOvYU^m0_R2?TiEe{&5zt;11tE|l9gjywWF#wnP~OV!<+ED(1$ONCXo?C% zA|rFtA~tGPR90P1<#XtsZ7H7EeX-E-Ihf3xS`Ztbi^!RiC2Omtg(q7N*)?dUa_m@S zXfhBJ3CWbWN>A2?M}224lGs~U_~Dluha_Z%L?ndHmMc!nR}Pdqd-KYj5jiu_K#;U{ SD=r!d-W!}Fdi^#fpe)sD>8if~ diff --git a/spring-ai-spring-boot-autoconfigure/src/test/resources/spring_framework.png b/spring-ai-spring-boot-autoconfigure/src/test/resources/spring_framework.png deleted file mode 100644 index 735057a6b1bda50f7dd245a3151109d4a76f04f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43647 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 EKXNh$1^@s6 diff --git a/spring-ai-spring-boot-docker-compose/pom.xml b/spring-ai-spring-boot-docker-compose/pom.xml index 21f60e00c..9aa2bc4c3 100644 --- a/spring-ai-spring-boot-docker-compose/pom.xml +++ b/spring-ai-spring-boot-docker-compose/pom.xml @@ -43,12 +43,6 @@ - - org.springframework.ai - spring-ai-spring-boot-autoconfigure - ${project.parent.version} - - org.springframework.ai spring-ai-autoconfigure-vector-store-opensearch @@ -107,6 +101,13 @@ true + + org.springframework.ai + spring-ai-autoconfigure-model-ollama + ${project.parent.version} + true + + org.springframework.ai diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-coherence-store/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-coherence-store/pom.xml index 993ba3e84..b05c43c4c 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-coherence-store/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-coherence-store/pom.xml @@ -42,12 +42,6 @@ spring-boot-starter - - org.springframework.ai - spring-ai-spring-boot-autoconfigure - ${project.parent.version} - - org.springframework.ai spring-ai-coherence-store diff --git a/spring-ai-spring-boot-testcontainers/pom.xml b/spring-ai-spring-boot-testcontainers/pom.xml index b60ef7e77..c90437477 100644 --- a/spring-ai-spring-boot-testcontainers/pom.xml +++ b/spring-ai-spring-boot-testcontainers/pom.xml @@ -46,7 +46,7 @@ org.springframework.ai - spring-ai-spring-boot-autoconfigure + spring-ai-autoconfigure-model-ollama ${project.parent.version} diff --git a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryIT.java b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryIT.java index 65c58f5db..d3217fc2c 100644 --- a/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryIT.java +++ b/spring-ai-spring-boot-testcontainers/src/test/java/org/springframework/ai/testcontainers/service/connection/ollama/OllamaContainerConnectionDetailsFactoryIT.java @@ -28,8 +28,8 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.ollama.OllamaContainer; -import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration; import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; @@ -79,7 +79,7 @@ class OllamaContainerConnectionDetailsFactoryIT { } @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration({ RestClientAutoConfiguration.class, OllamaAutoConfiguration.class }) + @ImportAutoConfiguration({ RestClientAutoConfiguration.class, OllamaEmbeddingAutoConfiguration.class }) static class Config { }