OpenAI implementation with autoconfig and starters

This commit is contained in:
Mark Pollack
2023-08-05 11:13:42 -04:00
parent 554f584d08
commit e042a8e604
16 changed files with 422 additions and 39 deletions

View File

@@ -1,8 +1,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-experimental-ai</artifactId>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
@@ -13,6 +13,9 @@
<modules>
<module>spring-ai-core</module>
<module>spring-ai-openai</module>
<module>spring-ai-spring-boot-autoconfigure</module>
<module>spring-ai-spring-boot-starters/spring-ai-starter-openai</module>
<module>spring-ai-docs</module>
</modules>
@@ -59,6 +62,7 @@
<!-- prodution dependencies -->
<spring-boot.version>3.1.2</spring-boot.version>
<stringtemplate.version>4.0.2</stringtemplate.version>
<open-ai-client.version>0.12.0</open-ai-client.version>
<!-- documentation dependencies -->
<asciidoctorj-pdf.version>1.6.2</asciidoctorj-pdf.version> <!-- FIXME build failure with version 2.3.9 -->

View File

@@ -2,8 +2,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-experimental-ai</artifactId>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-core</artifactId>

View File

@@ -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<String, Object> model) {
return new Prompt(new AiMessage(render(model), this.example));
return new Prompt(new AssistantMessage(render(model), this.example));
}
}

View File

@@ -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<Message> messages;
public Prompt(String contents) {
this.messages = Collections.singletonList(new HumanMessage(contents));
this.messages = Collections.singletonList(new UserMessage(contents));
}
public Prompt(Message message) {

View File

@@ -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<String, Object> properties) {
super(MessageType.ASSISTANT, content, properties);
this.example = example;
}
public boolean isExample() {
return example;
}
}

View File

@@ -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"),

View File

@@ -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) {

View File

@@ -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<String, Object> properties) {
super(MessageType.AI, content, properties);
this.example = example;
}
public boolean isExample() {
return example;
public UserMessage(String content, Map<String, Object> properties) {
super(MessageType.USER, content, properties);
}
}

View File

@@ -2,8 +2,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-experimental-ai</artifactId>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-docs</artifactId>

55
spring-ai-openai/pom.xml Normal file
View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-openai</artifactId>
<packaging>jar</packaging>
<name>Spring AI OpenAI</name>
<description>OpenAI support</description>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<scm>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<connection>git://github.com/spring-projects-experimental/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects-experimental/spring-ai.git</developerConnection>
</scm>
<dependencies>
<!-- production dependencies -->
<dependency>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.theokanning.openai-gpt3-java</groupId>
<artifactId>service</artifactId>
<version>${open-ai-client.version}</version>
</dependency>
<!-- Spring Framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<ChatCompletionRequest> 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> chatCompletionRequest) {
// TODO
throw new RuntimeException("LLMResult getLLMResult not yet implemented");
}
private List<ChatCompletionRequest> getChatCompletionRequest(Prompt[] prompts) {
List<ChatCompletionRequest> chatCompletionRequests = new ArrayList<>();
for (Prompt prompt : prompts) {
List<ChatMessage> chatMessages = convertToChatMessages(prompt.getMessages());
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(this.model)
.temperature(this.temperature)
.messages(chatMessages)
.build();
chatCompletionRequests.add(chatCompletionRequest);
}
return chatCompletionRequests;
}
private List<ChatMessage> convertToChatMessages(List<Message> messages) {
List<ChatMessage> 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;
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<packaging>jar</packaging>
<name>Spring AI Auto Configuration</name>
<description>Spring AI Auto Configuration</description>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<scm>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<connection>git://github.com/spring-projects-experimental/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects-experimental/spring-ai.git</developerConnection>
</scm>
<dependencies>
<!-- production dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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);
}
}

View File

@@ -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<String, Object> properties) {
super(MessageType.HUMAN, content, properties);
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}

View File

@@ -0,0 +1 @@
org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<packaging>jar</packaging>
<name>Spring AI Starters</name>
<description>Spring AI Auto Configuration</description>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<scm>
<url>https://github.com/spring-projects-experimental/spring-ai</url>
<connection>git://github.com/spring-projects-experimental/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects-experimental/spring-ai.git</developerConnection>
</scm>
<dependencies>
<!-- production dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.experimental.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>