Remove chat.service package and related classes
* This functionality is now provided using the advisors feature in ChatClient
This commit is contained in:
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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 }";
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user