Refactor advisor architecture in Spring AI

This commit introduces a major overhaul of the advisor system in Spring AI,
improving modularity, type safety, and consistency

Core Changes:
- Replace RequestAdvisor and ResponseAdvisor with CallAroundAdvisor and StreamAroundAdvisor
- Introduce AdvisedRequest and AdvisedResponse classes for better encapsulation
- Deprecate RequestResponseAdvisor in favor of new advisor types
- Remove AdvisorObservableHelper class

Advisor Implementation Updates:
- Update AbstractChatMemoryAdvisor, MessageChatMemoryAdvisor, PromptChatMemoryAdvisor,
QuestionAnswerAdvisor, SafeGuardAroundAdvisor, SimpleLoggerAdvisor, and
VectorStoreChatMemoryAdvisor to implement new advisor interfaces
- Remove CacheAroundAdvisor (functionality likely moved elsewhere)
- Make CallAroundAdvisor and StreamAroundAdvisor extend Ordered interface

Client and Chain Management:
- Modify DefaultChatClient to use new advisor chain approach
- Refactor DefaultAroundAdvisorChain for better ordering and observation
- Implement builder pattern for advisor chain construction in DefaultChatClient
- Separate call and stream advisors in DefaultAroundAdvisorChain

Observation and Context Handling:
- Update observation conventions and context handling in advisors
- Add order field to AdvisorObservationContext
- Modify DefaultAdvisorObservationConvention to include order in high cardinality key values

Testing and Integration:
- Refactor ChatClientAdvisorTests and add new AdvisorsTests
- Update integration tests to reflect new advisor structure
- Enhance AdvisorsTests to verify correct advisor execution order

New Features:
- Generalize the Protect From Blocking functionality across all advisors
- Add (experimental) Re2 advisor to enhance reasoning capabilities of LLMs
- Add disabled Re2 test in OpenAiChatClientIT

Documentation:
- Add Advisors documentation
- Enhance advisors documentation with order explanation and Re2 example

Advisor Ordering:
- Introduce Advisor constants for precedence ordering
- Update AbstractChatMemoryAdvisor to use new precedence constant
- Improve advisor ordering and management in DefaultAroundAdvisorChain.Builder
- Remove redundant reordering logic from DefaultAroundAdvisorChain

These changes aim to provide a more flexible and powerful advisor system,
allowing for easier implementation of complex AI-driven interactions

Co-authored-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
Christian Tzolov
2024-09-05 18:39:54 +02:00
committed by Mark Pollack
parent c81972ec45
commit 6fc76b7f9b
44 changed files with 1795 additions and 1051 deletions

View File

@@ -28,11 +28,11 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.AdvisedRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.RequestAdvisor;
import org.springframework.ai.chat.client.advisor.api.ResponseAdvisor;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.openai.OpenAiChatModel;
@@ -65,7 +65,7 @@ public class OpenAiPaymentTransactionIT {
record TransactionStatusResponse(String id, String status) {
}
private static class LoggingAdvisor implements RequestAdvisor, ResponseAdvisor {
private static class LoggingAdvisor implements CallAroundAdvisor {
private final Logger logger = LoggerFactory.getLogger(LoggingAdvisor.class);
@@ -74,7 +74,23 @@ public class OpenAiPaymentTransactionIT {
}
@Override
public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
public int getOrder() {
return 0;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
advisedRequest = this.before(advisedRequest);
AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);
this.observeAfter(advisedResponse);
return advisedResponse;
}
private AdvisedRequest before(AdvisedRequest request) {
logger.info("System text: \n" + request.systemText());
logger.info("System params: " + request.systemParams());
logger.info("User text: \n" + request.userText());
@@ -86,10 +102,8 @@ public class OpenAiPaymentTransactionIT {
return request;
}
@Override
public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
logger.info("Response: " + response);
return response;
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info("Response: " + advisedResponse.response());
}
}

View File

@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.params.ParameterizedTest;
@@ -64,6 +65,38 @@ class OpenAiChatClientIT extends AbstractIT {
record ActorsFilms(String actor, List<String> movies) {
}
@Test
@Disabled("Although the Re2 advisor improves the response correctness it is not always guarantied to work.")
void re2() {
// .user(" Could Scooby Doo fit in a Kangaroo Pouch? Choices: (A) Yes (B) No")
// .user("Roger has 5 tennis balls. He buys 2 more cans of tennis " +
// "balls. Each can has 3 tennis balls. How many tennis balls " +
// "does he have now?")
String REASON_QUESTION = """
What do these words have in common?
Freight Stone Often Canine.
""";
// @formatter:off
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultOptions(OpenAiChatOptions.builder()
.withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()).build())
.defaultUser(REASON_QUESTION)
.build();
String response = chatClient.prompt()
.advisors(new ReReadingAdvisor())
.call()
.content();
// @formatter:on
logger.info("" + response);
assertThat(response.toLowerCase().replace("(", " ").replace(")", " ").replace("\"", " ").replace("\"", " "))
.contains(" eight", " one", " ten", " nine");
}
@Test
void call() {

View File

@@ -0,0 +1,94 @@
/*
* 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.client;
import java.util.HashMap;
import java.util.Map;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAroundAdvisorChain;
import reactor.core.publisher.Flux;
/**
* Drawing inspiration from the human strategy of re-reading, this advisor implements a
* re-reading strategy for LLM reasoning, dubbed RE2, to enhance understanding in the
* input phase. Based on the article:
* <a href="https://arxiv.org/pdf/2309.06275">Re-Reading Improves Reasoning in Large
* Language Models</a>
*
* @author Christian Tzolov
* @since 1.0.0
*/
public class ReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {
private static final String DEFAULT_RE2_ADVISE_TEMPLATE = """
{re2_input_query}
Read the question again: {re2_input_query}
""";
private final String re2AdviseTemplate;
private int order = 0;
public ReReadingAdvisor() {
this(DEFAULT_RE2_ADVISE_TEMPLATE);
}
public ReReadingAdvisor(String re2AdviseTemplate) {
this.re2AdviseTemplate = re2AdviseTemplate;
}
public String getName() {
return this.getClass().getSimpleName();
}
private AdvisedRequest before(AdvisedRequest advisedRequest) {
Map<String, Object> advisedUserParams = new HashMap<>(advisedRequest.userParams());
advisedUserParams.put("re2_input_query", advisedRequest.userText());
return AdvisedRequest.from(advisedRequest)
.withUserText(this.re2AdviseTemplate)
.withUserParams(advisedUserParams)
.build();
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
return chain.nextAroundCall(this.before(advisedRequest));
}
@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {
return chain.nextAroundStream(this.before(advisedRequest));
}
@Override
public int getOrder() {
return this.order;
}
public ReReadingAdvisor withOrder(int order) {
this.order = order;
return this;
}
}

View File

@@ -28,11 +28,11 @@ 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.client.AdvisedRequest;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.RequestAdvisor;
import org.springframework.ai.chat.client.advisor.api.ResponseAdvisor;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.model.function.FunctionCallbackWrapper.Builder.SchemaType;
import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
@@ -65,7 +65,7 @@ public class VertexAiGeminiPaymentTransactionIT {
record TransactionStatusResponse(String id, String status) {
}
private static class LoggingAdvisor implements RequestAdvisor, ResponseAdvisor {
private static class LoggingAdvisor implements CallAroundAdvisor {
private final Logger logger = LoggerFactory.getLogger(LoggingAdvisor.class);
@@ -75,7 +75,18 @@ public class VertexAiGeminiPaymentTransactionIT {
}
@Override
public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
public int getOrder() {
return 0;
}
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
var response = chain.nextAroundCall(before(advisedRequest));
observeAfter(response);
return response;
}
private AdvisedRequest before(AdvisedRequest request) {
logger.info("System text: \n" + request.systemText());
logger.info("System params: " + request.systemParams());
logger.info("User text: \n" + request.userText());
@@ -87,10 +98,8 @@ public class VertexAiGeminiPaymentTransactionIT {
return request;
}
@Override
public ChatResponse adviseResponse(ChatResponse response, Map<String, Object> context) {
logger.info("Response: " + response);
return response;
private void observeAfter(AdvisedResponse advisedResponse) {
logger.info("Response: " + advisedResponse.response());
}
}