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:
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user