From e042a8e60471171d9d33b386e9f1246a11f5cdee Mon Sep 17 00:00:00 2001 From: Mark Pollack Date: Sat, 5 Aug 2023 11:13:42 -0400 Subject: [PATCH] OpenAI implementation with autoconfig and starters --- pom.xml | 8 +- spring-ai-core/pom.xml | 4 +- .../ai/core/prompt/AiPromptTemplate.java | 6 +- .../ai/core/prompt/Prompt.java | 4 +- .../prompt/messages/AssistantMessage.java | 49 +++++++ .../ai/core/prompt/messages/MessageType.java | 6 +- .../core/prompt/messages/SystemMessage.java | 6 + .../{AiMessage.java => UserMessage.java} | 27 ++-- spring-ai-docs/pom.xml | 4 +- spring-ai-openai/pom.xml | 55 ++++++++ .../ai/openai/llm/OpenAiClient.java | 130 ++++++++++++++++++ spring-ai-spring-boot-autoconfigure/pom.xml | 45 ++++++ .../openai/OpenAiAutoConfiguration.java | 49 +++++++ .../openai/OpenAiProperties.java | 17 ++- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../spring-ai-starter-openai/pom.xml | 50 +++++++ 16 files changed, 422 insertions(+), 39 deletions(-) create mode 100644 spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AssistantMessage.java rename spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/{AiMessage.java => UserMessage.java} (59%) create mode 100644 spring-ai-openai/pom.xml create mode 100644 spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java create mode 100644 spring-ai-spring-boot-autoconfigure/pom.xml create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java rename spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/HumanMessage.java => spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java (64%) create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml diff --git a/pom.xml b/pom.xml index b97fc03f9..7037a6bc5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,8 @@ 4.0.0 - org.springframework.ai - spring-experimental-ai + org.springframework.experimental.ai + spring-ai 1.0.0-SNAPSHOT pom @@ -13,6 +13,9 @@ spring-ai-core + spring-ai-openai + spring-ai-spring-boot-autoconfigure + spring-ai-spring-boot-starters/spring-ai-starter-openai spring-ai-docs @@ -59,6 +62,7 @@ 3.1.2 4.0.2 + 0.12.0 1.6.2 diff --git a/spring-ai-core/pom.xml b/spring-ai-core/pom.xml index 78462cf22..ad196f8cc 100644 --- a/spring-ai-core/pom.xml +++ b/spring-ai-core/pom.xml @@ -2,8 +2,8 @@ 4.0.0 - org.springframework.ai - spring-experimental-ai + org.springframework.experimental.ai + spring-ai 1.0.0-SNAPSHOT spring-ai-core diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/AiPromptTemplate.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/AiPromptTemplate.java index 46f7c5ec7..a9c80a5f1 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/AiPromptTemplate.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/AiPromptTemplate.java @@ -18,7 +18,7 @@ package org.springframework.ai.core.prompt; import java.util.Map; -import org.springframework.ai.core.prompt.messages.AiMessage; +import org.springframework.ai.core.prompt.messages.AssistantMessage; public class AiPromptTemplate extends PromptTemplate { @@ -35,12 +35,12 @@ public class AiPromptTemplate extends PromptTemplate { @Override public Prompt create() { - return new Prompt(new AiMessage(render())); + return new Prompt(new AssistantMessage(render())); } @Override public Prompt create(Map model) { - return new Prompt(new AiMessage(render(model), this.example)); + return new Prompt(new AssistantMessage(render(model), this.example)); } } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/Prompt.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/Prompt.java index 639f6b68f..b0dc4f8bd 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/Prompt.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/Prompt.java @@ -19,7 +19,7 @@ package org.springframework.ai.core.prompt; import java.util.Collections; import java.util.List; -import org.springframework.ai.core.prompt.messages.HumanMessage; +import org.springframework.ai.core.prompt.messages.UserMessage; import org.springframework.ai.core.prompt.messages.Message; public class Prompt { @@ -27,7 +27,7 @@ public class Prompt { private List messages; public Prompt(String contents) { - this.messages = Collections.singletonList(new HumanMessage(contents)); + this.messages = Collections.singletonList(new UserMessage(contents)); } public Prompt(Message message) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AssistantMessage.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AssistantMessage.java new file mode 100644 index 000000000..f1ced5717 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AssistantMessage.java @@ -0,0 +1,49 @@ +/* + * 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.core.prompt.messages; + +import java.util.Map; + +/** + * Lets the model know the content was generated as a response to the user. This role + * indicates messages that the model has previously generated in the conversation. By + * including assistant messages in the series, you provide context to the model about + * prior exchanges in the conversation. + */ +public class AssistantMessage extends AbstractMessage { + + private boolean example = false; + + public AssistantMessage(String content) { + super(MessageType.ASSISTANT, content); + } + + public AssistantMessage(String content, boolean example) { + super(MessageType.ASSISTANT, content); + this.example = example; + } + + public AssistantMessage(String content, boolean example, Map properties) { + super(MessageType.ASSISTANT, content, properties); + this.example = example; + } + + public boolean isExample() { + return example; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/MessageType.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/MessageType.java index 7478d197b..62f761aad 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/MessageType.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/MessageType.java @@ -17,11 +17,9 @@ package org.springframework.ai.core.prompt.messages; public enum MessageType { - HUMAN("human"), + USER("user"), - AI("ai"), - - CHAT("chat"), + ASSISTANT("assistant"), SYSTEM("system"), diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/SystemMessage.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/SystemMessage.java index 1c85d9091..e8f6362f1 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/SystemMessage.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/SystemMessage.java @@ -18,6 +18,12 @@ package org.springframework.ai.core.prompt.messages; import java.util.Map; +/** + * A message of the type 'system' passed as input. The system message gives high level + * instructions for the conversation. This role typically provides high-level instructions + * for the conversation. For example, you might use a system message to instruct the model + * to behave like a certain character or to provide answers in a specific format. + */ public class SystemMessage extends AbstractMessage { public SystemMessage(String content) { diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AiMessage.java b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/UserMessage.java similarity index 59% rename from spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AiMessage.java rename to spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/UserMessage.java index b03b35b0b..9abb2388b 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/AiMessage.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/UserMessage.java @@ -18,26 +18,19 @@ package org.springframework.ai.core.prompt.messages; import java.util.Map; -public class AiMessage extends AbstractMessage { +/** + * A message of the type 'user' passed as input Messages with the user role are from the + * end-user or developer. They represent questions, prompts, or any input that you want + * the model to respond to. + */ +public class UserMessage extends AbstractMessage { - private boolean example = false; - - public AiMessage(String content) { - super(MessageType.AI, content); + public UserMessage(String content) { + super(MessageType.USER, content); } - public AiMessage(String content, boolean example) { - super(MessageType.AI, content); - this.example = example; - } - - public AiMessage(String content, boolean example, Map properties) { - super(MessageType.AI, content, properties); - this.example = example; - } - - public boolean isExample() { - return example; + public UserMessage(String content, Map properties) { + super(MessageType.USER, content, properties); } } diff --git a/spring-ai-docs/pom.xml b/spring-ai-docs/pom.xml index a8c8a2ef7..af287815b 100644 --- a/spring-ai-docs/pom.xml +++ b/spring-ai-docs/pom.xml @@ -2,8 +2,8 @@ 4.0.0 - org.springframework.ai - spring-experimental-ai + org.springframework.experimental.ai + spring-ai 1.0.0-SNAPSHOT spring-ai-docs diff --git a/spring-ai-openai/pom.xml b/spring-ai-openai/pom.xml new file mode 100644 index 000000000..435601d43 --- /dev/null +++ b/spring-ai-openai/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 1.0.0-SNAPSHOT + + spring-ai-openai + jar + Spring AI 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.theokanning.openai-gpt3-java + service + ${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-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 new file mode 100644 index 000000000..666a1a947 --- /dev/null +++ b/spring-ai-openai/src/main/java/org/springframework/ai/openai/llm/OpenAiClient.java @@ -0,0 +1,130 @@ +/* + * 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.openai.llm; + +import java.util.ArrayList; +import java.util.List; + +import com.theokanning.openai.completion.chat.ChatCompletionRequest; +import com.theokanning.openai.completion.chat.ChatMessage; +import com.theokanning.openai.service.OpenAiService; +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.ai.core.prompt.messages.Message; +import org.springframework.util.Assert; + +/** + * Implementation of {@link LlmClient} backed by an OpenAiService + */ +public class OpenAiClient implements LlmClient { + + private static final Logger logger = LoggerFactory.getLogger(OpenAiClient.class); + + // TODO expose request options into Prompt API via PromptOptions + private Double temperature = 0.3; + + private String model = "gpt-3.5-turbo"; + + private final OpenAiService openAiService; + + public OpenAiClient(OpenAiService openAiService) { + Assert.notNull(openAiService, "OpenAiService must not be null"); + this.openAiService = openAiService; + } + + @Override + public String generate(String text) { + ChatCompletionRequest chatCompletionRequest = getChatCompletionRequest(text); + return getResponse(chatCompletionRequest); + } + + @Override + public LLMResult generate(Prompt... prompts) { + List chatCompletionRequests = getChatCompletionRequest(prompts); + return getLLMResult(chatCompletionRequests); + } + + private ChatCompletionRequest getChatCompletionRequest(String text) { + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(this.model) + .temperature(this.temperature) + .messages(List.of(new ChatMessage("user", text))) + .build(); + return chatCompletionRequest; + } + + private String getResponse(ChatCompletionRequest chatCompletionRequest) { + StringBuilder builder = new StringBuilder(); + this.openAiService.createChatCompletion(chatCompletionRequest).getChoices().forEach(choice -> { + builder.append(choice.getMessage().getContent()); + }); + + String response = builder.toString(); + return response; + } + + private LLMResult getLLMResult(List chatCompletionRequest) { + // TODO + throw new RuntimeException("LLMResult getLLMResult not yet implemented"); + } + + private List getChatCompletionRequest(Prompt[] prompts) { + List chatCompletionRequests = new ArrayList<>(); + for (Prompt prompt : prompts) { + List chatMessages = convertToChatMessages(prompt.getMessages()); + ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder() + .model(this.model) + .temperature(this.temperature) + .messages(chatMessages) + .build(); + chatCompletionRequests.add(chatCompletionRequest); + } + return chatCompletionRequests; + } + + private List convertToChatMessages(List messages) { + List chatMessages = new ArrayList<>(); + for (Message promptMessage : messages) { + switch (promptMessage.getMessageType()) { + case USER: + chatMessages.add(new ChatMessage("user", promptMessage.getContent())); + break; + case ASSISTANT: + // TODO - valid? + chatMessages.add(new ChatMessage("assistant", promptMessage.getContent())); + break; + case SYSTEM: + chatMessages.add(new ChatMessage("system", promptMessage.getContent())); + break; + case FUNCTION: + logger.error( + "Can not send a Spring AI Function MessageType to the ChatGPT API, use 'system', 'user' or 'ai' message types."); + break; + default: + logger.error("Unknown Spring AI Chat MessageType. Use 'system', 'human' or 'ai' message types."); + break; + } + } + return chatMessages; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml new file mode 100644 index 000000000..0aec2cc4e --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + org.springframework.experimental.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-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-openai + ${project.parent.version} + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + 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 new file mode 100644 index 000000000..3278ce18e --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java @@ -0,0 +1,49 @@ +/* + * 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.openai; + +import com.theokanning.openai.service.OpenAiService; + +import org.springframework.ai.openai.llm.OpenAiClient; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.util.StringUtils; + +@AutoConfiguration +@ConditionalOnClass(OpenAiService.class) +@EnableConfigurationProperties(OpenAiProperties.class) +public class OpenAiAutoConfiguration { + + private final OpenAiProperties openAiProperties; + + public OpenAiAutoConfiguration(OpenAiProperties openAiProperties) { + this.openAiProperties = openAiProperties; + } + + @Bean + 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"); + } + OpenAiService openAiService = new OpenAiService(openAiProperties.getApiKey()); + return new OpenAiClient(openAiService); + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/HumanMessage.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java similarity index 64% rename from spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/HumanMessage.java rename to spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java index d0da89e86..ae61e1040 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/core/prompt/messages/HumanMessage.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiProperties.java @@ -14,18 +14,21 @@ * limitations under the License. */ -package org.springframework.ai.core.prompt.messages; +package org.springframework.ai.autoconfigure.openai; -import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; -public class HumanMessage extends AbstractMessage { +@ConfigurationProperties(prefix = "spring.ai.openai") +public class OpenAiProperties { - public HumanMessage(String content) { - super(MessageType.HUMAN, content); + private String apiKey; + + public String getApiKey() { + return apiKey; } - public HumanMessage(String content, Map properties) { - super(MessageType.HUMAN, content, properties); + public void setApiKey(String apiKey) { + this.apiKey = apiKey; } } 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 new file mode 100644 index 000000000..337264244 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration \ No newline at end of file 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 new file mode 100644 index 000000000..4a336226f --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + org.springframework.experimental.ai + spring-ai + 1.0.0-SNAPSHOT + + spring-ai-openai-spring-boot-starter + jar + Spring AI Starters + Spring AI 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-openai + ${project.parent.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + +