Add a new abstraction to simplify implementation of common ChatBot use cases

* Add ChatBot and basic DefaultChatBot
* Add streaming ChatBot support.
* Add Evaluator interface and RelevancyEvaluator implementation
* Add Content data type abstraction for Document and Message
* Renaming and package refactoring
* update .gitignore to allow node package name
* Add List<Media> to node and move ai.transformer package to ai.prompt.transformer
* Add Short/Long term memory support.
* Add mixing transformers support

Docs TBD
This commit is contained in:
Mark Pollack
2024-04-18 13:09:47 -04:00
parent 012a2ad74a
commit dfb8bf6a44
54 changed files with 3376 additions and 99 deletions

View File

@@ -57,8 +57,8 @@ public class ClientIT {
}
```""";
assertThat(chatResponse.getResult().getOutput().getContent()).isEqualTo(expectedResponse);
assertThat(chatResponse.getResult().getOutput().getProperties()).containsKey("generated_tokens");
assertThat(chatResponse.getResult().getOutput().getProperties()).containsEntry("generated_tokens", 39);
assertThat(chatResponse.getResult().getOutput().getMetadata()).containsKey("generated_tokens");
assertThat(chatResponse.getResult().getOutput().getMetadata()).containsEntry("generated_tokens", 39);
}

View File

@@ -1,5 +1,6 @@
<?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">
<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>
@@ -74,6 +75,38 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai</artifactId>
</exclusion>
</exclusions>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>qdrant</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,131 @@
/*
* 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.openai.chat.chatbot;
import java.util.List;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.chatbot.ChatBot;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.qdrant.QdrantContainer;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.chatbot.DefaultStreamingChatBot;
import org.springframework.ai.chat.chatbot.StreamingChatBot;
import org.springframework.ai.chat.history.VectorStoreChatMemoryAgentListener;
import org.springframework.ai.chat.history.VectorStoreChatMemoryRetriever;
import org.springframework.ai.chat.history.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.history.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@Testcontainers
@SpringBootTest(classes = ChatMemoryLongTermSystemPromptIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
@Autowired
public ChatMemoryLongTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatClient openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatClient(openAiApi);
}
@Bean
public EmbeddingClient embeddingClient(OpenAiApi openAiApi) {
return new OpenAiEmbeddingClient(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingClient embeddingClient) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingClient);
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatBot memoryChatAgent(OpenAiChatClient chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new VectorStoreChatMemoryAgentListener(vectorStore)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatAgent(OpenAiChatClient streamingChatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new VectorStoreChatMemoryAgentListener(vectorStore)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatClient chatClient) {
return new RelevancyEvaluator(chatClient);
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.openai.chat.chatbot;
import java.util.List;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.chatbot.ChatBot;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.chatbot.DefaultStreamingChatBot;
import org.springframework.ai.chat.chatbot.StreamingChatBot;
import org.springframework.ai.chat.history.ChatMemory;
import org.springframework.ai.chat.history.ChatMemoryAgentListener;
import org.springframework.ai.chat.history.ChatMemoryRetriever;
import org.springframework.ai.chat.history.InMemoryChatMemory;
import org.springframework.ai.chat.history.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.history.MessageChatMemoryAugmentor;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@SpringBootTest(classes = ChatMemoryShortTermMessageListIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermMessageListIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatClient openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatClient(openAiApi);
}
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatBot memoryChatAgent(OpenAiChatClient chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new ChatMemoryAgentListener(chatHistory)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatAgent(OpenAiChatClient streamingChatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new ChatMemoryAgentListener(chatHistory)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatClient chatClient) {
return new RelevancyEvaluator(chatClient);
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.openai.chat.chatbot;
import java.util.List;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.chatbot.ChatBot;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.chatbot.DefaultStreamingChatBot;
import org.springframework.ai.chat.chatbot.StreamingChatBot;
import org.springframework.ai.chat.history.ChatMemory;
import org.springframework.ai.chat.history.ChatMemoryAgentListener;
import org.springframework.ai.chat.history.ChatMemoryRetriever;
import org.springframework.ai.chat.history.InMemoryChatMemory;
import org.springframework.ai.chat.history.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.history.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@SpringBootTest(classes = ChatMemoryShortTermSystemPromptIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatClient openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatClient(openAiApi);
}
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatBot memoryChatAgent(OpenAiChatClient chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new ChatMemoryAgentListener(chatHistory)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatAgent(OpenAiChatClient streamingChatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatAgentListeners(List.of(new ChatMemoryAgentListener(chatHistory)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatClient chatClient) {
return new RelevancyEvaluator(chatClient);
}
}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.openai.chat.chatbot;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
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.chat.chatbot.ChatBot;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.qdrant.QdrantContainer;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.history.ChatMemory;
import org.springframework.ai.chat.history.ChatMemoryAgentListener;
import org.springframework.ai.chat.history.ChatMemoryRetriever;
import org.springframework.ai.chat.history.InMemoryChatMemory;
import org.springframework.ai.chat.history.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.history.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.chat.history.VectorStoreChatMemoryAgentListener;
import org.springframework.ai.chat.history.VectorStoreChatMemoryRetriever;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.QuestionContextAugmentor;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.VectorStoreRetriever;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentTransformer;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.evaluation.EvaluationRequest;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.ai.openai.api.OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW;
@Testcontainers
@SpringBootTest(classes = LongShortTermChatMemoryWithRagIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class LongShortTermChatMemoryWithRagIT {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
@Autowired
ChatBot chatBot;
@Autowired
RelevancyEvaluator relevancyEvaluator;
@Autowired
VectorStore vectorStore;
@Value("classpath:/data/acme/bikes.json")
private Resource bikesResource;
void loadData() {
var metadataEnricher = new DocumentTransformer() {
@Override
public List<Document> apply(List<Document> documents) {
documents.forEach(d -> {
Map<String, Object> metadata = d.getMetadata();
metadata.put(TransformerContentType.EXTERNAL_KNOWLEDGE, "true");
});
return documents;
}
};
JsonReader jsonReader = new JsonReader(bikesResource, "name", "price", "shortDescription", "description");
var textSplitter = new TokenTextSplitter();
vectorStore.accept(metadataEnricher.apply(textSplitter.apply(jsonReader.get())));
}
// @Autowired
// StreamingChatBot streamingChatBot;
@Test
void memoryChatBot() {
loadData();
var prompt = new Prompt(new UserMessage("My name is Christian and I like mountain bikes."));
PromptContext promptContext = new PromptContext(prompt);
var chatBotResponse1 = this.chatBot.call(promptContext);
logger.info("Response1: " + chatBotResponse1.getChatResponse().getResult().getOutput().getContent());
var chatBotResponse2 = this.chatBot.call(new PromptContext(
new Prompt(new String("What is my name and what bike model would you suggest for me?"))));
logger.info("Response2: " + chatBotResponse2.getChatResponse().getResult().getOutput().getContent());
// logger.info(chatBotResponse2.getPromptContext().getContents().toString());
assertThat(chatBotResponse2.getChatResponse().getResult().getOutput().getContent()).contains("Christian");
EvaluationResponse evaluationResponse = this.relevancyEvaluator
.evaluate(new EvaluationRequest(chatBotResponse2));
assertTrue(evaluationResponse.isPass(), "Response is not relevant to the question");
}
@SpringBootConfiguration
static class Config {
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatClient openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatClient(openAiApi);
}
@Bean
public OpenAiEmbeddingClient embeddingClient(OpenAiApi openAiApi) {
return new OpenAiEmbeddingClient(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingClient embeddingClient) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingClient);
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatBot memoryChatAgent(OpenAiChatClient chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator, ChatMemory chatHistory) {
return DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults()),
new ChatMemoryRetriever(chatHistory, Map.of(TransformerContentType.SHORT_TERM_MEMORY, "")),
new VectorStoreChatMemoryRetriever(vectorStore, 10,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
.withContentPostProcessors(List.of(
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000,
Set.of(TransformerContentType.SHORT_TERM_MEMORY)),
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000,
Set.of(TransformerContentType.LONG_TERM_MEMORY)),
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 2000,
Set.of(TransformerContentType.EXTERNAL_KNOWLEDGE))))
.withAugmentors(List.of(new QuestionContextAugmentor(),
new SystemPromptChatMemoryAugmentor(
"""
Use the long term conversation history from the LONG TERM HISTORY section to provide accurate answers.
LONG TERM HISTORY:
{history}
""",
Set.of(TransformerContentType.LONG_TERM_MEMORY)),
new SystemPromptChatMemoryAugmentor(Set.of(TransformerContentType.SHORT_TERM_MEMORY))))
.withChatAgentListeners(List.of(new ChatMemoryAgentListener(chatHistory),
new VectorStoreChatMemoryAgentListener(vectorStore,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
.build();
}
// @Bean
// public StreamingChatBot memoryStreamingChatAgent(OpenAiChatClient
// streamingChatClient,
// VectorStore vectorStore, TokenCountEstimator tokenCountEstimator, ChatHistory
// chatHistory) {
// return DefaultStreamingChatBot.builder(streamingChatClient)
// .withRetrievers(List.of(new ChatHistoryRetriever(chatHistory), new
// DocumentChatHistoryRetriever(vectorStore, 10)))
// .withDocumentPostProcessors(List.of(new
// LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
// .withAugmentors(List.of(new TextChatHistoryAugmenter()))
// .withChatAgentListeners(List.of(new ChatHistoryAgentListener(chatHistory), new
// DocumentChatHistoryAgentListener(vectorStore)))
// .build();
// }
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatClient chatClient) {
// Use GPT 4 as a better model for determining relevancy. gpt 3.5 makes basic
// mistakes
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.withModel(GPT_4_TURBO_PREVIEW.getValue())
.build();
return new RelevancyEvaluator(chatClient, openAiChatOptions);
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* 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.openai.chat.chatbot;
import java.util.List;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.springframework.ai.chat.chatbot.ChatBot;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.qdrant.QdrantContainer;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.QuestionContextAugmentor;
import org.springframework.ai.chat.prompt.transformer.VectorStoreRetriever;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.evaluation.EvaluationRequest;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
import org.springframework.ai.openai.OpenAiEmbeddingClient;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.ai.openai.api.OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW;
@Testcontainers
@SpringBootTest(classes = OpenAiDefaultChatBotIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiDefaultChatBotIT {
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.7.4");
private final ChatClient chatClient;
private final VectorStore vectorStore;
@Value("classpath:/data/acme/bikes.json")
private Resource bikesResource;
private ChatBot chatBot;
@Autowired
public OpenAiDefaultChatBotIT(ChatClient chatClient, ChatBot chatBot, VectorStore vectorStore) {
this.chatClient = chatClient;
this.chatBot = chatBot;
this.vectorStore = vectorStore;
}
@Test
void simpleChat() {
loadData();
var prompt = new Prompt(new UserMessage("What bike is good for city commuting?"));
var chatBotResponse = this.chatBot.call(new PromptContext(prompt));
String answer = chatBotResponse.getChatResponse().getResult().getOutput().getContent();
assertTrue(answer.contains("Celerity"), "Response does not include 'Celerity'");
// Use GPT 4 as a better model for determining relevancy. gpt 3.5 makes basic
// mistakes
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.withModel(GPT_4_TURBO_PREVIEW.getValue())
.build();
var relevancyEvaluator = new RelevancyEvaluator(this.chatClient, openAiChatOptions);
EvaluationRequest evaluationRequest = new EvaluationRequest(chatBotResponse);
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertTrue(evaluationResponse.isPass(), "Response is not relevant to the question");
}
void loadData() {
JsonReader jsonReader = new JsonReader(bikesResource, "name", "price", "shortDescription", "description");
var textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(jsonReader.get());
for (Document splitDocument : splitDocuments) {
splitDocument.getMetadata().put(TransformerContentType.EXTERNAL_KNOWLEDGE, "true");
}
vectorStore.accept(splitDocuments);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public ChatClient openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatClient(openAiApi);
}
@Bean
public EmbeddingClient embeddingClient(OpenAiApi openAiApi) {
return new OpenAiEmbeddingClient(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingClient embeddingClient) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingClient);
}
@Bean
public ChatBot chatBot(ChatClient chatClient, VectorStore vectorStore) {
return DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults())))
.withAugmentors(List.of(new QuestionContextAugmentor()))
.build();
}
}
}