Changed ChatBot to ChatService with other related name changes

* PromptContext -> ChatServiceContext
* Added PromptChange to ChatServiceContext to capture PromptTransformer changes
* DefaultChatBot -> PromptTransformingChatService
* DefaultStreamingChatBot -> StreamingPromptTransformingChatService
* package name changes, chatbot->service and history->memory
* Added fluent builders to a few PromptTransformer implementations
* Add license headers
This commit is contained in:
Mark Pollack
2024-05-15 13:57:34 +02:00
parent d610dd6f1d
commit 867154c082
34 changed files with 837 additions and 643 deletions

View File

@@ -14,25 +14,25 @@
* limitations under the License.
*/
package org.springframework.ai.openai.chat.chatbot;
package org.springframework.ai.openai.chat.service;
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.springframework.ai.chat.service.ChatService;
import org.springframework.ai.chat.service.StreamingChatService;
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.VectorStoreChatMemoryChatBotListener;
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.chat.service.PromptTransformingChatService;
import org.springframework.ai.chat.service.StreamingPromptTransformingChatService;
import org.springframework.ai.chat.memory.VectorStoreChatMemoryChatServiceListener;
import org.springframework.ai.chat.memory.VectorStoreChatMemoryRetriever;
import org.springframework.ai.chat.memory.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.memory.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
@@ -61,9 +61,9 @@ public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
@Autowired
public ChatMemoryLongTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
public ChatMemoryLongTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
@@ -98,26 +98,26 @@ public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
}
@Bean
public ChatBot memoryChatBot(OpenAiChatClient chatClient, VectorStore vectorStore,
public ChatService memoryChatService(OpenAiChatClient chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatBotListeners(List.of(new VectorStoreChatMemoryChatBotListener(vectorStore)))
.withChatServiceListeners(List.of(new VectorStoreChatMemoryChatServiceListener(vectorStore)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatBot(OpenAiChatClient streamingChatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
public StreamingChatService memoryStreamingChatService(OpenAiChatClient streamingChatClient,
VectorStore vectorStore, TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatBotListeners(List.of(new VectorStoreChatMemoryChatBotListener(vectorStore)))
.withChatServiceListeners(List.of(new VectorStoreChatMemoryChatServiceListener(vectorStore)))
.build();
}

View File

@@ -13,22 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.openai.chat.chatbot;
package org.springframework.ai.openai.chat.service;
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.ChatMemoryChatBotListener;
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.chat.service.ChatService;
import org.springframework.ai.chat.service.PromptTransformingChatService;
import org.springframework.ai.chat.service.StreamingPromptTransformingChatService;
import org.springframework.ai.chat.service.StreamingChatService;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryChatServiceListener;
import org.springframework.ai.chat.memory.ChatMemoryRetriever;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.memory.MessageChatMemoryAugmentor;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
@@ -45,9 +45,9 @@ import org.springframework.context.annotation.Bean;
public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermMessageListIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
public ChatMemoryShortTermMessageListIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
@@ -74,26 +74,26 @@ public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
}
@Bean
public ChatBot memoryChatBot(OpenAiChatClient chatClient, ChatMemory chatHistory,
public ChatService memoryChatService(OpenAiChatClient chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatBot(OpenAiChatClient streamingChatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
public StreamingChatService memoryStreamingChatService(OpenAiChatClient streamingChatClient,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}

View File

@@ -14,22 +14,22 @@
* limitations under the License.
*/
package org.springframework.ai.openai.chat.chatbot;
package org.springframework.ai.openai.chat.service;
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.ChatMemoryChatBotListener;
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.service.ChatService;
import org.springframework.ai.chat.service.PromptTransformingChatService;
import org.springframework.ai.chat.service.StreamingPromptTransformingChatService;
import org.springframework.ai.chat.service.StreamingChatService;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryChatServiceListener;
import org.springframework.ai.chat.memory.ChatMemoryRetriever;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.memory.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatClient;
@@ -46,9 +46,9 @@ import org.springframework.context.annotation.Bean;
public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatBot) {
super(relevancyEvaluator, chatBot, streamingChatBot);
public ChatMemoryShortTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
@@ -75,26 +75,26 @@ public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
}
@Bean
public ChatBot memoryChatBot(OpenAiChatClient chatClient, ChatMemory chatHistory,
public ChatService memoryChatService(OpenAiChatClient chatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return DefaultChatBot.builder(chatClient)
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public StreamingChatBot memoryStreamingChatBot(OpenAiChatClient streamingChatClient, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
public StreamingChatService memoryStreamingChatService(OpenAiChatClient streamingChatClient,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return DefaultStreamingChatBot.builder(streamingChatClient)
return StreamingPromptTransformingChatService.builder(streamingChatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.openai.chat.chatbot;
package org.springframework.ai.openai.chat.service;
import java.util.List;
import java.util.Map;
@@ -26,24 +26,24 @@ 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.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.service.ChatService;
import org.springframework.ai.chat.service.PromptTransformingChatService;
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.ChatMemoryChatBotListener;
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.VectorStoreChatMemoryChatBotListener;
import org.springframework.ai.chat.history.VectorStoreChatMemoryRetriever;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.ChatMemoryChatServiceListener;
import org.springframework.ai.chat.memory.ChatMemoryRetriever;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.memory.LastMaxTokenSizeContentTransformer;
import org.springframework.ai.chat.memory.SystemPromptChatMemoryAugmentor;
import org.springframework.ai.chat.memory.VectorStoreChatMemoryChatServiceListener;
import org.springframework.ai.chat.memory.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;
@@ -89,7 +89,7 @@ public class LongShortTermChatMemoryWithRagIT {
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
@Autowired
ChatBot chatBot;
ChatService chatService;
@Autowired
RelevancyEvaluator relevancyEvaluator;
@@ -122,29 +122,29 @@ public class LongShortTermChatMemoryWithRagIT {
}
// @Autowired
// StreamingChatBot streamingChatBot;
// StreamingChatService streamingChatService;
@Test
void memoryChatBot() {
void memoryChatService() {
loadData();
var prompt = new Prompt(new UserMessage("My name is Christian and I like mountain bikes."));
PromptContext promptContext = new PromptContext(prompt);
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var chatBotResponse1 = this.chatBot.call(promptContext);
var chatServiceResponse1 = this.chatService.call(chatServiceContext);
logger.info("Response1: " + chatBotResponse1.getChatResponse().getResult().getOutput().getContent());
logger.info("Response1: " + chatServiceResponse1.getChatResponse().getResult().getOutput().getContent());
var chatBotResponse2 = this.chatBot.call(new PromptContext(
var chatServiceResponse2 = this.chatService.call(new ChatServiceContext(
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("Response2: " + chatServiceResponse2.getChatResponse().getResult().getOutput().getContent());
// logger.info(chatBotResponse2.getPromptContext().getContents().toString());
assertThat(chatBotResponse2.getChatResponse().getResult().getOutput().getContent()).contains("Christian");
// logger.info(chatServiceResponse2.getPromptContext().getContents().toString());
assertThat(chatServiceResponse2.getChatResponse().getResult().getOutput().getContent()).contains("Christian");
EvaluationResponse evaluationResponse = this.relevancyEvaluator
.evaluate(new EvaluationRequest(chatBotResponse2));
.evaluate(new EvaluationRequest(chatServiceResponse2));
assertTrue(evaluationResponse.isPass(), "Response is not relevant to the question");
@@ -187,12 +187,15 @@ public class LongShortTermChatMemoryWithRagIT {
}
@Bean
public ChatBot memoryChatBot(OpenAiChatClient chatClient, VectorStore vectorStore,
public ChatService memoryChatService(OpenAiChatClient chatClient, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator, ChatMemory chatHistory) {
return DefaultChatBot.builder(chatClient)
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults()),
new ChatMemoryRetriever(chatHistory, Map.of(TransformerContentType.SHORT_TERM_MEMORY, "")),
ChatMemoryRetriever.builder()
.withChatHistory(chatHistory)
.withMetadata(Map.of(TransformerContentType.SHORT_TERM_MEMORY, ""))
.build(),
new VectorStoreChatMemoryRetriever(vectorStore, 10,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
@@ -214,19 +217,19 @@ public class LongShortTermChatMemoryWithRagIT {
Set.of(TransformerContentType.LONG_TERM_MEMORY)),
new SystemPromptChatMemoryAugmentor(Set.of(TransformerContentType.SHORT_TERM_MEMORY))))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory),
new VectorStoreChatMemoryChatBotListener(vectorStore,
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory),
new VectorStoreChatMemoryChatServiceListener(vectorStore,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
.build();
}
// @Bean
// public StreamingChatBot memoryStreamingChatAgent(OpenAiChatClient
// public StreamingChatService memoryStreamingChatAgent(OpenAiChatClient
// streamingChatClient,
// VectorStore vectorStore, TokenCountEstimator tokenCountEstimator, ChatHistory
// chatHistory) {
// return DefaultStreamingChatBot.builder(streamingChatClient)
// return StreamingPromptTransformingChatService.builder(streamingChatClient)
// .withRetrievers(List.of(new ChatHistoryRetriever(chatHistory), new
// DocumentChatHistoryRetriever(vectorStore, 10)))
// .withDocumentPostProcessors(List.of(new

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.openai.chat.chatbot;
package org.springframework.ai.openai.chat.service;
import java.util.List;
@@ -22,20 +22,19 @@ 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.service.ChatService;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.testcontainers.containers.wait.strategy.Wait;
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.service.PromptTransformingChatService;
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.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.QuestionContextAugmentor;
import org.springframework.ai.chat.prompt.transformer.VectorStoreRetriever;
import org.springframework.ai.embedding.EmbeddingClient;
@@ -61,9 +60,9 @@ 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)
@SpringBootTest(classes = OpenAiPromptTransformingChatServiceIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiDefaultChatBotIT {
public class OpenAiPromptTransformingChatServiceIT {
private static final String COLLECTION_NAME = "test_collection";
@@ -79,12 +78,13 @@ public class OpenAiDefaultChatBotIT {
@Value("classpath:/data/acme/bikes.json")
private Resource bikesResource;
private ChatBot chatBot;
private ChatService chatService;
@Autowired
public OpenAiDefaultChatBotIT(ChatClient chatClient, ChatBot chatBot, VectorStore vectorStore) {
public OpenAiPromptTransformingChatServiceIT(ChatClient chatClient, ChatService chatService,
VectorStore vectorStore) {
this.chatClient = chatClient;
this.chatBot = chatBot;
this.chatService = chatService;
this.vectorStore = vectorStore;
}
@@ -93,8 +93,8 @@ public class OpenAiDefaultChatBotIT {
loadData();
var prompt = new Prompt(new UserMessage("What reliable road bike?"));
var chatBotResponse = this.chatBot.call(new PromptContext(prompt));
String answer = chatBotResponse.getChatResponse().getResult().getOutput().getContent();
var chatServiceResponse = this.chatService.call(new ChatServiceContext(prompt));
String answer = chatServiceResponse.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
@@ -103,7 +103,7 @@ public class OpenAiDefaultChatBotIT {
.withModel(GPT_4_TURBO_PREVIEW.getValue())
.build();
var relevancyEvaluator = new RelevancyEvaluator(this.chatClient, openAiChatOptions);
EvaluationRequest evaluationRequest = new EvaluationRequest(chatBotResponse);
EvaluationRequest evaluationRequest = new EvaluationRequest(chatServiceResponse);
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
assertTrue(evaluationResponse.isPass(), "Response is not relevant to the question");
@@ -148,8 +148,8 @@ public class OpenAiDefaultChatBotIT {
}
@Bean
public ChatBot chatBot(ChatClient chatClient, VectorStore vectorStore) {
return DefaultChatBot.builder(chatClient)
public ChatService chatService(ChatClient chatClient, VectorStore vectorStore) {
return PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults())))
.withAugmentors(List.of(new QuestionContextAugmentor()))
.build();

View File

@@ -1,72 +0,0 @@
package org.springframework.ai.chat.chatbot;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
/**
* Encapsulates the response from the ChatBot. Contains the most up-to-date PromptContext
* and the final ChatResponse
*
* @author Mark Pollack
* @since 1.0 M1
*/
public class StreamingChatBotResponse {
private final PromptContext promptContext;
private final Flux<ChatResponse> chatResponse;
public StreamingChatBotResponse(PromptContext promptContext, Flux<ChatResponse> chatResponse) {
this.promptContext = promptContext;
this.chatResponse = chatResponse;
}
public PromptContext getPromptContext() {
return promptContext;
}
public Flux<ChatResponse> getChatResponse() {
return chatResponse;
}
@Override
public String toString() {
return "ChatBotResponse{" + "promptContext=" + promptContext + ", chatResponse=" + chatResponse + '}';
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((promptContext == null) ? 0 : promptContext.hashCode());
result = prime * result + ((chatResponse == null) ? 0 : chatResponse.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
StreamingChatBotResponse other = (StreamingChatBotResponse) obj;
if (promptContext == null) {
if (other.promptContext != null)
return false;
}
else if (!promptContext.equals(other.promptContext))
return false;
if (chatResponse == null) {
if (other.chatResponse != null)
return false;
}
else if (!chatResponse.equals(other.chatResponse))
return false;
return true;
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.List;

View File

@@ -14,47 +14,47 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.List;
import org.springframework.ai.chat.chatbot.ChatBotResponse;
import org.springframework.ai.chat.chatbot.ChatBotListener;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.service.ChatServiceListener;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
/**
* @author Christian Tzolov
*/
public class ChatMemoryChatBotListener implements ChatBotListener {
public class ChatMemoryChatServiceListener implements ChatServiceListener {
private final ChatMemory chatHistory;
public ChatMemoryChatBotListener(ChatMemory chatHistory) {
public ChatMemoryChatServiceListener(ChatMemory chatHistory) {
this.chatHistory = chatHistory;
}
@Override
public void onStart(PromptContext promptContext) {
var messagesToAdd = promptContext.getPrompt()
public void onStart(ChatServiceContext chatServiceContext) {
var messagesToAdd = chatServiceContext.getPrompt()
.getInstructions()
.stream()
.filter(m -> !m.getMetadata().containsKey(TransformerContentType.MEMORY))
.filter(m -> (m.getMessageType() == MessageType.ASSISTANT || m.getMessageType() == MessageType.USER))
.toList();
this.chatHistory.add(promptContext.getConversationId(), messagesToAdd);
this.chatHistory.add(chatServiceContext.getConversationId(), messagesToAdd);
}
@Override
public void onComplete(ChatBotResponse chatBotResponse) {
List<Message> assistantMessages = chatBotResponse.getChatResponse()
public void onComplete(ChatServiceResponse chatServiceResponse) {
List<Message> assistantMessages = chatServiceResponse.getChatResponse()
.getResults()
.stream()
.map(g -> (Message) g.getOutput())
.toList();
this.chatHistory.add(chatBotResponse.getPromptContext().getConversationId(), assistantMessages);
this.chatHistory.add(chatServiceResponse.getPromptContext().getConversationId(), assistantMessages);
}
}

View File

@@ -14,68 +14,105 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.AbstractPromptTransformer;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.Content;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.Content;
/**
* @author Christian Tzolov
*/
public class ChatMemoryRetriever implements PromptTransformer {
public class ChatMemoryRetriever extends AbstractPromptTransformer {
private final ChatMemory chatHistory;
/**
* Additional metadata to be assigned to the retrieved history messages.
*/
private final Map<String, Object> additionalMetadata;
private final Map<String, Object> metadata;
private final int maxHistorySize;
public ChatMemoryRetriever(ChatMemory chatHistory) {
this(chatHistory, Map.of());
this(chatHistory, 1000, Map.of(), "ChatMemoryRetriever");
}
public ChatMemoryRetriever(ChatMemory chatHistory, Map<String, Object> additionalMetadata) {
this(chatHistory, 1000, additionalMetadata);
}
public ChatMemoryRetriever(ChatMemory chatHistory, int maxHistorySize, Map<String, Object> additionalMetadata) {
public ChatMemoryRetriever(ChatMemory chatHistory, int maxHistorySize, Map<String, Object> metadata, String name) {
this.chatHistory = chatHistory;
this.additionalMetadata = additionalMetadata;
this.metadata = metadata;
this.maxHistorySize = maxHistorySize;
this.setName(name);
}
@Override
public PromptContext transform(PromptContext promptContext) {
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Message> messageHistory = this.chatHistory.get(promptContext.getConversationId(), maxHistorySize);
List<Message> messageHistory = this.chatHistory.get(chatServiceContext.getConversationId(), maxHistorySize);
List<Content> historyContent = (messageHistory != null)
? messageHistory.stream().filter(m -> m.getMessageType() != MessageType.SYSTEM).map(m -> {
Content content = new Document(m.getContent(), new ArrayList<>(m.getMedia()),
new HashMap<>(m.getMetadata()));
content.getMetadata().putAll(this.additionalMetadata);
content.getMetadata().putAll(this.metadata);
content.getMetadata().put(TransformerContentType.MEMORY, true);
return content;
}).toList() : List.of();
List<Content> updatedContents = new ArrayList<>(
promptContext.getContents() != null ? promptContext.getContents() : List.of());
chatServiceContext.getContents() != null ? chatServiceContext.getContents() : List.of());
updatedContents.addAll(historyContent);
return PromptContext.from(promptContext).withContents(updatedContents).build();
return ChatServiceContext.from(chatServiceContext).withContents(updatedContents).build();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private ChatMemory chatHistory;
private Map<String, Object> metadata = Map.of();
private int maxHistorySize = 1000;
private String name = "ChatMemoryRetriever";
public Builder withChatHistory(ChatMemory chatHistory) {
this.chatHistory = chatHistory;
return this;
}
public Builder withMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}
public Builder withMaxHistorySize(int maxHistorySize) {
this.maxHistorySize = maxHistorySize;
return this;
}
public Builder withName(String name) {
this.name = name;
return this;
}
public ChatMemoryRetriever build() {
return new ChatMemoryRetriever(this.chatHistory, this.maxHistorySize, this.metadata, this.name);
}
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.List;

View File

@@ -14,14 +14,14 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import org.springframework.ai.chat.prompt.transformer.AbstractPromptTransformer;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.model.Content;
import org.springframework.ai.tokenizer.TokenCountEstimator;
@@ -33,7 +33,7 @@ import org.springframework.ai.tokenizer.TokenCountEstimator;
*
* @author Christian Tzolov
*/
public class LastMaxTokenSizeContentTransformer implements PromptTransformer {
public class LastMaxTokenSizeContentTransformer extends AbstractPromptTransformer {
protected final TokenCountEstimator tokenCountEstimator;
@@ -56,15 +56,15 @@ public class LastMaxTokenSizeContentTransformer implements PromptTransformer {
this.filterTags = filterTags;
}
protected List<Content> doGetDatumToModify(PromptContext promptContext) {
return promptContext.getContents()
protected List<Content> doGetDatumToModify(ChatServiceContext chatServiceContext) {
return chatServiceContext.getContents()
.stream()
.filter(content -> this.filterTags.stream().allMatch(tag -> content.getMetadata().containsKey(tag)))
.toList();
}
protected List<Content> doGetDatumNotToModify(PromptContext promptContext) {
return promptContext.getContents()
protected List<Content> doGetDatumNotToModify(ChatServiceContext chatServiceContext) {
return chatServiceContext.getContents()
.stream()
.filter(content -> !this.filterTags.stream().allMatch(tag -> content.getMetadata().containsKey(tag)))
.toList();
@@ -79,22 +79,22 @@ public class LastMaxTokenSizeContentTransformer implements PromptTransformer {
}
@Override
public PromptContext transform(PromptContext promptContext) {
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Content> datum = this.doGetDatumToModify(promptContext);
List<Content> datum = this.doGetDatumToModify(chatServiceContext);
int totalSize = this.doEstimateTokenCount(datum);
if (totalSize <= this.maxTokenSize) {
return promptContext;
return chatServiceContext;
}
List<Content> purgedContent = this.purgeExcess(datum, totalSize);
var updatedContent = new ArrayList<>(doGetDatumNotToModify(promptContext));
var updatedContent = new ArrayList<>(doGetDatumNotToModify(chatServiceContext));
updatedContent.addAll(purgedContent);
return PromptContext.from(promptContext).withContents(updatedContent).build();
return ChatServiceContext.from(chatServiceContext).withContents(updatedContent).build();
}
protected List<Content> purgeExcess(List<Content> datum, int totalSize) {

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.List;
@@ -26,22 +26,23 @@ import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.AbstractPromptTransformer;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.PromptChange;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
/**
* @author Christian Tzolov
*/
public class MessageChatMemoryAugmentor implements PromptTransformer {
public class MessageChatMemoryAugmentor extends AbstractPromptTransformer {
@Override
public PromptContext transform(PromptContext promptContext) {
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
var originalPrompt = promptContext.getPrompt();
var originalPrompt = chatServiceContext.getPrompt();
// Convert the retrieved contents into a list of messages.
List<Message> historyMessages = promptContext.getContents()
List<Message> historyMessages = chatServiceContext.getContents()
.stream()
.filter(content -> content.getMetadata().containsKey(TransformerContentType.MEMORY))
.map(content -> {
@@ -63,8 +64,10 @@ public class MessageChatMemoryAugmentor implements PromptTransformer {
promptMessages.addAll(originalPrompt.getInstructions());
Prompt newPrompt = new Prompt(promptMessages, (ChatOptions) originalPrompt.getOptions());
PromptChange promptChange = new PromptChange(originalPrompt, newPrompt, this.getName(),
"Added chat memory as individual messages in the prompt");
return PromptContext.from(promptContext).withPrompt(newPrompt).addPromptHistory(originalPrompt).build();
return ChatServiceContext.from(chatServiceContext).withPrompt(newPrompt).withPromptChange(promptChange).build();
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.HashSet;
@@ -28,15 +28,16 @@ import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.AbstractPromptTransformer;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.PromptChange;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import org.springframework.util.Assert;
/**
* @author Christian Tzolov
*/
public class SystemPromptChatMemoryAugmentor implements PromptTransformer {
public class SystemPromptChatMemoryAugmentor extends AbstractPromptTransformer {
public static final String DEFAULT_HISTORY_PROMPT = """
Use the conversation history from the HISTORY section to provide accurate answers.
@@ -72,9 +73,9 @@ public class SystemPromptChatMemoryAugmentor implements PromptTransformer {
}
@Override
public PromptContext transform(PromptContext promptContext) {
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
var originalPrompt = promptContext.getPrompt();
var originalPrompt = chatServiceContext.getPrompt();
List<Message> systemMessages = (originalPrompt.getInstructions() != null) ? originalPrompt.getInstructions()
.stream()
@@ -89,7 +90,7 @@ public class SystemPromptChatMemoryAugmentor implements PromptTransformer {
SystemMessage originalSystemMessage = (!systemMessages.isEmpty()) ? (SystemMessage) systemMessages.get(0)
: new SystemMessage("");
String historyContext = promptContext.getContents()
String historyContext = chatServiceContext.getContents()
.stream()
.filter(content -> this.filterTags.stream().allMatch(tag -> content.getMetadata().containsKey(tag)))
.map(content -> content.getMetadata().get(AbstractMessage.MESSAGE_TYPE) + ": " + content.getContent())
@@ -103,8 +104,9 @@ public class SystemPromptChatMemoryAugmentor implements PromptTransformer {
newPromptMessages.addAll(nonSystemMessages);
Prompt newPrompt = new Prompt(newPromptMessages, (ChatOptions) originalPrompt.getOptions());
return PromptContext.from(promptContext).withPrompt(newPrompt).addPromptHistory(originalPrompt).build();
PromptChange promptChange = new PromptChange(originalPrompt, newPrompt, this.getName(),
"Added chat memory into the system prompt");
return ChatServiceContext.from(chatServiceContext).withPrompt(newPrompt).withPromptChange(promptChange).build();
}
}

View File

@@ -14,18 +14,18 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.chatbot.ChatBotListener;
import org.springframework.ai.chat.chatbot.ChatBotResponse;
import org.springframework.ai.chat.service.ChatServiceListener;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.util.CollectionUtils;
@@ -33,43 +33,43 @@ import org.springframework.util.CollectionUtils;
/**
* @author Christian Tzolov
*/
public class VectorStoreChatMemoryChatBotListener implements ChatBotListener {
public class VectorStoreChatMemoryChatServiceListener implements ChatServiceListener {
private final VectorStore vectorStore;
private final Map<String, Object> additionalMetadata;
public VectorStoreChatMemoryChatBotListener(VectorStore vectorStore) {
public VectorStoreChatMemoryChatServiceListener(VectorStore vectorStore) {
this(vectorStore, new HashMap<>());
}
public VectorStoreChatMemoryChatBotListener(VectorStore vectorStore, Map<String, Object> additionalMetadata) {
public VectorStoreChatMemoryChatServiceListener(VectorStore vectorStore, Map<String, Object> additionalMetadata) {
this.vectorStore = vectorStore;
this.additionalMetadata = additionalMetadata;
}
@Override
public void onStart(PromptContext promptContext) {
public void onStart(ChatServiceContext chatServiceContext) {
if (!CollectionUtils.isEmpty(promptContext.getPrompt().getInstructions())) {
List<Document> docs = toDocuments(promptContext.getPrompt().getInstructions(),
promptContext.getConversationId());
if (!CollectionUtils.isEmpty(chatServiceContext.getPrompt().getInstructions())) {
List<Document> docs = toDocuments(chatServiceContext.getPrompt().getInstructions(),
chatServiceContext.getConversationId());
this.vectorStore.add(docs);
}
}
@Override
public void onComplete(ChatBotResponse chatBotResponse) {
if (!CollectionUtils.isEmpty(chatBotResponse.getChatResponse().getResults())) {
List<Message> assistantMessages = chatBotResponse.getChatResponse()
public void onComplete(ChatServiceResponse chatServiceResponse) {
if (!CollectionUtils.isEmpty(chatServiceResponse.getChatResponse().getResults())) {
List<Message> assistantMessages = chatServiceResponse.getChatResponse()
.getResults()
.stream()
.map(g -> (org.springframework.ai.chat.messages.Message) g.getOutput())
.toList();
List<Document> docs = toDocuments(assistantMessages,
chatBotResponse.getPromptContext().getConversationId());
chatServiceResponse.getPromptContext().getConversationId());
this.vectorStore.add(docs);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.ArrayList;
import java.util.List;
@@ -22,9 +22,9 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.AbstractPromptTransformer;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.Content;
import org.springframework.ai.vectorstore.SearchRequest;
@@ -34,7 +34,7 @@ import org.springframework.util.CollectionUtils;
/**
* @author Christian Tzolov
*/
public class VectorStoreChatMemoryRetriever implements PromptTransformer {
public class VectorStoreChatMemoryRetriever extends AbstractPromptTransformer {
private final VectorStore vectorStore;
@@ -56,11 +56,11 @@ public class VectorStoreChatMemoryRetriever implements PromptTransformer {
}
@Override
public PromptContext transform(PromptContext promptContext) {
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Content> updatedContents = new ArrayList<>(
promptContext.getContents() != null ? promptContext.getContents() : List.of());
chatServiceContext.getContents() != null ? chatServiceContext.getContents() : List.of());
String query = promptContext.getPrompt()
String query = chatServiceContext.getPrompt()
.getInstructions()
.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
@@ -70,7 +70,7 @@ public class VectorStoreChatMemoryRetriever implements PromptTransformer {
var searchRequest = SearchRequest.query(query)
.withTopK(this.topK)
.withFilterExpression(
TransformerContentType.CONVERSATION_ID + "=='" + promptContext.getConversationId() + "'");
TransformerContentType.CONVERSATION_ID + "=='" + chatServiceContext.getConversationId() + "'");
List<Document> documents = this.vectorStore.similaritySearch(searchRequest);
@@ -82,7 +82,7 @@ public class VectorStoreChatMemoryRetriever implements PromptTransformer {
updatedContents.addAll(documents);
}
return PromptContext.from(promptContext).withContents(updatedContents).build();
return ChatServiceContext.from(chatServiceContext).withContents(updatedContents).build();
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.chat.prompt.transformer;
/**
* AbstractPromptTransformer is an abstract class that provides a base implementation of
* the PromptTransformer interface. It includes a name field and corresponding accessor
* methods, as well as a default implementation for the transform method.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public abstract class AbstractPromptTransformer implements PromptTransformer {
private String name = getClass().getSimpleName();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,195 @@
/*
* 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.chat.prompt.transformer;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Content;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Represents the execution context for the {@link ChatService}. This context is used to
* pass initial parameters to the service and facilitate data sharing between different
* components within the service.
*
* <p>
* The {@code ChatServiceContext} includes essential information such as the initial
* prompt and a conversation ID, which are crucial for the correct operation of the chat
* service.
* </p>
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class ChatServiceContext {
private Prompt prompt; // The most up-to-date prompt to use
private List<Content> contents; // The most up-to-date data to use
private List<PromptChange> promptChanges; // The changes make due to transformations
private String conversationId;
/**
* Contextual data that can be shared between processing steps in a ChatService
* implementation.
*/
private Map<String, Object> context = new ConcurrentHashMap<>();
public ChatServiceContext(Prompt prompt) {
this(prompt, "default", new ArrayList<>());
}
public ChatServiceContext(Prompt prompt, String conversationId) {
this(prompt, conversationId, new ArrayList<>());
}
public ChatServiceContext(Prompt prompt, String conversationId, List<Content> contents) {
this.prompt = prompt;
this.conversationId = conversationId;
this.promptChanges = new ArrayList<>();
this.promptChanges.add(new PromptChange(null, prompt, "none", "initial prompt"));
this.contents = contents;
}
public Prompt getPrompt() {
return this.prompt;
}
public void updatePrompt(Prompt prompt, String transformerName, String description) {
this.promptChanges.add(new PromptChange(this.prompt, prompt, transformerName, description));
this.prompt = prompt; // set the new prompt as current
}
public void addData(Content datum) {
this.contents.add(datum);
}
public List<Content> getContents() {
return this.contents;
}
public void setContents(List<Content> contents) {
this.contents = contents;
}
public List<PromptChange> getPromptChanges() {
return this.promptChanges;
}
public String getConversationId() {
return this.conversationId;
}
public Map<String, Object> getContext() {
return this.context;
}
public static Builder from(ChatServiceContext chatServiceContext) {
return ChatServiceContext.builder()
.withContents(new ArrayList<>(
chatServiceContext.getContents() != null ? chatServiceContext.getContents() : List.of()))
.withPrompt(chatServiceContext.getPrompt().copy()) // deep copy
.withMetadata(
new HashMap<>(chatServiceContext.getContext() != null ? chatServiceContext.getContext() : Map.of()))
.withPromptChanges(new ArrayList<>(
chatServiceContext.getPromptChanges() != null ? chatServiceContext.getPromptChanges() : List.of()))
.withConversationId(chatServiceContext.getConversationId());
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Prompt prompt;
private List<Content> contents;
private List<PromptChange> promptChanges;
private String conversationId;
private Map<String, Object> context = new HashMap<>();
public Builder withPrompt(Prompt prompt) {
this.prompt = prompt;
return this;
}
public Builder withContents(List<Content> contents) {
this.contents = new ArrayList<>(contents);
return this;
}
public Builder withPromptChanges(List<PromptChange> promptChanges) {
this.promptChanges = new ArrayList<>(promptChanges);
return this;
}
public Builder withPromptChange(PromptChange promptChange) {
this.promptChanges.add(promptChange);
return this;
}
public Builder withConversationId(String conversationId) {
this.conversationId = conversationId;
return this;
}
public Builder withMetadata(Map<String, Object> context) {
this.context = new HashMap<>(context);
return this;
}
public ChatServiceContext build() {
ChatServiceContext chatServiceContext = new ChatServiceContext(this.prompt, this.conversationId,
this.contents);
chatServiceContext.promptChanges = promptChanges;
chatServiceContext.context = context;
return chatServiceContext;
}
}
@Override
public String toString() {
return "ChatServiceContext{" + "prompt=" + prompt + ", contents=" + contents + ", promptHistory="
+ promptChanges + ", conversationId='" + conversationId + '\'' + ", metadata=" + context + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof ChatServiceContext that))
return false;
return Objects.equals(prompt, that.prompt) && Objects.equals(contents, that.contents)
&& Objects.equals(promptChanges, that.promptChanges)
&& Objects.equals(conversationId, that.conversationId) && Objects.equals(context, that.context);
}
@Override
public int hashCode() {
return Objects.hash(prompt, contents, promptChanges, conversationId, context);
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.chat.prompt.transformer;
import org.springframework.ai.chat.prompt.Prompt;
/**
* The PromptChange class represents a change made to a Prompt object. It contains
* information about the original prompt, the revised prompt, the name of the transformer
* that made the change, and a description of the change.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public record PromptChange(Prompt original, Prompt revised, String transformerName, String description) {
}

View File

@@ -1,189 +0,0 @@
/*
* 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.chat.prompt.transformer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Content;
/**
* The shared, at the moment, mutable, data structure that can be used to implement
* ChatBot functionality.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0
*/
public class PromptContext {
private Prompt prompt; // The most up-to-date prompt to use
private List<Content> contents; // The most up-to-date data to use
private List<Prompt> promptHistory;
private String conversationId = "default";
private Map<String, Object> metadata = new HashMap<>();
public PromptContext(Prompt prompt) {
this(prompt, new ArrayList<>());
}
public PromptContext(Prompt prompt, String conversationId) {
this(prompt, new ArrayList<>());
this.conversationId = conversationId;
}
public PromptContext(Prompt prompt, List<Content> contents) {
this.prompt = prompt;
this.promptHistory = new ArrayList<>();
this.promptHistory.add(prompt);
this.contents = contents;
}
public Prompt getPrompt() {
return prompt;
}
public void setPrompt(Prompt prompt) {
this.prompt = prompt;
}
public void addData(Content datum) {
this.contents.add(datum);
}
public List<Content> getContents() {
return contents;
}
public void setContents(List<Content> contents) {
this.contents = contents;
}
public void addPromptHistory(Prompt prompt) {
this.promptHistory.add(prompt);
}
public List<Prompt> getPromptHistory() {
return promptHistory;
}
public String getConversationId() {
return conversationId;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public static Builder from(PromptContext promptContext) {
return PromptContext.builder()
.withContents(
new ArrayList<>(promptContext.getContents() != null ? promptContext.getContents() : List.of()))
.withPrompt(promptContext.getPrompt().copy()) // deep copy
.withMetadata(new HashMap<>(promptContext.getMetadata() != null ? promptContext.getMetadata() : Map.of()))
.withPromptHistory(new ArrayList<>(
promptContext.getPromptHistory() != null ? promptContext.getPromptHistory() : List.of()))
.withConversationId(promptContext.getConversationId());
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Prompt prompt;
private List<Content> contents;
private List<Prompt> promptHistory;
private String conversationId;
private Map<String, Object> metadata = new HashMap<>();
public Builder withPrompt(Prompt prompt) {
this.prompt = prompt;
return this;
}
public Builder withContents(List<Content> contents) {
this.contents = new ArrayList<>(contents);
return this;
}
public Builder withPromptHistory(List<Prompt> promptHistory) {
this.promptHistory = new ArrayList<>(promptHistory);
return this;
}
public Builder addPromptHistory(Prompt prompt) {
this.promptHistory.add(prompt);
return this;
}
public Builder withConversationId(String conversationId) {
this.conversationId = conversationId;
return this;
}
public Builder withMetadata(Map<String, Object> metadata) {
this.metadata = new HashMap<>(metadata);
return this;
}
public PromptContext build() {
PromptContext promptContext = new PromptContext(prompt, contents);
promptContext.promptHistory = promptHistory;
promptContext.conversationId = conversationId;
promptContext.metadata = metadata;
return promptContext;
}
}
@Override
public String toString() {
return "PromptContext{" + "prompt=" + prompt + ", contents=" + contents + ", promptHistory=" + promptHistory
+ ", conversationId='" + conversationId + '\'' + ", metadata=" + metadata + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof PromptContext that))
return false;
return Objects.equals(prompt, that.prompt) && Objects.equals(contents, that.contents)
&& Objects.equals(promptHistory, that.promptHistory)
&& Objects.equals(conversationId, that.conversationId) && Objects.equals(metadata, that.metadata);
}
@Override
public int hashCode() {
return Objects.hash(prompt, contents, promptHistory, conversationId, metadata);
}
}

View File

@@ -17,23 +17,24 @@
package org.springframework.ai.chat.prompt.transformer;
/**
* Responsible for transforming a Prompt. The PromptContext contains the necessary data to
* make the transformation
* Responsible for transforming a Prompt. The ChatServiceContext contains the necessary
* data to make the transformation
*
* Implementations may retrieve data and modify the Prompt object in the PromptContext as
* needed.
* Implementations may retrieve data and modify the Prompt object in the
* ChatServiceContext as needed.
*
* @author Mark Pollack
* @since 1.0 M1
* @author Christian Tzolov
* @since 1.0.0 M1
*/
@FunctionalInterface
public interface PromptTransformer {
/**
* Transforms the given PromptContext.
* @param context the PromptContext to transform
* @return the transformed PromptContext
* Transforms the given ChatServiceContext.
* @param context the ChatServiceContext to transform
* @return the transformed ChatServiceContext
*/
PromptContext transform(PromptContext context);
ChatServiceContext transform(ChatServiceContext context);
}

View File

@@ -32,11 +32,15 @@ import org.springframework.ai.model.Content;
* additional context to create a new prompt. The default user text contains the
* placeholder names "question" and "context". The "question" placeholder is filled using
* the value of the current UserMessage and the "context" placeholder is filled with
* Documents contained in the PromptContext's Nodes.
* Documents contained in the ChatServiceContext's Nodes.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class QuestionContextAugmentor implements PromptTransformer {
public class QuestionContextAugmentor extends AbstractPromptTransformer {
private static final String DEFAULT_USER_PROMPT_TEXT = """
private static final String DEFAULT_USER_TEXT = """
"Context information is below.\\n"
"---------------------\\n"
"{context}\\n"
@@ -48,16 +52,26 @@ public class QuestionContextAugmentor implements PromptTransformer {
"Answer: "
""";
private String userText;
public QuestionContextAugmentor() {
this.userText = DEFAULT_USER_TEXT;
this.setName("QuestionContextAugmentor");
}
public String getUserText() {
return userText;
}
@Override
public PromptContext transform(PromptContext promptContext) {
String context = doCreateContext(promptContext.getContents());
Map<String, Object> contextMap = doCreateContextMap(promptContext.getPrompt(), context);
Prompt prompt = doCreatePrompt(promptContext.getPrompt(), contextMap);
promptContext.setPrompt(prompt);
promptContext.addPromptHistory(prompt); // BUG? shouldn't this be original
// promptContext.getPrompt()?
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
String context = doCreateContext(chatServiceContext.getContents());
Map<String, Object> contextMap = doCreateContextMap(chatServiceContext.getPrompt(), context);
Prompt prompt = doCreatePrompt(chatServiceContext.getPrompt(), contextMap);
chatServiceContext.updatePrompt(prompt, this.getName(), "Updated prompt with Q/A user text");
// For now return the modified instance instead of a copy
return promptContext;
return chatServiceContext;
}
protected String doCreateContext(List<Content> data) {
@@ -75,7 +89,7 @@ public class QuestionContextAugmentor implements PromptTransformer {
}
protected Prompt doCreatePrompt(Prompt originalPrompt, Map<String, Object> contextMap) {
PromptTemplate promptTemplate = new PromptTemplate(DEFAULT_USER_PROMPT_TEXT);
PromptTemplate promptTemplate = new PromptTemplate(getUserText());
Message userMessageToAppend = promptTemplate.createMessage(contextMap);
List<Message> messageList = originalPrompt.getInstructions()
.stream()
@@ -85,4 +99,33 @@ public class QuestionContextAugmentor implements PromptTransformer {
return new Prompt(messageList, (ChatOptions) originalPrompt.getOptions());
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private String userText;
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withUserText(String userText) {
this.userText = userText;
return this;
}
public QuestionContextAugmentor build() {
QuestionContextAugmentor instance = new QuestionContextAugmentor();
instance.userText = this.userText != null ? this.userText : instance.userText;
instance.setName(this.name != null ? this.name : instance.getName());
return instance;
}
}
}

View File

@@ -17,7 +17,10 @@
package org.springframework.ai.chat.prompt.transformer;
/**
* This class provides constants for different content types used by transformers.
*
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class TransformerContentType {

View File

@@ -23,16 +23,29 @@ import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Transforms the PromptContext by retrieving documents from a VectorStore
* A transformer class that retrieves documents from a {@link VectorStore}
*
* <p>
* The {@code VectorStoreRetriever} leverages a {@link SearchRequest} to query the
* {@link VectorStore} and retrieve documents that are semantically similar to the user's
* input. These documents are then added to the {@link ChatServiceContext} for further
* processing.
* </p>
*
* @see VectorStore
* @see SearchRequest
* @see ChatServiceContext
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class VectorStoreRetriever implements PromptTransformer {
public class VectorStoreRetriever extends AbstractPromptTransformer {
private final Logger logger = LoggerFactory.getLogger(getClass());
@@ -41,8 +54,13 @@ public class VectorStoreRetriever implements PromptTransformer {
private final SearchRequest searchRequest;
public VectorStoreRetriever(VectorStore vectorStore, SearchRequest searchRequest) {
this(vectorStore, searchRequest, "VectorStoreRetriever");
}
public VectorStoreRetriever(VectorStore vectorStore, SearchRequest searchRequest, String name) {
this.vectorStore = vectorStore;
this.searchRequest = searchRequest;
this.setName(name);
}
public VectorStore getVectorStore() {
@@ -54,8 +72,8 @@ public class VectorStoreRetriever implements PromptTransformer {
}
@Override
public PromptContext transform(PromptContext promptContext) {
List<Message> instructions = promptContext.getPrompt().getInstructions();
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Message> instructions = chatServiceContext.getPrompt().getInstructions();
String userMessage = instructions.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(m -> m.getContent())
@@ -67,9 +85,9 @@ public class VectorStoreRetriever implements PromptTransformer {
for (Document document : documents) {
var content = new Document(document.getContent(), document.getMetadata());
// content.getMetadata().put(TransformerContentType.DOMAIN_DATA, true);
promptContext.addData(content);
chatServiceContext.addData(content);
}
return promptContext;
return chatServiceContext;
}
@Override

View File

@@ -14,27 +14,26 @@
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* A ChatBot encapsulates the logic to perform common AI use cases such as Retrieval
* Augmented Generation.
* A ChatService encapsulates the logic to implement AI use cases.
*
* @author Mark Pollack
* @since 1.0 M1
*/
public interface ChatBot {
public interface ChatService {
/**
* Call the chatbot to execute AI actions
* @param promptContext A shared data structure used by the ChatBot to perform
* processing of the Prompt. It includes the intial Prompt and a conversation ID at
* Call the service to execute AI actions
* @param chatServiceContext A data structure used by the ChatService to perform
* processing of the Prompt. It includes the initial Prompt and a conversation ID at
* the start of execution.
* @return the ChatBotResponse that contains the ChatResponse and the latest
* PromptContext
* @return the ChatServiceResponse that contains the ChatResponse and the latest
* ChatServiceContext
*/
ChatBotResponse call(PromptContext promptContext);
ChatServiceResponse call(ChatServiceContext chatServiceContext);
}

View File

@@ -14,23 +14,23 @@
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* The ChatBotListener is a callback interface that can be implemented by classes that
* want to be notified of the completion of a ChatBot execution.
* The ChatServiceListener is a callback interface that can be implemented by classes that
* want to be notified of the completion of a ChatService execution.
*
* @author Mark Pollack
* @author Christian Tzolov
*/
public interface ChatBotListener {
public interface ChatServiceListener {
default void onStart(PromptContext promptContext) {
default void onStart(ChatServiceContext chatServiceContext) {
}
void onComplete(ChatBotResponse chatBotResponse);
void onComplete(ChatServiceResponse chatServiceResponse);
}

View File

@@ -14,33 +14,33 @@
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import java.util.Objects;
/**
* Encapsulates the response from the ChatBot. Contains the most up-to-date PromptContext
* and the final ChatResponse
* Encapsulates the response from the ChatService. Contains the most up-to-date
* ChatServiceContext and the final ChatResponse
*
* @author Mark Pollack
* @since 1.0 M1
*/
public class ChatBotResponse {
public class ChatServiceResponse {
private final PromptContext promptContext;
private final ChatServiceContext chatServiceContext;
private final ChatResponse chatResponse;
public ChatBotResponse(PromptContext promptContext, ChatResponse chatResponse) {
this.promptContext = promptContext;
public ChatServiceResponse(ChatServiceContext chatServiceContext, ChatResponse chatResponse) {
this.chatServiceContext = chatServiceContext;
this.chatResponse = chatResponse;
}
public PromptContext getPromptContext() {
return promptContext;
public ChatServiceContext getPromptContext() {
return chatServiceContext;
}
public ChatResponse getChatResponse() {
@@ -49,21 +49,23 @@ public class ChatBotResponse {
@Override
public String toString() {
return "ChatBotResponse{" + "promptContext=" + promptContext + ", chatResponse=" + chatResponse + '}';
return "ChatServiceResponse{" + "chatServiceContext=" + chatServiceContext + ", chatResponse=" + chatResponse
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof ChatBotResponse that))
if (!(o instanceof ChatServiceResponse that))
return false;
return Objects.equals(promptContext, that.promptContext) && Objects.equals(chatResponse, that.chatResponse);
return Objects.equals(chatServiceContext, that.chatServiceContext)
&& Objects.equals(chatResponse, that.chatResponse);
}
@Override
public int hashCode() {
return Objects.hash(promptContext, chatResponse);
return Objects.hash(chatServiceContext, chatResponse);
}
}

View File

@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import java.util.ArrayList;
@@ -25,10 +25,15 @@ import java.util.List;
import java.util.Objects;
/**
* A PromptTransformingChatService implements the ChatService interface and performs
* transformation of the prompt using a series of PromptTransformers. It also provides a
* builder class for easier construction of the PromptTransformingChatService instance.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0 M1
*/
public class DefaultChatBot implements ChatBot {
public class PromptTransformingChatService implements ChatService {
private ChatClient chatClient;
@@ -38,60 +43,60 @@ public class DefaultChatBot implements ChatBot {
private List<PromptTransformer> augmentors;
private List<ChatBotListener> chatBotListeners;
private List<ChatServiceListener> chatServiceListeners;
public DefaultChatBot(ChatClient chatClient, List<PromptTransformer> retrievers,
public PromptTransformingChatService(ChatClient chatClient, List<PromptTransformer> retrievers,
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatBotListener> chatBotListeners) {
List<ChatServiceListener> chatServiceListeners) {
Objects.requireNonNull(chatClient, "chatClient must not be null");
this.chatClient = chatClient;
this.retrievers = retrievers;
this.documentPostProcessors = documentPostProcessors;
this.augmentors = augmentors;
this.chatBotListeners = chatBotListeners;
this.chatServiceListeners = chatServiceListeners;
}
public static DefaultChatBotBuilder builder(ChatClient chatClient) {
return new DefaultChatBotBuilder().withChatClient(chatClient);
public static Builder builder(ChatClient chatClient) {
return new Builder().withChatClient(chatClient);
}
@Override
public ChatBotResponse call(PromptContext promptContext) {
public ChatServiceResponse call(ChatServiceContext chatServiceContext) {
PromptContext promptContextOnStart = PromptContext.from(promptContext).build();
ChatServiceContext chatServiceContextOnStart = ChatServiceContext.from(chatServiceContext).build();
// Perform retrieval of documents and messages
for (PromptTransformer retriever : this.retrievers) {
promptContext = retriever.transform(promptContext);
chatServiceContext = retriever.transform(chatServiceContext);
}
// Perform post processing of all retrieved documents and messages
for (PromptTransformer documentPostProcessor : this.documentPostProcessors) {
promptContext = documentPostProcessor.transform(promptContext);
chatServiceContext = documentPostProcessor.transform(chatServiceContext);
}
// Perform prompt augmentation
for (PromptTransformer augmentor : this.augmentors) {
promptContext = augmentor.transform(promptContext);
chatServiceContext = augmentor.transform(chatServiceContext);
}
// Invoke Listeners onStart
for (ChatBotListener listener : this.chatBotListeners) {
listener.onStart(promptContextOnStart);
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onStart(chatServiceContextOnStart);
}
// Perform generation
ChatResponse chatResponse = this.chatClient.call(promptContext.getPrompt());
ChatResponse chatResponse = this.chatClient.call(chatServiceContext.getPrompt());
// Invoke Listeners onComplete
ChatBotResponse chatBotResponse = new ChatBotResponse(promptContext, chatResponse);
for (ChatBotListener listener : this.chatBotListeners) {
listener.onComplete(chatBotResponse);
ChatServiceResponse chatServiceResponse = new ChatServiceResponse(chatServiceContext, chatResponse);
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onComplete(chatServiceResponse);
}
return chatBotResponse;
return chatServiceResponse;
}
public static class DefaultChatBotBuilder {
public static class Builder {
private ChatClient chatClient;
@@ -101,35 +106,36 @@ public class DefaultChatBot implements ChatBot {
private List<PromptTransformer> augmentors = new ArrayList<>();
private List<ChatBotListener> chatBotListeners = new ArrayList<>();
private List<ChatServiceListener> chatServiceListeners = new ArrayList<>();
public DefaultChatBotBuilder withChatClient(ChatClient chatClient) {
public Builder withChatClient(ChatClient chatClient) {
this.chatClient = chatClient;
return this;
}
public DefaultChatBotBuilder withRetrievers(List<PromptTransformer> retrievers) {
public Builder withRetrievers(List<PromptTransformer> retrievers) {
this.retrievers = retrievers;
return this;
}
public DefaultChatBotBuilder withContentPostProcessors(List<PromptTransformer> documentPostProcessors) {
public Builder withContentPostProcessors(List<PromptTransformer> documentPostProcessors) {
this.documentPostProcessors = documentPostProcessors;
return this;
}
public DefaultChatBotBuilder withAugmentors(List<PromptTransformer> augmentors) {
public Builder withAugmentors(List<PromptTransformer> augmentors) {
this.augmentors = augmentors;
return this;
}
public DefaultChatBotBuilder withChatBotListeners(List<ChatBotListener> chatBotListeners) {
this.chatBotListeners = chatBotListeners;
public Builder withChatServiceListeners(List<ChatServiceListener> chatServiceListeners) {
this.chatServiceListeners = chatServiceListeners;
return this;
}
public DefaultChatBot build() {
return new DefaultChatBot(chatClient, retrievers, documentPostProcessors, augmentors, chatBotListeners);
public PromptTransformingChatService build() {
return new PromptTransformingChatService(chatClient, retrievers, documentPostProcessors, augmentors,
chatServiceListeners);
}
}

View File

@@ -13,28 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* A ChatBot encapsulates the logic to perform common AI use cases such as Retrieval
* Augmented Generation.
* A ChatService encapsulates the logic to implement AI use cases.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0 M1
*/
public interface StreamingChatBot {
public interface StreamingChatService {
/**
* Call the chatbot to execute AI actions
* @param promptContext A shared data structure used by the ChatBot to perform
* processing of the Prompt. It includes the intial Prompt and a conversation ID at
* the start of execution.
* @return the StreamingChatBotResponse that contains the ChatResponse and the latest
* PromptContext
* Call the service to execute AI actions
* @param chatServiceContext A shared data structure used by the ChatService to
* perform processing of the Prompt. It includes the intial Prompt and a conversation
* ID at the start of execution.
* @return the StreamingChatServiceResponse that contains the ChatResponse and the
* latest ChatServiceContext
*/
StreamingChatBotResponse stream(PromptContext promptContext);
StreamingChatServiceResponse stream(ChatServiceContext chatServiceContext);
}

View File

@@ -0,0 +1,73 @@
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
/**
* Encapsulates the response from the ChatService. Contains the most up-to-date
* ChatServiceContext and the final ChatResponse
*
* @author Mark Pollack
* @since 1.0 M1
*/
public class StreamingChatServiceResponse {
private final ChatServiceContext chatServiceContext;
private final Flux<ChatResponse> chatResponse;
public StreamingChatServiceResponse(ChatServiceContext chatServiceContext, Flux<ChatResponse> chatResponse) {
this.chatServiceContext = chatServiceContext;
this.chatResponse = chatResponse;
}
public ChatServiceContext getPromptContext() {
return chatServiceContext;
}
public Flux<ChatResponse> getChatResponse() {
return chatResponse;
}
@Override
public String toString() {
return "ChatServiceResponse{" + "chatServiceContext=" + chatServiceContext + ", chatResponse=" + chatResponse
+ '}';
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((chatServiceContext == null) ? 0 : chatServiceContext.hashCode());
result = prime * result + ((chatResponse == null) ? 0 : chatResponse.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
StreamingChatServiceResponse other = (StreamingChatServiceResponse) obj;
if (chatServiceContext == null) {
if (other.chatServiceContext != null)
return false;
}
else if (!chatServiceContext.equals(other.chatServiceContext))
return false;
if (chatResponse == null) {
if (other.chatResponse != null)
return false;
}
else if (!chatResponse.equals(other.chatResponse))
return false;
return true;
}
}

View File

@@ -13,25 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ai.chat.chatbot;
package org.springframework.ai.chat.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.StreamingChatClient;
import org.springframework.ai.chat.messages.MessageAggregator;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
/**
* @author Mark Pollack
* @author Christian Tzolov
*/
public class DefaultStreamingChatBot implements StreamingChatBot {
public class StreamingPromptTransformingChatService implements StreamingChatService {
private StreamingChatClient streamingChatClient;
@@ -41,63 +41,63 @@ public class DefaultStreamingChatBot implements StreamingChatBot {
private List<PromptTransformer> augmentors;
private List<ChatBotListener> chatBotListeners;
private List<ChatServiceListener> chatServiceListeners;
public DefaultStreamingChatBot(StreamingChatClient chatClient, List<PromptTransformer> retrievers,
public StreamingPromptTransformingChatService(StreamingChatClient chatClient, List<PromptTransformer> retrievers,
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatBotListener> chatBotListeners) {
List<ChatServiceListener> chatServiceListeners) {
Objects.requireNonNull(chatClient, "chatClient must not be null");
this.streamingChatClient = chatClient;
this.retrievers = retrievers;
this.documentPostProcessors = documentPostProcessors;
this.augmentors = augmentors;
this.chatBotListeners = chatBotListeners;
this.chatServiceListeners = chatServiceListeners;
}
public static DefaultChatBotBuilder builder(StreamingChatClient chatClient) {
return new DefaultChatBotBuilder().withChatClient(chatClient);
public static Builder builder(StreamingChatClient chatClient) {
return new Builder().withChatClient(chatClient);
}
@Override
public StreamingChatBotResponse stream(PromptContext promptContext) {
public StreamingChatServiceResponse stream(ChatServiceContext chatServiceContext) {
PromptContext promptContextOnStart = PromptContext.from(promptContext).build();
ChatServiceContext chatServiceContextOnStart = ChatServiceContext.from(chatServiceContext).build();
// Perform retrieval of documents and messages
for (PromptTransformer retriever : this.retrievers) {
promptContext = retriever.transform(promptContext);
chatServiceContext = retriever.transform(chatServiceContext);
}
// Perform post processing of all retrieved documents and messages
for (PromptTransformer documentPostProcessor : this.documentPostProcessors) {
promptContext = documentPostProcessor.transform(promptContext);
chatServiceContext = documentPostProcessor.transform(chatServiceContext);
}
// Perform prompt augmentation
for (PromptTransformer augmentor : this.augmentors) {
promptContext = augmentor.transform(promptContext);
chatServiceContext = augmentor.transform(chatServiceContext);
}
// Invoke Listeners onStart
for (ChatBotListener listener : this.chatBotListeners) {
listener.onStart(promptContextOnStart);
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onStart(chatServiceContextOnStart);
}
// Perform generation
final var promptContext2 = promptContext;
final var promptContext2 = chatServiceContext;
Flux<ChatResponse> fluxChatResponse = new MessageAggregator()
.aggregate(this.streamingChatClient.stream(promptContext.getPrompt()), chatResponse -> {
for (ChatBotListener listener : this.chatBotListeners) {
listener.onComplete(new ChatBotResponse(promptContext2, chatResponse));
.aggregate(this.streamingChatClient.stream(chatServiceContext.getPrompt()), chatResponse -> {
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onComplete(new ChatServiceResponse(promptContext2, chatResponse));
}
});
// Invoke Listeners onComplete
return new StreamingChatBotResponse(promptContext, fluxChatResponse);
return new StreamingChatServiceResponse(chatServiceContext, fluxChatResponse);
}
public static class DefaultChatBotBuilder {
public static class Builder {
private StreamingChatClient chatClient;
@@ -107,36 +107,36 @@ public class DefaultStreamingChatBot implements StreamingChatBot {
private List<PromptTransformer> augmentors = new ArrayList<>();
private List<ChatBotListener> chatBotListeners = new ArrayList<>();
private List<ChatServiceListener> chatServiceListeners = new ArrayList<>();
public DefaultChatBotBuilder withChatClient(StreamingChatClient chatClient) {
public Builder withChatClient(StreamingChatClient chatClient) {
this.chatClient = chatClient;
return this;
}
public DefaultChatBotBuilder withRetrievers(List<PromptTransformer> retrievers) {
public Builder withRetrievers(List<PromptTransformer> retrievers) {
this.retrievers = retrievers;
return this;
}
public DefaultChatBotBuilder withDocumentPostProcessors(List<PromptTransformer> documentPostProcessors) {
public Builder withDocumentPostProcessors(List<PromptTransformer> documentPostProcessors) {
this.documentPostProcessors = documentPostProcessors;
return this;
}
public DefaultChatBotBuilder withAugmentors(List<PromptTransformer> augmentors) {
public Builder withAugmentors(List<PromptTransformer> augmentors) {
this.augmentors = augmentors;
return this;
}
public DefaultChatBotBuilder withChatBotListeners(List<ChatBotListener> chatBotListeners) {
this.chatBotListeners = chatBotListeners;
public Builder withChatServiceListeners(List<ChatServiceListener> chatServiceListeners) {
this.chatServiceListeners = chatServiceListeners;
return this;
}
public DefaultStreamingChatBot build() {
return new DefaultStreamingChatBot(chatClient, retrievers, documentPostProcessors, augmentors,
chatBotListeners);
public StreamingPromptTransformingChatService build() {
return new StreamingPromptTransformingChatService(chatClient, retrievers, documentPostProcessors,
augmentors, chatServiceListeners);
}
}

View File

@@ -1,7 +1,7 @@
package org.springframework.ai.evaluation;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.chatbot.ChatBotResponse;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Content;
@@ -16,9 +16,9 @@ public class EvaluationRequest {
private final ChatResponse chatResponse;
public EvaluationRequest(ChatBotResponse chatBotResponse) {
this(chatBotResponse.getPromptContext().getPromptHistory().get(0),
chatBotResponse.getPromptContext().getContents(), chatBotResponse.getChatResponse());
public EvaluationRequest(ChatServiceResponse chatServiceResponse) {
this(chatServiceResponse.getPromptContext().getPromptChanges().get(0).revised(),
chatServiceResponse.getPromptContext().getContents(), chatServiceResponse.getChatResponse());
}
public EvaluationRequest(Prompt prompt, List<Content> dataList, ChatResponse chatResponse) {

View File

@@ -19,7 +19,7 @@ public interface Content {
/**
* Get the content of the message.
*/
String getContent();
String getContent(); // TODO consider getText
/**
* Get the media associated with the content.

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.ai.chat.history;
package org.springframework.ai.chat.memory;
import java.util.List;
@@ -29,12 +29,12 @@ import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.Generation;
import org.springframework.ai.chat.StreamingChatClient;
import org.springframework.ai.chat.chatbot.ChatBotResponse;
import org.springframework.ai.chat.chatbot.DefaultChatBot;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.service.PromptTransformingChatService;
import org.springframework.ai.chat.messages.Message;
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.ChatServiceContext;
import org.springframework.ai.model.Content;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
@@ -61,15 +61,15 @@ public class ChatMemoryTests {
ChatMemory chatHistory = new InMemoryChatMemory();
DefaultChatBot chatBot = DefaultChatBot.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
PromptTransformingChatService chatService = PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(ChatMemoryRetriever.builder().withChatHistory(chatHistory).build()))
.withContentPostProcessors(
List.of(new LastMaxTokenSizeContentTransformer(new JTokkitTokenCountEstimator(), 10)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
chatClientUserMessages(chatBot, chatHistory);
chatClientUserMessages(chatService, chatHistory);
}
@Test
@@ -77,32 +77,32 @@ public class ChatMemoryTests {
ChatMemory chatHistory = new InMemoryChatMemory();
DefaultChatBot chatBot = DefaultChatBot.builder(chatClient)
PromptTransformingChatService chatService = PromptTransformingChatService.builder(chatClient)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(
List.of(new LastMaxTokenSizeContentTransformer(new JTokkitTokenCountEstimator(), 10)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatBotListeners(List.of(new ChatMemoryChatBotListener(chatHistory)))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
chatClientUserMessages(chatBot, chatHistory);
chatClientUserMessages(chatService, chatHistory);
}
public void chatClientUserMessages(DefaultChatBot chatBot, ChatMemory chatHistory) {
public void chatClientUserMessages(PromptTransformingChatService chatService, ChatMemory chatHistory) {
when(chatClient.call(promptCaptor.capture()))
.thenReturn(new ChatResponse(List.of(new Generation("assistant:1"))))
.thenReturn(new ChatResponse(List.of(new Generation("assistant:2"))))
.thenReturn(new ChatResponse(List.of(new Generation("assistant:3"))));
var promptContext = PromptContext.builder()
var promptContext = ChatServiceContext.builder()
.withConversationId("test-session-id")
.withPrompt(new Prompt(
List.of(new UserMessage("user:1"), new UserMessage("user:2"), new UserMessage("user:3"),
new UserMessage("user:4"), new UserMessage("user:5"))))
.build();
ChatBotResponse response1 = chatBot.call(promptContext);
ChatServiceResponse response1 = chatService.call(promptContext);
assertThat(response1.getChatResponse().getResult().getOutput().getContent()).isEqualTo("assistant:1");
@@ -112,7 +112,7 @@ public class ChatMemoryTests {
List<Message> history = chatHistory.get("test-session-id", 1000);
assertThat(history).hasSize(6);
ChatBotResponse response2 = chatBot.call(PromptContext.builder()
ChatServiceResponse response2 = chatService.call(ChatServiceContext.builder()
.withConversationId("test-session-id")
.withPrompt(new Prompt(
List.of(new UserMessage("user:6"), new UserMessage("user:7"), new UserMessage("user:8"))))
@@ -129,7 +129,7 @@ public class ChatMemoryTests {
assertThat(contents.get(1).getContent()).isEqualTo("user:5");
assertThat(contents.get(2).getContent()).isEqualTo("assistant:1");
ChatBotResponse response3 = chatBot.call(PromptContext.builder()
ChatServiceResponse response3 = chatService.call(ChatServiceContext.builder()
.withConversationId("test-session-id")
.withPrompt(new Prompt(List.of(new UserMessage("user:9")))).build());
assertThat(response3.getChatResponse().getResult().getOutput().getContent()).isEqualTo("assistant:3");

View File

@@ -22,11 +22,11 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.chatbot.ChatBot;
import org.springframework.ai.chat.chatbot.StreamingChatBot;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.service.ChatService;
import org.springframework.ai.chat.service.StreamingChatService;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.PromptContext;
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,48 +39,49 @@ public class BaseMemoryTest {
protected RelevancyEvaluator relevancyEvaluator;
protected ChatBot chatBot;
protected ChatService chatService;
protected StreamingChatBot streamingChatBot;
protected StreamingChatService streamingChatService;
public BaseMemoryTest(RelevancyEvaluator relevancyEvaluator, ChatBot chatBot,
StreamingChatBot streamingChatClient) {
public BaseMemoryTest(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatClient) {
this.relevancyEvaluator = relevancyEvaluator;
this.chatBot = chatBot;
this.streamingChatBot = streamingChatClient;
this.chatService = chatService;
this.streamingChatService = streamingChatClient;
}
@Test
void memoryChatBot() {
void memoryChatService() {
var prompt = new Prompt(new UserMessage("my name John Vincent Atanasoff"));
PromptContext promptContext = new PromptContext(prompt);
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var chatBotResponse1 = this.chatBot.call(promptContext);
var chatServiceResponse1 = this.chatService.call(chatServiceContext);
logger.info("Response1: " + chatBotResponse1.getChatResponse().getResult().getOutput().getContent());
logger.info("Response1: " + chatServiceResponse1.getChatResponse().getResult().getOutput().getContent());
// response varies too much.
// assertThat(chatBotResponse1.getChatResponse().getResult().getOutput().getContent()).contains("John");
// assertThat(chatServiceResponse1.getChatResponse().getResult().getOutput().getContent()).contains("John");
var chatBotResponse2 = this.chatBot.call(new PromptContext(new Prompt(new String("What is my name?"))));
logger.info("Response2: " + chatBotResponse2.getChatResponse().getResult().getOutput().getContent());
assertThat(chatBotResponse2.getChatResponse().getResult().getOutput().getContent())
var chatServiceResponse2 = this.chatService
.call(new ChatServiceContext(new Prompt(new String("What is my name?"))));
logger.info("Response2: " + chatServiceResponse2.getChatResponse().getResult().getOutput().getContent());
assertThat(chatServiceResponse2.getChatResponse().getResult().getOutput().getContent())
.contains("John Vincent Atanasoff");
EvaluationResponse evaluationResponse = this.relevancyEvaluator
.evaluate(new EvaluationRequest(chatBotResponse2));
.evaluate(new EvaluationRequest(chatServiceResponse2));
logger.info("" + evaluationResponse);
}
@Test
void memoryStreamingChatBot() {
void memoryStreamingChatService() {
var prompt = new Prompt(new UserMessage("my name John Vincent Atanasoff"));
PromptContext promptContext = new PromptContext(prompt);
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var fluxChatBotResponse1 = this.streamingChatBot.stream(promptContext);
var fluxChatServiceResponse1 = this.streamingChatService.stream(chatServiceContext);
String chatBotResponse1 = fluxChatBotResponse1.getChatResponse()
String chatServiceResponse1 = fluxChatServiceResponse1.getChatResponse()
.collectList()
.block()
.stream()
@@ -88,13 +89,13 @@ public class BaseMemoryTest {
.map(response -> response.getResult().getOutput().getContent())
.collect(Collectors.joining());
logger.info("Response1: " + chatBotResponse1);
// response varies too much assertThat(chatBotResponse1).contains("John");
logger.info("Response1: " + chatServiceResponse1);
// response varies too much assertThat(chatServiceResponse1).contains("John");
var fluxChatBotResponse2 = this.streamingChatBot
.stream(new PromptContext(new Prompt(new String("What is my name?"))));
var fluxChatServiceResponse2 = this.streamingChatService
.stream(new ChatServiceContext(new Prompt(new String("What is my name?"))));
String chatBotResponse2 = fluxChatBotResponse2.getChatResponse()
String chatServiceResponse2 = fluxChatServiceResponse2.getChatResponse()
.collectList()
.block()
.stream()
@@ -102,8 +103,8 @@ public class BaseMemoryTest {
.map(response -> response.getResult().getOutput().getContent())
.collect(Collectors.joining());
logger.info("Response2: " + chatBotResponse2);
assertThat(chatBotResponse2).contains("John Vincent Atanasoff");
logger.info("Response2: " + chatServiceResponse2);
assertThat(chatServiceResponse2).contains("John Vincent Atanasoff");
}
}