Remove chat.service package and related classes

* This functionality is now provided using the advisors feature in ChatClient
This commit is contained in:
Mark Pollack
2024-05-27 15:36:30 -04:00
parent a64dd06c48
commit 574138a26a
45 changed files with 110 additions and 2937 deletions

View File

@@ -24,6 +24,8 @@ import org.springframework.ai.chat.metadata.Usage;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* {@link ChatResponseMetadata} implementation for {@literal AnthropicApi}.
*
@@ -33,7 +35,7 @@ import org.springframework.util.Assert;
* @see Usage
* @since 1.0.0
*/
public class AnthropicChatResponseMetadata implements ChatResponseMetadata {
public class AnthropicChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";

View File

@@ -22,6 +22,8 @@ import org.springframework.ai.chat.metadata.PromptMetadata;
import org.springframework.ai.chat.metadata.Usage;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* {@link ChatResponseMetadata} implementation for
* {@literal Microsoft Azure OpenAI Service}.
@@ -30,7 +32,7 @@ import org.springframework.util.Assert;
* @see ChatResponseMetadata
* @since 0.7.1
*/
public class AzureOpenAiChatResponseMetadata implements ChatResponseMetadata {
public class AzureOpenAiChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";

View File

@@ -20,13 +20,15 @@ import org.springframework.ai.chat.metadata.Usage;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* {@link ChatResponseMetadata} implementation for {@literal Ollama}
*
* @see ChatResponseMetadata
* @author Fu Cheng
*/
public class OllamaChatResponseMetadata implements ChatResponseMetadata {
public class OllamaChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, usage: %2$s, rateLimit: %3$s }";

View File

@@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.function.FunctionCallingOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ResponseFormat;
import org.springframework.ai.openai.api.OpenAiApi.FunctionTool;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@@ -178,6 +179,11 @@ public class OpenAiChatOptions implements FunctionCallingOptions, ChatOptions {
return this;
}
public Builder withModel(OpenAiApi.ChatModel openAiChatModel) {
this.options.model = openAiChatModel.getModelName();
return this;
}
public Builder withFrequencyPenalty(Float frequencyPenalty) {
this.options.frequencyPenalty = frequencyPenalty;
return this;

View File

@@ -128,12 +128,12 @@ public class OpenAiApi {
* Vision requests can now use JSON mode and function calling.
* Currently points to gpt-4-turbo-2024-04-09.
*/
GPT_4_0_TURBO("gpt-4-turbo"),
GPT_4_TURBO("gpt-4-turbo"),
/**
* GPT-4 Turbo with Vision model. Vision requests can now use JSON mode and function calling
*/
GPT_4_0_TURBO_2204_04_09("gpt-4-turbo-2024-04-09"),
GPT_4_TURBO_2204_04_09("gpt-4-turbo-2024-04-09"),
/**
* (New) GPT-4 Turbo - latest GPT-4 model intended to reduce cases

View File

@@ -24,6 +24,8 @@ import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* {@link ChatResponseMetadata} implementation for {@literal OpenAI}.
*
@@ -33,7 +35,7 @@ import org.springframework.util.Assert;
* @see Usage
* @since 0.7.0
*/
public class OpenAiChatResponseMetadata implements ChatResponseMetadata {
public class OpenAiChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";

View File

@@ -19,9 +19,10 @@ import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Objects;
public class OpenAiImageResponseMetadata implements ImageResponseMetadata {
public class OpenAiImageResponseMetadata extends HashMap<String, Object> implements ImageResponseMetadata {
private final Long created;

View File

@@ -23,13 +23,15 @@ import org.springframework.ai.openai.api.OpenAiAudioApi;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* Audio speech metadata implementation for {@literal OpenAI}.
*
* @author Ahmed Yousri
* @see RateLimit
*/
public class OpenAiAudioSpeechResponseMetadata implements ResponseMetadata {
public class OpenAiAudioSpeechResponseMetadata extends HashMap<String, Object> implements ResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, requestsLimit: %2$s }";

View File

@@ -23,6 +23,8 @@ import org.springframework.ai.openai.metadata.OpenAiRateLimit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.util.HashMap;
/**
* Audio transcription metadata implementation for {@literal OpenAI}.
*
@@ -30,7 +32,7 @@ import org.springframework.util.Assert;
* @since 0.8.1
* @see RateLimit
*/
public class OpenAiAudioTranscriptionResponseMetadata implements ResponseMetadata {
public class OpenAiAudioTranscriptionResponseMetadata extends HashMap<String, Object> implements ResponseMetadata {
protected static final String AI_METADATA_STRING = "{ @type: %1$s, rateLimit: %4$s }";

View File

@@ -198,7 +198,7 @@ public class OpenAiPaymentTransactionIT {
public OpenAiChatModel openAiClient(OpenAiApi openAiApi, FunctionCallbackContext functionCallbackContext) {
return new OpenAiChatModel(openAiApi,
OpenAiChatOptions.builder()
.withModel(ChatModel.GPT_4_0_TURBO.getModelName())
.withModel(ChatModel.GPT_4_TURBO.getModelName())
.withTemperature(0.1f)
.build(),
functionCallbackContext, RetryUtils.DEFAULT_RETRY_TEMPLATE);

View File

@@ -1,133 +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.openai.chat.service;
import java.util.List;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
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.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.EmbeddingModel;
import org.springframework.ai.evaluation.BaseMemoryTest;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@Disabled("ChatService Memory implementation are deprecated. No need to test them.")
@Testcontainers
@SpringBootTest(classes = ChatMemoryLongTermSystemPromptIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryLongTermSystemPromptIT extends BaseMemoryTest {
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
@Autowired
public ChatMemoryLongTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public EmbeddingModel embeddingModel(OpenAiApi openAiApi) {
return new OpenAiEmbeddingModel(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true);
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatService memoryChatService(OpenAiChatModel chatModel, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new VectorStoreChatMemoryChatServiceListener(vectorStore)))
.build();
}
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatModel streamingChatModel,
VectorStore vectorStore, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatModel)
.withRetrievers(List.of(new VectorStoreChatMemoryRetriever(vectorStore, 10)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new VectorStoreChatMemoryChatServiceListener(vectorStore)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatModel chatModel) {
return new RelevancyEvaluator(chatModel);
}
}
}

View File

@@ -1,109 +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.openai.chat.service;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
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.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@Disabled("ChatService Memory implementation are deprecated. No need to test them.")
@SpringBootTest(classes = ChatMemoryShortTermMessageListIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryShortTermMessageListIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermMessageListIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatService memoryChatService(OpenAiChatModel chatModel, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatModel streamingChatModel,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatModel chatModel) {
return new RelevancyEvaluator(chatModel);
}
}
}

View File

@@ -1,110 +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.openai.chat.service;
import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
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.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
@Disabled("ChatService Memory implementation are deprecated. No need to test them.")
@SpringBootTest(classes = ChatMemoryShortTermSystemPromptIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class ChatMemoryShortTermSystemPromptIT extends BaseMemoryTest {
@Autowired
public ChatMemoryShortTermSystemPromptIT(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatService) {
super(relevancyEvaluator, chatService, streamingChatService);
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatService memoryChatService(OpenAiChatModel chatModel, ChatMemory chatHistory,
TokenCountEstimator tokenCountEstimator) {
return PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public StreamingChatService memoryStreamingChatService(OpenAiChatModel streamingChatModel,
ChatMemory chatHistory, TokenCountEstimator tokenCountEstimator) {
return StreamingPromptTransformingChatService.builder(streamingChatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withDocumentPostProcessors(List.of(new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
}
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatModel chatModel) {
return new RelevancyEvaluator(chatModel);
}
}
}

View File

@@ -1,256 +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.openai.chat.service;
import java.util.List;
import java.util.Map;
import java.util.Set;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import org.junit.jupiter.api.Disabled;
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.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.springframework.ai.openai.OpenAiChatModel;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.qdrant.QdrantContainer;
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.QuestionContextAugmentor;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.VectorStoreRetriever;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentTransformer;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.ai.openai.api.OpenAiApi.ChatModel.GPT_4_TURBO_PREVIEW;
@Disabled("ChatService Memory implementation are deprecated. No need to test them.")
@Testcontainers
@SpringBootTest(classes = LongShortTermChatMemoryWithRagIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class LongShortTermChatMemoryWithRagIT {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
@Autowired
ChatService chatService;
@Autowired
RelevancyEvaluator relevancyEvaluator;
@Autowired
VectorStore vectorStore;
@Value("classpath:/data/acme/bikes.json")
private Resource bikesResource;
void loadData() {
var metadataEnricher = new DocumentTransformer() {
@Override
public List<Document> apply(List<Document> documents) {
documents.forEach(d -> {
Map<String, Object> metadata = d.getMetadata();
metadata.put(TransformerContentType.EXTERNAL_KNOWLEDGE, "true");
});
return documents;
}
};
JsonReader jsonReader = new JsonReader(bikesResource, "name", "price", "shortDescription", "description");
var textSplitter = new TokenTextSplitter();
vectorStore.accept(metadataEnricher.apply(textSplitter.apply(jsonReader.get())));
}
// @Autowired
// StreamingChatService streamingChatService;
@Test
void memoryChatService() {
loadData();
var prompt = new Prompt(new UserMessage("My name is Christian and I like mountain bikes."));
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var chatServiceResponse1 = this.chatService.call(chatServiceContext);
logger.info("Response1: " + chatServiceResponse1.getChatResponse().getResult().getOutput().getContent());
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: " + chatServiceResponse2.getChatResponse().getResult().getOutput().getContent());
// logger.info(chatServiceResponse2.getPromptContext().getContents().toString());
assertThat(chatServiceResponse2.getChatResponse().getResult().getOutput().getContent()).contains("Christian");
EvaluationResponse evaluationResponse = this.relevancyEvaluator
.evaluate(chatServiceResponse2.toEvaluationRequest());
assertTrue(evaluationResponse.isPass(), "Response is not relevant to the question");
}
@SpringBootConfiguration
static class Config {
@Bean
public ChatMemory chatHistory() {
return new InMemoryChatMemory();
}
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public OpenAiChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public OpenAiEmbeddingModel embeddingModel(OpenAiApi openAiApi) {
return new OpenAiEmbeddingModel(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true);
}
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public ChatService memoryChatService(OpenAiChatModel chatModel, VectorStore vectorStore,
TokenCountEstimator tokenCountEstimator, ChatMemory chatHistory) {
return PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults()),
ChatMemoryRetriever.builder()
.withChatHistory(chatHistory)
.withMetadata(Map.of(TransformerContentType.SHORT_TERM_MEMORY, ""))
.build(),
new VectorStoreChatMemoryRetriever(vectorStore, 10,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
.withContentPostProcessors(List.of(
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000,
Set.of(TransformerContentType.SHORT_TERM_MEMORY)),
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000,
Set.of(TransformerContentType.LONG_TERM_MEMORY)),
new LastMaxTokenSizeContentTransformer(tokenCountEstimator, 2000,
Set.of(TransformerContentType.EXTERNAL_KNOWLEDGE))))
.withAugmentors(List.of(new QuestionContextAugmentor(),
new SystemPromptChatMemoryAugmentor(
"""
Use the long term conversation history from the LONG TERM HISTORY section to provide accurate answers.
LONG TERM HISTORY:
{history}
""",
Set.of(TransformerContentType.LONG_TERM_MEMORY)),
new SystemPromptChatMemoryAugmentor(Set.of(TransformerContentType.SHORT_TERM_MEMORY))))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory),
new VectorStoreChatMemoryChatServiceListener(vectorStore,
Map.of(TransformerContentType.LONG_TERM_MEMORY, ""))))
.build();
}
// @Bean
// public StreamingChatService memoryStreamingChatAgent(OpenAiChatModel
// streamingChatModel,
// VectorStore vectorStore, TokenCountEstimator tokenCountEstimator, ChatHistory
// chatHistory) {
// return StreamingPromptTransformingChatService.builder(streamingChatModel)
// .withRetrievers(List.of(new ChatHistoryRetriever(chatHistory), new
// DocumentChatHistoryRetriever(vectorStore, 10)))
// .withDocumentPostProcessors(List.of(new
// LastMaxTokenSizeContentTransformer(tokenCountEstimator, 1000)))
// .withAugmentors(List.of(new TextChatHistoryAugmenter()))
// .withChatAgentListeners(List.of(new ChatHistoryAgentListener(chatHistory), new
// DocumentChatHistoryAgentListener(vectorStore)))
// .build();
// }
@Bean
public RelevancyEvaluator relevancyEvaluator(OpenAiChatModel chatModel) {
// Use GPT 4 as a better model for determining relevancy. gpt 3.5 makes basic
// mistakes
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder()
.withModel(GPT_4_TURBO_PREVIEW.getValue())
.build();
return new RelevancyEvaluator(chatModel, openAiChatOptions);
}
}
}

View File

@@ -1,175 +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.openai.chat.service;
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.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.QuestionContextAugmentor;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
import org.springframework.ai.chat.prompt.transformer.VectorStoreRetriever;
import org.springframework.ai.chat.service.ChatService;
import org.springframework.ai.chat.service.PromptTransformingChatService;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.evaluation.EvaluationResponse;
import org.springframework.ai.evaluation.RelevancyEvaluator;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.qdrant.QdrantContainer;
import java.util.List;
import java.util.function.Supplier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ai.openai.api.OpenAiApi.ChatModel.GPT_4_0_TURBO;
@Testcontainers
@SpringBootTest(classes = OpenAiPromptTransformingChatServiceIT.Config.class)
@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".+")
public class OpenAiPromptTransformingChatServiceIT {
private static final String COLLECTION_NAME = "test_collection";
private static final int QDRANT_GRPC_PORT = 6334;
@Container
static QdrantContainer qdrantContainer = new QdrantContainer("qdrant/qdrant:v1.9.2");
private final ChatModel chatModel;
private final VectorStore vectorStore;
@Value("classpath:/data/acme/bikes.json")
private Resource bikesResource;
private ChatService chatService;
@Autowired
public OpenAiPromptTransformingChatServiceIT(ChatModel chatModel, ChatService chatService,
VectorStore vectorStore) {
this.chatModel = chatModel;
this.chatService = chatService;
this.vectorStore = vectorStore;
}
@Test
void simpleChat() {
loadData();
String question = "What reliable road bike?";
var prompt = new Prompt(new UserMessage(question));
var chatServiceResponse = this.chatService.call(new ChatServiceContext(prompt));
String answer = chatServiceResponse.getChatResponse().getResult().getOutput().getContent();
assertThat(answer).containsAnyOf("Celerity", "Velocity")
.as("Answer does not include 'Celerity' or 'Velocity'. Answer = %s", answer);
// Use GPT 4 Turbo as a better model for determining relevancy.
OpenAiChatOptions openAiChatOptions = OpenAiChatOptions.builder().withModel(GPT_4_0_TURBO.getValue()).build();
var relevancyEvaluator = new RelevancyEvaluator(this.chatModel, openAiChatOptions);
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(chatServiceResponse.toEvaluationRequest());
assertThat(evaluationResponse.isPass())
.as("Response is not relevant to the question. Question = %s; Answer = %s", question, answer);
}
void loadData() {
JsonReader jsonReader = new JsonReader(bikesResource, "name", "price", "shortDescription", "description");
var textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.split(jsonReader.read());
for (Document splitDocument : splitDocuments) {
splitDocument.getMetadata().put(TransformerContentType.EXTERNAL_KNOWLEDGE, "true");
}
vectorStore.write(splitDocuments);
}
void loadData2() {
JsonReader jsonReader = null;
TokenTextSplitter tokenTextSplitter = null;
VectorStore vectorStore = null;
List<Document> documents = jsonReader.read();
List<Document> splitDocuments = tokenTextSplitter.split(documents);
vectorStore.write(splitDocuments);
// Now in java.util.Function style.
Supplier<List<Document>> docs = jsonReader::read;
}
@SpringBootConfiguration
static class Config {
@Bean
public OpenAiApi chatCompletionApi() {
return new OpenAiApi(System.getenv("OPENAI_API_KEY"));
}
@Bean
public ChatModel openAiClient(OpenAiApi openAiApi) {
return new OpenAiChatModel(openAiApi);
}
@Bean
public EmbeddingModel embeddingModel(OpenAiApi openAiApi) {
return new OpenAiEmbeddingModel(openAiApi);
}
@Bean
public VectorStore qdrantVectorStore(EmbeddingModel embeddingModel) {
QdrantClient qdrantClient = new QdrantClient(QdrantGrpcClient
.newBuilder(qdrantContainer.getHost(), qdrantContainer.getMappedPort(QDRANT_GRPC_PORT), false)
.build());
return new QdrantVectorStore(qdrantClient, COLLECTION_NAME, embeddingModel, true);
}
@Bean
public ChatService chatService(ChatModel chatModel, VectorStore vectorStore) {
return PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new VectorStoreRetriever(vectorStore, SearchRequest.defaults())))
.withAugmentors(List.of(new QuestionContextAugmentor()))
.build();
}
}
}

View File

@@ -18,11 +18,13 @@ package org.springframework.ai.vertexai.gemini.metadata;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.metadata.Usage;
import java.util.HashMap;
/**
* @author Christian Tzolov
* @since 0.8.1
*/
public class VertexAiChatResponseMetadata implements ChatResponseMetadata {
public class VertexAiChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
private final VertexAiUsage usage;

View File

@@ -23,11 +23,13 @@ import java.util.stream.Collectors;
import org.springframework.ai.chat.client.AdvisedRequest;
import org.springframework.ai.chat.client.RequestResponseAdvisor;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.model.Content;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
/**
* Context for the question is retrieved from a Vector Store and added to the prompt's
@@ -54,10 +56,22 @@ public class QuestionAnswerAdvisor implements RequestResponseAdvisor {
private final SearchRequest searchRequest;
public static String RETRIEVED_DOCUMENTS = "qa_retrieved_documents";
public QuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest) {
this(vectorStore, searchRequest, DEFAULT_USER_TEXT_ADVISE);
}
/**
* The QuestionAnswerAdvisor retrieves context information from a Vector Store and
* combines it with the user's text.
* @param vectorStore The vector store to use
* @param searchRequest The search request defined using the portable filter
* expression syntax
* @param userTextAdvise the user text to append to the existing user prompt. The text
* should contain a placeholder named "question_answer_context".
*
*/
public QuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest, String userTextAdvise) {
Assert.notNull(vectorStore, "The vectorStore must not be null!");
@@ -78,6 +92,8 @@ public class QuestionAnswerAdvisor implements RequestResponseAdvisor {
// 2. Search for similar documents in the vector store.
List<Document> documents = vectorStore.similaritySearch(searchRequest.withQuery(request.userText()));
context.put(RETRIEVED_DOCUMENTS, documents);
// 3. Create the context from the documents.
String documentContext = documents.stream()
.map(Content::getContent)
@@ -95,4 +111,18 @@ public class QuestionAnswerAdvisor implements RequestResponseAdvisor {
return advisedRequest;
}
@Override
public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
response.getMetadata().put(RETRIEVED_DOCUMENTS, context.get(RETRIEVED_DOCUMENTS));
return response;
}
@Override
public Flux<ChatResponse> adviseResponse(Flux<ChatResponse> fluxResponse, Map<String, Object> context) {
return fluxResponse.map(cr -> {
cr.getMetadata().put(RETRIEVED_DOCUMENTS, context.get(RETRIEVED_DOCUMENTS));
return cr;
});
}
}

View File

@@ -1,65 +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.memory;
import java.util.List;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.service.ChatServiceListener;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.TransformerContentType;
/**
* @deprecated Use the {@link MessageChatMemoryAdvisor} or {@link PromptChatMemoryAdvisor}
* instead.
* @author Christian Tzolov
*/
@Deprecated
public class ChatMemoryChatServiceListener implements ChatServiceListener {
private final ChatMemory chatHistory;
public ChatMemoryChatServiceListener(ChatMemory chatHistory) {
this.chatHistory = chatHistory;
}
@Override
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(chatServiceContext.getConversationId(), messagesToAdd);
}
@Override
public void onComplete(ChatServiceResponse chatServiceResponse) {
List<Message> assistantMessages = chatServiceResponse.getChatResponse()
.getResults()
.stream()
.map(g -> (Message) g.getOutput())
.toList();
this.chatHistory.add(chatServiceResponse.getPromptContext().getConversationId(), assistantMessages);
}
}

View File

@@ -1,118 +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.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;
/**
* @author Christian Tzolov
*/
public class ChatMemoryRetriever extends AbstractPromptTransformer {
private final ChatMemory chatHistory;
/**
* Additional metadata to be assigned to the retrieved history messages.
*/
private final Map<String, Object> metadata;
private final int maxHistorySize;
public ChatMemoryRetriever(ChatMemory chatHistory) {
this(chatHistory, 1000, Map.of(), "ChatMemoryRetriever");
}
public ChatMemoryRetriever(ChatMemory chatHistory, int maxHistorySize, Map<String, Object> metadata, String name) {
this.chatHistory = chatHistory;
this.metadata = metadata;
this.maxHistorySize = maxHistorySize;
this.setName(name);
}
@Override
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
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.metadata);
content.getMetadata().put(TransformerContentType.MEMORY, true);
return content;
}).toList() : List.of();
List<Content> updatedContents = new ArrayList<>(
chatServiceContext.getContents() != null ? chatServiceContext.getContents() : List.of());
updatedContents.addAll(historyContent);
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

@@ -1,125 +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.memory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.springframework.ai.chat.client.advisor.LastMaxTokenSizeContentPurger;
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;
/**
*
* Returns a new list of content (e.g list of messages of list of documents) that is a
* subset of the input list of contents and complies with the max token size constraint.
*
* The token estimator is used to estimate the token count of the datum.
*
* @deprecated Use the {@link LastMaxTokenSizeContentPurger} instead.
* @author Christian Tzolov
*/
@Deprecated
public class LastMaxTokenSizeContentTransformer extends AbstractPromptTransformer {
protected final TokenCountEstimator tokenCountEstimator;
protected final int maxTokenSize;
/**
* Only Content entries with the following metadata tags will be included in the
* history.
*/
private final Set<String> filterTags;
public LastMaxTokenSizeContentTransformer(TokenCountEstimator tokenCountEstimator, int maxTokenSize) {
this(tokenCountEstimator, maxTokenSize, Set.of());
}
public LastMaxTokenSizeContentTransformer(TokenCountEstimator tokenCountEstimator, int maxTokenSize,
Set<String> filterTags) {
this.tokenCountEstimator = tokenCountEstimator;
this.maxTokenSize = maxTokenSize;
this.filterTags = filterTags;
}
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(ChatServiceContext chatServiceContext) {
return chatServiceContext.getContents()
.stream()
.filter(content -> !this.filterTags.stream().allMatch(tag -> content.getMetadata().containsKey(tag)))
.toList();
}
protected int doEstimateTokenCount(Content datum) {
return this.tokenCountEstimator.estimate(datum);
}
protected int doEstimateTokenCount(List<Content> datum) {
return datum.stream().mapToInt(this::doEstimateTokenCount).sum();
}
@Override
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Content> datum = this.doGetDatumToModify(chatServiceContext);
int totalSize = this.doEstimateTokenCount(datum);
if (totalSize <= this.maxTokenSize) {
return chatServiceContext;
}
List<Content> purgedContent = this.purgeExcess(datum, totalSize);
var updatedContent = new ArrayList<>(doGetDatumNotToModify(chatServiceContext));
updatedContent.addAll(purgedContent);
return ChatServiceContext.from(chatServiceContext).withContents(updatedContent).build();
}
protected List<Content> purgeExcess(List<Content> datum, int totalSize) {
int index = 0;
List<Content> newList = new ArrayList<>();
while (index < datum.size() && totalSize > this.maxTokenSize) {
Content oldDatum = datum.get(index++);
int oldMessageTokenSize = this.doEstimateTokenCount(oldDatum);
totalSize = totalSize - oldMessageTokenSize;
}
if (index >= datum.size()) {
return List.of();
}
// add the rest of the messages.
newList.addAll(datum.subList(index, datum.size()));
return newList;
}
}

View File

@@ -1,76 +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.memory;
import java.util.ArrayList;
import java.util.List;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.messages.AbstractMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
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;
/**
* @deprecated Use the {@link MessageChatMemoryAdvisor} instead.
* @author Christian Tzolov
*/
@Deprecated
public class MessageChatMemoryAugmentor extends AbstractPromptTransformer {
@Override
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
var originalPrompt = chatServiceContext.getPrompt();
// Convert the retrieved contents into a list of messages.
List<Message> historyMessages = chatServiceContext.getContents()
.stream()
.filter(content -> content.getMetadata().containsKey(TransformerContentType.MEMORY))
.map(content -> {
MessageType messageType = MessageType
.valueOf("" + content.getMetadata().get(AbstractMessage.MESSAGE_TYPE));
Message message = null;
if (messageType == MessageType.ASSISTANT) {
message = new AssistantMessage(content.getContent(), content.getMetadata());
}
else if (messageType == MessageType.USER) {
message = new UserMessage(content.getContent(), List.of(), content.getMetadata());
}
return message;
})
.filter(m -> m != null)
.toList();
var promptMessages = new ArrayList<>(historyMessages);
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 ChatServiceContext.from(chatServiceContext).withPrompt(newPrompt).withPromptChange(promptChange).build();
}
}

View File

@@ -1,115 +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.memory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
import org.springframework.ai.chat.messages.AbstractMessage;
import org.springframework.ai.chat.messages.Message;
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.util.Assert;
/**
* @deprecated Use the {@link PromptChatMemoryAdvisor} instead.
* @author Christian Tzolov
*/
@Deprecated
public class SystemPromptChatMemoryAugmentor extends AbstractPromptTransformer {
public static final String DEFAULT_HISTORY_PROMPT = """
Use the conversation history from the HISTORY section to provide accurate answers.
HISTORY:
{history}
""";
private final String historyPrompt;
/**
* Only Content entries with the following metadata tags will be included in the
* history.
*/
private final Set<String> filterTags;
public SystemPromptChatMemoryAugmentor() {
this(DEFAULT_HISTORY_PROMPT, new HashSet<>());
}
public SystemPromptChatMemoryAugmentor(Set<String> filterTags) {
this(DEFAULT_HISTORY_PROMPT, filterTags);
}
public SystemPromptChatMemoryAugmentor(String historyPrompt, Set<String> metadataFilterTags) {
Assert.hasText(historyPrompt, "The historyPrompt must not be empty!");
Assert.notNull(metadataFilterTags, "The metadataFilterTags must not be null!");
this.historyPrompt = historyPrompt;
this.filterTags = new HashSet<>(metadataFilterTags);
// Always include the message history type tag.
this.filterTags.add(TransformerContentType.MEMORY);
}
@Override
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
var originalPrompt = chatServiceContext.getPrompt();
List<Message> systemMessages = (originalPrompt.getInstructions() != null) ? originalPrompt.getInstructions()
.stream()
.filter(m -> m.getMessageType() == MessageType.SYSTEM)
.toList() : List.of();
List<Message> nonSystemMessages = (originalPrompt.getInstructions() != null) ? originalPrompt.getInstructions()
.stream()
.filter(m -> m.getMessageType() != MessageType.SYSTEM)
.toList() : List.of();
SystemMessage originalSystemMessage = (!systemMessages.isEmpty()) ? (SystemMessage) systemMessages.get(0)
: new SystemMessage("");
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())
.collect(Collectors.joining(System.lineSeparator()));
SystemMessage newSystemMessage = new SystemMessage(originalSystemMessage.getContent() + System.lineSeparator()
+ this.historyPrompt.replace("{history}", historyContext));
List<Message> newPromptMessages = new ArrayList<>();
newPromptMessages.add(newSystemMessage);
newPromptMessages.addAll(nonSystemMessages);
Prompt newPrompt = new Prompt(newPromptMessages, (ChatOptions) originalPrompt.getOptions());
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

@@ -1,101 +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.memory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.ai.chat.service.ChatServiceListener;
import org.springframework.ai.chat.service.ChatServiceResponse;
import org.springframework.ai.chat.client.advisor.VectorStoreChatMemoryAdvisor;
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.ChatServiceContext;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.util.CollectionUtils;
/**
* @deprecated Use the {@link VectorStoreChatMemoryAdvisor} instead.
* @author Christian Tzolov
*/
@Deprecated
public class VectorStoreChatMemoryChatServiceListener implements ChatServiceListener {
private final VectorStore vectorStore;
private final Map<String, Object> additionalMetadata;
public VectorStoreChatMemoryChatServiceListener(VectorStore vectorStore) {
this(vectorStore, new HashMap<>());
}
public VectorStoreChatMemoryChatServiceListener(VectorStore vectorStore, Map<String, Object> additionalMetadata) {
this.vectorStore = vectorStore;
this.additionalMetadata = additionalMetadata;
}
@Override
public void onStart(ChatServiceContext chatServiceContext) {
if (!CollectionUtils.isEmpty(chatServiceContext.getPrompt().getInstructions())) {
List<Document> docs = toDocuments(chatServiceContext.getPrompt().getInstructions(),
chatServiceContext.getConversationId());
this.vectorStore.add(docs);
}
}
@Override
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,
chatServiceResponse.getPromptContext().getConversationId());
this.vectorStore.add(docs);
}
}
private List<Document> toDocuments(List<Message> messages, String conversationId) {
List<Document> docs = messages.stream()
.filter(m -> m.getMessageType() == MessageType.USER || m.getMessageType() == MessageType.ASSISTANT)
.map(message -> {
var metadata = new HashMap<>(message.getMetadata() != null ? message.getMetadata() : new HashMap<>());
metadata.putAll(this.additionalMetadata);
metadata.put(TransformerContentType.CONVERSATION_ID, conversationId);
metadata.put("messageType", message.getMessageType().name());
metadata.put(TransformerContentType.MEMORY, true);
metadata.put(TransformerContentType.LONG_TERM_MEMORY, true);
var doc = new Document(message.getContent(), metadata);
return doc;
})
.toList();
return docs;
}
}

View File

@@ -1,91 +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.memory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.advisor.VectorStoreChatMemoryAdvisor;
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 org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.util.CollectionUtils;
/**
* @deprecated Use the {@link VectorStoreChatMemoryAdvisor} instead.
* @author Christian Tzolov
*/
@Deprecated
public class VectorStoreChatMemoryRetriever extends AbstractPromptTransformer {
private final VectorStore vectorStore;
private final int topK;
/**
* Additional metadata to be assigned to the retrieved history messages.
*/
private final Map<String, Object> additionalMetadata;
public VectorStoreChatMemoryRetriever(VectorStore vectorStore, int topK) {
this(vectorStore, topK, Map.of());
}
public VectorStoreChatMemoryRetriever(VectorStore vectorStore, int topK, Map<String, Object> additionalMetadata) {
this.vectorStore = vectorStore;
this.topK = topK;
this.additionalMetadata = additionalMetadata;
}
@Override
public ChatServiceContext transform(ChatServiceContext chatServiceContext) {
List<Content> updatedContents = new ArrayList<>(
chatServiceContext.getContents() != null ? chatServiceContext.getContents() : List.of());
String query = chatServiceContext.getPrompt()
.getInstructions()
.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(m -> m.getContent())
.collect(Collectors.joining());
var searchRequest = SearchRequest.query(query)
.withTopK(this.topK)
.withFilterExpression(
TransformerContentType.CONVERSATION_ID + "=='" + chatServiceContext.getConversationId() + "'");
List<Document> documents = this.vectorStore.similaritySearch(searchRequest);
if (!CollectionUtils.isEmpty(documents)) {
documents.forEach(d -> {
d.getMetadata().putAll(this.additionalMetadata);
d.getMetadata().put(TransformerContentType.MEMORY, true);
});
updatedContents.addAll(documents);
}
return ChatServiceContext.from(chatServiceContext).withContents(updatedContents).build();
}
}

View File

@@ -17,6 +17,8 @@ package org.springframework.ai.chat.metadata;
import org.springframework.ai.model.ResponseMetadata;
import java.util.HashMap;
/**
* Abstract Data Type (ADT) modeling common AI provider metadata returned in an AI
* response.
@@ -26,8 +28,11 @@ import org.springframework.ai.model.ResponseMetadata;
*/
public interface ChatResponseMetadata extends ResponseMetadata {
ChatResponseMetadata NULL = new ChatResponseMetadata() {
};
static class DefaultChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
}
ChatResponseMetadata NULL = new DefaultChatResponseMetadata();
/**
* Returns AI provider specific metadata on rate limits.

View File

@@ -1,39 +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;
/**
* 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

@@ -1,200 +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 org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.service.ChatService;
import org.springframework.ai.model.Content;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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

@@ -1,31 +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 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,40 +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;
/**
* 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
* ChatServiceContext as needed.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
@FunctionalInterface
public interface PromptTransformer {
/**
* Transforms the given ChatServiceContext.
* @param context the ChatServiceContext to transform
* @return the transformed ChatServiceContext
*/
ChatServiceContext transform(ChatServiceContext context);
}

View File

@@ -1,135 +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.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.model.Content;
/**
* Transforms the Prompt by taking to the current prompt in the Prompt Context and adding
* 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 ChatServiceContext's Nodes.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class QuestionContextAugmentor extends AbstractPromptTransformer {
private static final String DEFAULT_USER_TEXT = """
"Context information is below.\\n"
"---------------------\\n"
"{context}\\n"
"---------------------\\n"
"Given the context and provided history information and not prior knowledge, "
"reply to the user comment. If the answer is not in the context, inform "
"the user that you can't answer the question.\\n"
"User comment: {question}\\n"
"Answer: "
""";
private String userText;
public QuestionContextAugmentor() {
this(DEFAULT_USER_TEXT);
}
public QuestionContextAugmentor(String userText) {
this.userText = userText;
this.setName("QuestionContextAugmentor");
}
public String getUserText() {
return userText;
}
@Override
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 chatServiceContext;
}
protected String doCreateContext(List<Content> data) {
return data.stream().map(Content::getContent).collect(Collectors.joining(System.lineSeparator()));
}
private Map<String, Object> doCreateContextMap(Prompt prompt, String context) {
String originalUserMessage = prompt.getInstructions()
.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(m -> m.getContent())
.collect(Collectors.joining(System.lineSeparator()));
return Map.of("context", context, "question", originalUserMessage);
}
protected Prompt doCreatePrompt(Prompt originalPrompt, Map<String, Object> contextMap) {
PromptTemplate promptTemplate = new PromptTemplate(getUserText());
Message userMessageToAppend = promptTemplate.createMessage(contextMap);
List<Message> messageList = originalPrompt.getInstructions()
.stream()
.filter(m -> m.getMessageType() != MessageType.USER)
.collect(Collectors.toList());
messageList.add(userMessageToAppend);
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

@@ -1,37 +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;
/**
* This class provides constants for different content types used by transformers.
*
* @author Christian Tzolov
* @since 1.0.0 M1
*/
public class TransformerContentType {
public static final String MEMORY = "MEMORY_TYPE";
public static final String LONG_TERM_MEMORY = "LONG_TERM_MEMORY_TYPE";
public static final String SHORT_TERM_MEMORY = "SHORT_TERM_MEMORY_TYPE";
public static final String CONVERSATION_ID = "conversationId";
public static final String EXTERNAL_KNOWLEDGE = "externalKnowledge";
}

View File

@@ -1,112 +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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.Message;
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 java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 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 extends AbstractPromptTransformer {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final VectorStore vectorStore;
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() {
return vectorStore;
}
public SearchRequest getSearchRequest() {
return searchRequest;
}
@Override
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())
.collect(Collectors.joining(System.lineSeparator()));
List<Document> documents = vectorStore.similaritySearch(searchRequest.withQuery(userMessage));
logger.info("Retrieved {} documents for user message {}", documents.size(), userMessage);
for (Document document : documents) {
var content = new Document(document.getContent(), document.getMetadata());
// content.getMetadata().put(TransformerContentType.DOMAIN_DATA, true);
chatServiceContext.addData(content);
}
return chatServiceContext;
}
@Override
public String toString() {
return "VectorStoreRetriever{" + "vectorStore=" + vectorStore + ", searchRequest=" + searchRequest + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof VectorStoreRetriever that))
return false;
return Objects.equals(vectorStore, that.vectorStore) && Objects.equals(searchRequest, that.searchRequest);
}
@Override
public int hashCode() {
return Objects.hash(vectorStore, searchRequest);
}
}

View File

@@ -1,39 +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.service;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* A ChatService encapsulates the logic to implement AI use cases.
*
* @author Mark Pollack
* @since 1.0 M1
*/
public interface ChatService {
/**
* 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 ChatServiceResponse that contains the ChatResponse and the latest
* ChatServiceContext
*/
ChatServiceResponse call(ChatServiceContext chatServiceContext);
}

View File

@@ -1,36 +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.service;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* 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 ChatServiceListener {
default void onStart(ChatServiceContext chatServiceContext) {
}
void onComplete(ChatServiceResponse chatServiceResponse);
}

View File

@@ -1,77 +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.service;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.evaluation.EvaluationRequest;
import java.util.Objects;
/**
* 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 ChatServiceResponse {
private final ChatServiceContext chatServiceContext;
private final ChatResponse chatResponse;
public ChatServiceResponse(ChatServiceContext chatServiceContext, ChatResponse chatResponse) {
this.chatServiceContext = chatServiceContext;
this.chatResponse = chatResponse;
}
public ChatServiceContext getPromptContext() {
return chatServiceContext;
}
public ChatResponse getChatResponse() {
return chatResponse;
}
public EvaluationRequest toEvaluationRequest() {
return new EvaluationRequest(getPromptContext().getPromptChanges().get(0).revised(),
getPromptContext().getContents(), getChatResponse());
}
@Override
public String toString() {
return "ChatServiceResponse{" + "chatServiceContext=" + chatServiceContext + ", chatResponse=" + chatResponse
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof ChatServiceResponse that))
return false;
return Objects.equals(chatServiceContext, that.chatServiceContext)
&& Objects.equals(chatResponse, that.chatResponse);
}
@Override
public int hashCode() {
return Objects.hash(chatServiceContext, chatResponse);
}
}

View File

@@ -1,143 +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.service;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
import java.util.ArrayList;
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 PromptTransformingChatService implements ChatService {
private ChatModel chatModel;
private List<PromptTransformer> retrievers;
private List<PromptTransformer> documentPostProcessors;
private List<PromptTransformer> augmentors;
private List<ChatServiceListener> chatServiceListeners;
public PromptTransformingChatService(ChatModel chatModel, List<PromptTransformer> retrievers,
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatServiceListener> chatServiceListeners) {
Objects.requireNonNull(chatModel, "chatModel must not be null");
this.chatModel = chatModel;
this.retrievers = retrievers;
this.documentPostProcessors = documentPostProcessors;
this.augmentors = augmentors;
this.chatServiceListeners = chatServiceListeners;
}
public static Builder builder(ChatModel chatModel) {
return new Builder().withChatModel(chatModel);
}
@Override
public ChatServiceResponse call(ChatServiceContext chatServiceContext) {
ChatServiceContext chatServiceContextOnStart = ChatServiceContext.from(chatServiceContext).build();
// Perform retrieval of documents and messages
for (PromptTransformer retriever : this.retrievers) {
chatServiceContext = retriever.transform(chatServiceContext);
}
// Perform post processing of all retrieved documents and messages
for (PromptTransformer documentPostProcessor : this.documentPostProcessors) {
chatServiceContext = documentPostProcessor.transform(chatServiceContext);
}
// Perform prompt augmentation
for (PromptTransformer augmentor : this.augmentors) {
chatServiceContext = augmentor.transform(chatServiceContext);
}
// Invoke Listeners onStart
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onStart(chatServiceContextOnStart);
}
// Perform generation
ChatResponse chatResponse = this.chatModel.call(chatServiceContext.getPrompt());
// Invoke Listeners onComplete
ChatServiceResponse chatServiceResponse = new ChatServiceResponse(chatServiceContext, chatResponse);
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onComplete(chatServiceResponse);
}
return chatServiceResponse;
}
public static class Builder {
private ChatModel chatModel;
private List<PromptTransformer> retrievers = new ArrayList<>();
private List<PromptTransformer> documentPostProcessors = new ArrayList<>();
private List<PromptTransformer> augmentors = new ArrayList<>();
private List<ChatServiceListener> chatServiceListeners = new ArrayList<>();
public Builder withChatModel(ChatModel chatModel) {
this.chatModel = chatModel;
return this;
}
public Builder withRetrievers(List<PromptTransformer> retrievers) {
this.retrievers = retrievers;
return this;
}
public Builder withContentPostProcessors(List<PromptTransformer> documentPostProcessors) {
this.documentPostProcessors = documentPostProcessors;
return this;
}
public Builder withAugmentors(List<PromptTransformer> augmentors) {
this.augmentors = augmentors;
return this;
}
public Builder withChatServiceListeners(List<ChatServiceListener> chatServiceListeners) {
this.chatServiceListeners = chatServiceListeners;
return this;
}
public PromptTransformingChatService build() {
return new PromptTransformingChatService(chatModel, retrievers, documentPostProcessors, augmentors,
chatServiceListeners);
}
}
}

View File

@@ -1,39 +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.service;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
/**
* A ChatService encapsulates the logic to implement AI use cases.
*
* @author Mark Pollack
* @author Christian Tzolov
* @since 1.0 M1
*/
public interface StreamingChatService {
/**
* 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
*/
StreamingChatServiceResponse stream(ChatServiceContext chatServiceContext);
}

View File

@@ -1,73 +0,0 @@
package org.springframework.ai.chat.service;
import org.springframework.ai.chat.prompt.transformer.ChatServiceContext;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.model.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

@@ -1,144 +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.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.model.ChatResponse;
import org.springframework.ai.chat.model.MessageAggregator;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.transformer.PromptTransformer;
/**
* @author Mark Pollack
* @author Christian Tzolov
*/
public class StreamingPromptTransformingChatService implements StreamingChatService {
private StreamingChatModel streamingChatModel;
private List<PromptTransformer> retrievers;
private List<PromptTransformer> documentPostProcessors;
private List<PromptTransformer> augmentors;
private List<ChatServiceListener> chatServiceListeners;
public StreamingPromptTransformingChatService(StreamingChatModel chatModel, List<PromptTransformer> retrievers,
List<PromptTransformer> documentPostProcessors, List<PromptTransformer> augmentors,
List<ChatServiceListener> chatServiceListeners) {
Objects.requireNonNull(chatModel, "chatModel must not be null");
this.streamingChatModel = chatModel;
this.retrievers = retrievers;
this.documentPostProcessors = documentPostProcessors;
this.augmentors = augmentors;
this.chatServiceListeners = chatServiceListeners;
}
public static Builder builder(StreamingChatModel chatModel) {
return new Builder().withChatModel(chatModel);
}
@Override
public StreamingChatServiceResponse stream(ChatServiceContext chatServiceContext) {
ChatServiceContext chatServiceContextOnStart = ChatServiceContext.from(chatServiceContext).build();
// Perform retrieval of documents and messages
for (PromptTransformer retriever : this.retrievers) {
chatServiceContext = retriever.transform(chatServiceContext);
}
// Perform post processing of all retrieved documents and messages
for (PromptTransformer documentPostProcessor : this.documentPostProcessors) {
chatServiceContext = documentPostProcessor.transform(chatServiceContext);
}
// Perform prompt augmentation
for (PromptTransformer augmentor : this.augmentors) {
chatServiceContext = augmentor.transform(chatServiceContext);
}
// Invoke Listeners onStart
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onStart(chatServiceContextOnStart);
}
// Perform generation
final var promptContext2 = chatServiceContext;
Flux<ChatResponse> fluxChatResponse = new MessageAggregator()
.aggregate(this.streamingChatModel.stream(chatServiceContext.getPrompt()), chatResponse -> {
for (ChatServiceListener listener : this.chatServiceListeners) {
listener.onComplete(new ChatServiceResponse(promptContext2, chatResponse));
}
});
// Invoke Listeners onComplete
return new StreamingChatServiceResponse(chatServiceContext, fluxChatResponse);
}
public static class Builder {
private StreamingChatModel chatModel;
private List<PromptTransformer> retrievers = new ArrayList<>();
private List<PromptTransformer> documentPostProcessors = new ArrayList<>();
private List<PromptTransformer> augmentors = new ArrayList<>();
private List<ChatServiceListener> chatServiceListeners = new ArrayList<>();
public Builder withChatModel(StreamingChatModel chatModel) {
this.chatModel = chatModel;
return this;
}
public Builder withRetrievers(List<PromptTransformer> retrievers) {
this.retrievers = retrievers;
return this;
}
public Builder withDocumentPostProcessors(List<PromptTransformer> documentPostProcessors) {
this.documentPostProcessors = documentPostProcessors;
return this;
}
public Builder withAugmentors(List<PromptTransformer> augmentors) {
this.augmentors = augmentors;
return this;
}
public Builder withChatServiceListeners(List<ChatServiceListener> chatServiceListeners) {
this.chatServiceListeners = chatServiceListeners;
return this;
}
public StreamingPromptTransformingChatService build() {
return new StreamingPromptTransformingChatService(chatModel, retrievers, documentPostProcessors, augmentors,
chatServiceListeners);
}
}
}

View File

@@ -1,28 +1,35 @@
package org.springframework.ai.evaluation;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.Content;
import java.util.List;
import java.util.Objects;
/**
* Represents an evaluation request, which includes the user's text, a list of content
* data, and a chat response. The evaluation request is used to evaluate the relevance or
* correctness of the chat response based on the context.
*
* @author Mark Pollack
* @since 1.0.0 M1
*/
public class EvaluationRequest {
private final Prompt prompt;
private final String userText;
private final List<Content> dataList;
private final ChatResponse chatResponse;
public EvaluationRequest(Prompt prompt, List<Content> dataList, ChatResponse chatResponse) {
this.prompt = prompt;
public EvaluationRequest(String userText, List<Content> dataList, ChatResponse chatResponse) {
this.userText = userText;
this.dataList = dataList;
this.chatResponse = chatResponse;
}
public Prompt getPrompt() {
return prompt;
public String getUserText() {
return this.userText;
}
public List<Content> getDataList() {
@@ -35,8 +42,8 @@ public class EvaluationRequest {
@Override
public String toString() {
return "EvaluationRequest{" + "prompt=" + prompt + ", dataList=" + dataList + ", chatResponse=" + chatResponse
+ '}';
return "EvaluationRequest{" + "userText='" + userText + '\'' + ", dataList=" + dataList + ", chatResponse="
+ chatResponse + '}';
}
@Override
@@ -45,13 +52,13 @@ public class EvaluationRequest {
return true;
if (!(o instanceof EvaluationRequest that))
return false;
return Objects.equals(prompt, that.prompt) && Objects.equals(dataList, that.dataList)
return Objects.equals(userText, that.userText) && Objects.equals(dataList, that.dataList)
&& Objects.equals(chatResponse, that.chatResponse);
}
@Override
public int hashCode() {
return Objects.hash(prompt, dataList, chatResponse);
return Objects.hash(userText, dataList, chatResponse);
}
}

View File

@@ -1,18 +1,10 @@
package org.springframework.ai.evaluation;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.model.Content;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class RelevancyEvaluator implements Evaluator {
@@ -29,32 +21,27 @@ public class RelevancyEvaluator implements Evaluator {
Answer: "
""";
private final ChatOptions chatOptions;
private final ChatClient.Builder chatClientBuilder;
private ChatModel chatModel;
public RelevancyEvaluator(ChatModel chatModel) {
this(chatModel, ChatOptionsBuilder.builder().build());
}
public RelevancyEvaluator(ChatModel chatModel, ChatOptions chatOptions) {
this.chatModel = chatModel;
this.chatOptions = chatOptions;
public RelevancyEvaluator(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
}
@Override
public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
var query = doGetUserQuestion(evaluationRequest);
var response = doGetResponse(evaluationRequest);
var context = doGetSupportingData(evaluationRequest);
var promptTemplate = new PromptTemplate(DEFAULT_EVALUATION_PROMPT_TEXT);
Message message = promptTemplate
.createMessage(Map.of("query", query, "response", response, "context", context));
String evaluationResponse = this.chatClientBuilder.build()
.prompt()
.user(userSpec -> userSpec.text(DEFAULT_EVALUATION_PROMPT_TEXT)
.param("query", evaluationRequest.getUserText())
.param("response", response)
.param("context", context))
.call()
.content();
ChatResponse chatResponse = this.chatModel.call(new Prompt(message, this.chatOptions));
var evaluationResponse = chatResponse.getResult().getOutput().getContent();
boolean passing = false;
float score = 0;
if (evaluationResponse.toLowerCase().contains("yes")) {
@@ -79,13 +66,4 @@ public class RelevancyEvaluator implements Evaluator {
return supportingData;
}
protected String doGetUserQuestion(EvaluationRequest evaluationRequest) {
List<Message> instructions = evaluationRequest.getPrompt().getInstructions();
String userMessage = instructions.stream()
.filter(m -> m.getMessageType() == MessageType.USER)
.map(m -> m.getContent())
.collect(Collectors.joining(System.lineSeparator()));
return userMessage;
}
}

View File

@@ -15,12 +15,18 @@
*/
package org.springframework.ai.image;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.model.ResponseMetadata;
import java.util.HashMap;
public interface ImageResponseMetadata extends ResponseMetadata {
ImageResponseMetadata NULL = new ImageResponseMetadata() {
};
static class DefaultImageResponseMetadata extends HashMap<String, Object> implements ImageResponseMetadata {
}
ImageResponseMetadata NULL = new DefaultImageResponseMetadata();
default Long getCreated() {
return System.currentTimeMillis();

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.ai.model;
import java.util.Map;
/**
* Interface representing metadata associated with an AI model's response. This interface
* is designed to provide additional information about the generative response from an AI
@@ -25,6 +27,6 @@ package org.springframework.ai.model;
* @author Mark Pollack
* @since 0.8.0
*/
public interface ResponseMetadata {
public interface ResponseMetadata extends Map<String, Object> {
}

View File

@@ -1,147 +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.memory;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.model.StreamingChatModel;
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.ChatServiceContext;
import org.springframework.ai.model.Content;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
/**
* @author Christian Tzolov
*/
@ExtendWith(MockitoExtension.class)
public class ChatMemoryTests {
@Mock
ChatModel chatModel;
@Mock
StreamingChatModel streamingChatModel;
@Captor
ArgumentCaptor<Prompt> promptCaptor;
@Test
public void chatMemoryMessageListAugmentor() {
ChatMemory chatHistory = new InMemoryChatMemory();
PromptTransformingChatService chatService = PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(ChatMemoryRetriever.builder().withChatHistory(chatHistory).build()))
.withContentPostProcessors(
List.of(new LastMaxTokenSizeContentTransformer(new JTokkitTokenCountEstimator(), 10)))
.withAugmentors(List.of(new MessageChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
chatModelUserMessages(chatService, chatHistory);
}
@Test
public void chatMemorySystemPromptAugmentor() {
ChatMemory chatHistory = new InMemoryChatMemory();
PromptTransformingChatService chatService = PromptTransformingChatService.builder(chatModel)
.withRetrievers(List.of(new ChatMemoryRetriever(chatHistory)))
.withContentPostProcessors(
List.of(new LastMaxTokenSizeContentTransformer(new JTokkitTokenCountEstimator(), 10)))
.withAugmentors(List.of(new SystemPromptChatMemoryAugmentor()))
.withChatServiceListeners(List.of(new ChatMemoryChatServiceListener(chatHistory)))
.build();
chatModelUserMessages(chatService, chatHistory);
}
public void chatModelUserMessages(PromptTransformingChatService chatService, ChatMemory chatHistory) {
when(chatModel.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 = 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();
ChatServiceResponse response1 = chatService.call(promptContext);
assertThat(response1.getChatResponse().getResult().getOutput().getContent()).isEqualTo("assistant:1");
List<Content> contents = response1.getPromptContext().getContents();
assertThat(contents).hasSize(0);
List<Message> history = chatHistory.get("test-session-id", 1000);
assertThat(history).hasSize(6);
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"))))
.build());
assertThat(response2.getChatResponse().getResult().getOutput().getContent()).isEqualTo("assistant:2");
history = chatHistory.get("test-session-id", 1000);
assertThat(history).hasSize(10);
contents = response2.getPromptContext().getContents();
assertThat(contents).hasSize(3);
assertThat(contents.get(0).getContent()).isEqualTo("user:4");
assertThat(contents.get(1).getContent()).isEqualTo("user:5");
assertThat(contents.get(2).getContent()).isEqualTo("assistant:1");
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");
history = chatHistory.get("test-session-id", 1000);
assertThat(history).hasSize(12);
contents = response3.getPromptContext().getContents();
assertThat(contents).hasSize(3);
assertThat(contents.get(0).getContent()).isEqualTo("user:7");
assertThat(contents.get(1).getContent()).isEqualTo("user:8");
assertThat(contents.get(2).getContent()).isEqualTo("assistant:2");
}
}

View File

@@ -1,110 +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.evaluation;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
public class BaseMemoryTest {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected RelevancyEvaluator relevancyEvaluator;
protected ChatService chatService;
protected StreamingChatService streamingChatService;
public BaseMemoryTest(RelevancyEvaluator relevancyEvaluator, ChatService chatService,
StreamingChatService streamingChatModel) {
this.relevancyEvaluator = relevancyEvaluator;
this.chatService = chatService;
this.streamingChatService = streamingChatModel;
}
@Test
void memoryChatService() {
var prompt = new Prompt(new UserMessage("my name John Vincent Atanasoff"));
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var chatServiceResponse1 = this.chatService.call(chatServiceContext);
logger.info("Response1: " + chatServiceResponse1.getChatResponse().getResult().getOutput().getContent());
// response varies too much.
// assertThat(chatServiceResponse1.getChatResponse().getResult().getOutput().getContent()).contains("John");
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(chatServiceResponse2.toEvaluationRequest());
logger.info("" + evaluationResponse);
}
@Test
void memoryStreamingChatService() {
var prompt = new Prompt(new UserMessage("my name John Vincent Atanasoff"));
ChatServiceContext chatServiceContext = new ChatServiceContext(prompt);
var fluxChatServiceResponse1 = this.streamingChatService.stream(chatServiceContext);
String chatServiceResponse1 = fluxChatServiceResponse1.getChatResponse()
.collectList()
.block()
.stream()
.filter(response -> response.getResult() != null)
.map(response -> response.getResult().getOutput().getContent())
.collect(Collectors.joining());
logger.info("Response1: " + chatServiceResponse1);
// response varies too much assertThat(chatServiceResponse1).contains("John");
var fluxChatServiceResponse2 = this.streamingChatService
.stream(new ChatServiceContext(new Prompt(new String("What is my name?"))));
String chatServiceResponse2 = fluxChatServiceResponse2.getChatResponse()
.collectList()
.block()
.stream()
.filter(response -> response.getResult() != null)
.map(response -> response.getResult().getOutput().getContent())
.collect(Collectors.joining());
logger.info("Response2: " + chatServiceResponse2);
assertThat(chatServiceResponse2).contains("John Vincent Atanasoff");
}
}