From a105548a273231292f5f30e774ccbd1ecad01440 Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Sat, 5 Aug 2023 17:15:30 -0400 Subject: [PATCH] Add Azure OpenAI support --- pom.xml | 3 + spring-ai-azure-openai/pom.xml | 55 +++++++++++ .../azure/openai/llm/AzureOpenAiClient.java | 94 +++++++++++++++++++ .../ai/core/prompt/PromptTests.java | 1 + .../ai/openai/llm/OpenAiClient.java | 19 +++- spring-ai-spring-boot-autoconfigure/pom.xml | 7 ++ .../openai/AzureOpenAiAutoConfiguration.java | 63 +++++++++++++ .../azure/openai/AzureOpenAiProperties.java | 74 +++++++++++++++ .../openai/OpenAiAutoConfiguration.java | 11 ++- .../openai/OpenAiProperties.java | 26 ++++- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../spring-ai-starter-azure-openai/pom.xml | 50 ++++++++++ .../spring-ai-starter-openai/pom.xml | 4 +- 13 files changed, 402 insertions(+), 8 deletions(-) create mode 100644 spring-ai-azure-openai/pom.xml create mode 100644 spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/llm/AzureOpenAiClient.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiProperties.java create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml diff --git a/pom.xml b/pom.xml index 7037a6bc5..d78e5124b 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,10 @@ spring-ai-core spring-ai-openai + spring-ai-azure-openai spring-ai-spring-boot-autoconfigure spring-ai-spring-boot-starters/spring-ai-starter-openai + spring-ai-spring-boot-starters/spring-ai-starter-azure-openai spring-ai-docs @@ -63,6 +65,7 @@ 3.1.2 4.0.2 0.12.0 + 1.0.0-beta.3 1.6.2 diff --git a/spring-ai-azure-openai/pom.xml b/spring-ai-azure-openai/pom.xml new file mode 100644 index 000000000..0e9dfdf75 --- /dev/null +++ b/spring-ai-azure-openai/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 1.0.0-SNAPSHOT + + spring-ai-azure-openai + jar + Spring AI Azure OpenAI + OpenAI support + https://github.com/spring-projects-experimental/spring-ai + + + https://github.com/spring-projects-experimental/spring-ai + git://github.com/spring-projects-experimental/spring-ai.git + git@github.com:spring-projects-experimental/spring-ai.git + + + + + + + org.springframework.experimental.ai + spring-ai-core + ${project.parent.version} + + + + com.azure + azure-ai-openai + ${azure-open-ai-client.version} + + + + + org.springframework + spring-context-support + + + + org.springframework.boot + spring-boot-starter-logging + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/llm/AzureOpenAiClient.java b/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/llm/AzureOpenAiClient.java new file mode 100644 index 000000000..ae123174d --- /dev/null +++ b/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/llm/AzureOpenAiClient.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 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.azure.openai.llm; + +import java.util.List; + +import com.azure.ai.openai.OpenAIClient; +import com.azure.ai.openai.models.ChatChoice; +import com.azure.ai.openai.models.ChatCompletions; +import com.azure.ai.openai.models.ChatCompletionsOptions; +import com.azure.ai.openai.models.ChatMessage; +import com.azure.ai.openai.models.ChatRole; +import com.azure.ai.openai.models.Choice; +import com.azure.ai.openai.models.Completions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.ai.core.llm.LLMResult; +import org.springframework.ai.core.llm.LlmClient; +import org.springframework.ai.core.prompt.Prompt; +import org.springframework.util.Assert; + +/** + * Implementation of {@link LlmClient} backed by an OpenAiService + */ +public class AzureOpenAiClient implements LlmClient { + + private static final Logger logger = LoggerFactory.getLogger(AzureOpenAiClient.class); + + private final OpenAIClient msoftOpenAiClient; + + private Double temperature = 0.5; + + private String model = "gpt-35-turbo"; + + public AzureOpenAiClient(OpenAIClient msoftOpenAiClient) { + Assert.notNull(msoftOpenAiClient, "OpenAiClient must not be null"); + this.msoftOpenAiClient = msoftOpenAiClient; + } + + @Override + public String generate(String text) { + ChatMessage chatMessage = new ChatMessage(ChatRole.USER, text); + + ChatCompletionsOptions options = new ChatCompletionsOptions(List.of(chatMessage)); + options.setTemperature(this.getTemperature()); + options.setModel(this.getModel()); + + ChatCompletions chatCompletions = this.msoftOpenAiClient.getChatCompletions(this.getModel(), options); + StringBuilder sb = new StringBuilder(); + for (ChatChoice choice : chatCompletions.getChoices()) { + if (choice.getMessage() != null && choice.getMessage().getContent() != null) { + sb.append(choice.getMessage().getContent()); + } + } + return sb.toString(); + } + + @Override + public LLMResult generate(Prompt... prompts) { + throw new RuntimeException("Method LLMResult generate(Prompt... prompts) not implemented."); + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + +} diff --git a/spring-ai-core/src/test/java/org/springframework/ai/core/prompt/PromptTests.java b/spring-ai-core/src/test/java/org/springframework/ai/core/prompt/PromptTests.java index 15d192c9c..803a09bef 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/core/prompt/PromptTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/core/prompt/PromptTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +@SuppressWarnings("unchecked") class PromptTests { @Test diff --git a/spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java b/spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java index 666a1a947..bfc8200cf 100644 --- a/spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java +++ b/spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java @@ -39,8 +39,9 @@ public class OpenAiClient implements LlmClient { private static final Logger logger = LoggerFactory.getLogger(OpenAiClient.class); + // TODO how to set default options for the entire client // TODO expose request options into Prompt API via PromptOptions - private Double temperature = 0.3; + private Double temperature = 0.5; private String model = "gpt-3.5-turbo"; @@ -51,6 +52,22 @@ public class OpenAiClient implements LlmClient { this.openAiService = openAiService; } + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + @Override public String generate(String text) { ChatCompletionRequest chatCompletionRequest = getChatCompletionRequest(text); diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml index 0aec2cc4e..59feb9016 100644 --- a/spring-ai-spring-boot-autoconfigure/pom.xml +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -34,6 +34,13 @@ true + + org.springframework.experimental.ai + spring-ai-azure-openai + ${project.parent.version} + true + + org.springframework.boot diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java new file mode 100644 index 000000000..de500e057 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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.azure.openai; + +import com.azure.ai.openai.OpenAIClient; +import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.AzureKeyCredential; + +import org.springframework.ai.azure.openai.llm.AzureOpenAiClient; +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.StringUtils; + +@AutoConfiguration +@ConditionalOnClass(OpenAIClientBuilder.class) +@EnableConfigurationProperties(AzureOpenAiProperties.class) +public class AzureOpenAiAutoConfiguration { + + private final AzureOpenAiProperties azureOpenAiProperties; + + public AzureOpenAiAutoConfiguration(AzureOpenAiProperties azureOpenAiProperties) { + this.azureOpenAiProperties = azureOpenAiProperties; + } + + @Bean + @ConditionalOnMissingBean + public OpenAIClient msoftSdkOpenAiClient(AzureOpenAiProperties azureOpenAiProperties) { + if (!StringUtils.hasText(azureOpenAiProperties.getApiKey())) { + throw new IllegalArgumentException("You must provide an API key with the property name " + + AzureOpenAiProperties.CONFIG_PREFIX + ".api-key"); + } + OpenAIClient msoftSdkOpenAiClient = new OpenAIClientBuilder().endpoint(this.azureOpenAiProperties.getEndpoint()) + .credential(new AzureKeyCredential(this.azureOpenAiProperties.getApiKey())) + .buildClient(); + return msoftSdkOpenAiClient; + } + + @Bean + public AzureOpenAiClient azureOpenAiClient(OpenAIClient msoftSdkOpenAiClient) { + AzureOpenAiClient azureOpenAiClient = new AzureOpenAiClient(msoftSdkOpenAiClient); + azureOpenAiClient.setTemperature(this.azureOpenAiProperties.getTemperature()); + azureOpenAiClient.setModel(this.azureOpenAiProperties.getModel()); + return azureOpenAiClient; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiProperties.java new file mode 100644 index 000000000..890a07c99 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiProperties.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 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.azure.openai; + +import java.net.URI; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(AzureOpenAiProperties.CONFIG_PREFIX) +public class AzureOpenAiProperties { + + // TODO look into Spring Cloud Azure project for credentials as well as + // e.g. com.azure.core.credential.AzureKeyCredential + public static final String CONFIG_PREFIX = "spring.ai.azure.openai"; + + private String apiKey; + + private String endpoint; + + private Double temperature = 0.5; + + private String model = "gpt-35-turbo"; + + public String getEndpoint() { + return endpoint; + } + + /** + * Sets the service endpoint that will be connected to by clients. + * @param endpoint The URL of the service endpoint + */ + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getApiKey() { + return apiKey; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java index 3278ce18e..23138f984 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java @@ -25,6 +25,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; +import static org.springframework.ai.autoconfigure.openai.OpenAiProperties.CONFIG_PREFIX; + @AutoConfiguration @ConditionalOnClass(OpenAiService.class) @EnableConfigurationProperties(OpenAiProperties.class) @@ -40,10 +42,13 @@ public class OpenAiAutoConfiguration { public OpenAiClient openAiClient(OpenAiProperties openAiProperties) { if (!StringUtils.hasText(openAiProperties.getApiKey())) { throw new IllegalArgumentException( - "You must provide an API key with the property name spring.ai.openai.api-key"); + "You must provide an API key with the property name " + CONFIG_PREFIX + ".api-key"); } - OpenAiService openAiService = new OpenAiService(openAiProperties.getApiKey()); - return new OpenAiClient(openAiService); + OpenAiService theoOpenAiService = new OpenAiService(openAiProperties.getApiKey()); + OpenAiClient openAiClient = new OpenAiClient(theoOpenAiService); + openAiClient.setTemperature(openAiProperties.getTemperature()); + openAiClient.setModel(openAiProperties.getModel()); + return openAiClient; } } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java index ae61e1040..095ac5433 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java @@ -18,11 +18,19 @@ package org.springframework.ai.autoconfigure.openai; import org.springframework.boot.context.properties.ConfigurationProperties; -@ConfigurationProperties(prefix = "spring.ai.openai") +import static org.springframework.ai.autoconfigure.openai.OpenAiProperties.CONFIG_PREFIX; + +@ConfigurationProperties(CONFIG_PREFIX) public class OpenAiProperties { + public static final String CONFIG_PREFIX = "spring.ai.openai"; + private String apiKey; + private Double temperature = 0.5; + + private String model = "gpt-3.5-turbo"; + public String getApiKey() { return apiKey; } @@ -31,4 +39,20 @@ public class OpenAiProperties { this.apiKey = apiKey; } + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public Double getTemperature() { + return temperature; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + } 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 index 337264244..343f10957 100644 --- 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 @@ -1 +1,2 @@ -org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration \ No newline at end of file +org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration +org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration \ No newline at end of file diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml new file mode 100644 index 000000000..65af3d774 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 1.0.0-SNAPSHOT + + spring-ai-azure-openai-spring-boot-starter + jar + Spring AI Starter - Azure OpenAI + Spring AI Azure OpenAI Auto Configuration + https://github.com/spring-projects-experimental/spring-ai + + + https://github.com/spring-projects-experimental/spring-ai + git://github.com/spring-projects-experimental/spring-ai.git + git@github.com:spring-projects-experimental/spring-ai.git + + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.experimental.ai + spring-ai-spring-boot-autoconfigure + ${project.parent.version} + + + + org.springframework.experimental.ai + spring-ai-azure-openai + ${project.parent.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml index 4a336226f..d4245a0a1 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml @@ -8,8 +8,8 @@ spring-ai-openai-spring-boot-starter jar - Spring AI Starters - Spring AI Auto Configuration + Spring AI Starter - OpenAI + Spring AI Open AI Auto Configuration https://github.com/spring-projects-experimental/spring-ai