Skip to content

Conversation

tzolov
Copy link
Contributor

@tzolov tzolov commented Oct 13, 2025

Add support for recursive (repetitive) advisor execution patterns, allowing advisors to re-invoke themselves or the remaining chain multiple times. This enables advanced patterns like retry logic, iterative refinement, and multi-pass processing.

  • Add AdvisorUtils.copyChainAfterAdvisor() utility to enable recursive chain invocation
  • Implement ToolCallAdvisor for recursive tool calling with configurable tool execution
  • Implement StructuredOutputValidationAdvisor for recursive output validation with retry logic
  • Add MCP JSON Jackson2 dependency for JSON schema validation
  • Add test suites for both new advisors and utility methods
  • Add documentation for recursive advisor patterns

Depends on: modelcontextprotocol/java-sdk#617

TODO: The ToolCallAdvisor doesn't implement streaming tool-calling model at the moment. Perhaps we can address this in a follow up issues along with auto-config integrations for the ToolCallAdvisor.

…-in advisors

Add support for recursive (repetitive) advisor execution patterns, allowing advisors to re-invoke
themselves or the remaining chain multiple times. This enables advanced patterns like retry logic,
iterative refinement, and multi-pass processing.

- Add AdvisorUtils.copyChainAfterAdvisor() utility to enable recursive chain invocation
- Implement ToolCallAdvisor for recursive tool calling with configurable tool execution
- Implement StructuredOutputValidationAdvisor for recursive output validation with retry logic
- Add MCP JSON Jackson2 dependency for JSON schema validation
- Add test suites for both new advisors and utility methods
- Add documentation for recursive advisor patterns

Signed-off-by: Christian Tzolov <[email protected]>
…idation feedback loop

Refactor retry logic to provide validation error feedback to LLM for self-correction.
Extract validation into separate method and augment prompts with error messages on retry.
Add comprehensive test coverage for various validation scenarios including nested objects,
lists, malformed JSON, and type mismatches.

Signed-off-by: Christian Tzolov <[email protected]>
Implements return direct functionality allowing tools to bypass the LLM
and return results directly to clients. Adds null safety checks for
chatResponse and comprehensive test coverage.

Signed-off-by: Christian Tzolov <[email protected]>
@tzolov tzolov marked this pull request as ready for review October 14, 2025 18:31
Assert.notNull(outputType, "outputType must not be null");
Assert.isTrue(advisorOrder > BaseAdvisor.HIGHEST_PRECEDENCE && advisorOrder < BaseAdvisor.LOWEST_PRECEDENCE,
"advisorOrder must be between HIGHEST_PRECEDENCE and LOWEST_PRECEDENCE");
Assert.isTrue(repeatAttempts >= 0, "repeatAttempts must be greater than or equal to 0");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a value of zero effectively makes this infinite, given the do {} while() construct. Is this what we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the repeatAttempts is set to 0 this effectively mean never repeat.
E.g. the loop is count = 0; do { count++ ...} while (count <= maxAttempt); and if the maxAttempt is 0 the loop should run only once?


this.advisorOrder = advisorOrder;

this.jsonvalidator = new DefaultJsonSchemaValidator();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be initialised above, and maybe even made static AFAICT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I wonted to make the objectmaper configurable and aligned with the default mapper used in spring ai (JsonParser.getObjectMapper()) but forgot to add it.
Will update the code


ChatClientResponse chatClientResponse = null;

var repeatCounter = new AtomicInteger(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not a regular int ???

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right. Not sure why i thought that some concurrency can occur. will make it int

* @return a new CallAdvisorChain containing all advisors after the specified advisor
* @throws IllegalArgumentException if the specified advisor is not part of the chain
*/
public static CallAdvisorChain copyChainAfterAdvisor(CallAdvisorChain callAdvisorChain, CallAdvisor after) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a method on CallAdvisorChain instead? I feel like it would be more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have a point

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will refactor it


if (toolExecutionResult.returnDirect()) {
// Interupt the tool calling loop and return the tool execution result
// directly to the client application instead of returning it tothe
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// directly to the client application instead of returning it tothe
// directly to the client application instead of returning it to the

// Interupt the tool calling loop and return the tool execution result
// directly to the client application instead of returning it tothe
// LLM.
isToolCall = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be changed to a break statement IMO


= Recursive Advisors

== What is a Recursive Advisor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
== What is a Recursive Advisor
== What is a Recursive Advisor?

* Implementing Evaluation logic with modifications to the request
* Implementing retry logic with modifications to the request

The `AdvisorUtils.copyChainAfterAdvisor()` method is the key utility that enables recursive advisor patterns.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change here if you change this to be an instance method on chain

…ain interface

- Add CallAdvisorChain.copy(CallAdvisor after) method to replace AdvisorUtils.copyChainAfterAdvisor()
- Implement copy() method in DefaultAroundAdvisorChain
- Update StructuredOutputValidationAdvisor and ToolCallAdvisor to use new copy() API
- Add ObjectMapper parameter support to StructuredOutputValidationAdvisor for custom JSON processing
- Improve ToolCallAdvisor return direct logic using break instead of flag
- Move tests from AdvisorUtilsTests to DefaultAroundAdvisorChainTests
- Update documentation to reflect API changes

This refactoring improves the API design by moving chain manipulation logic
closer to where it belongs (on the chain itself) rather than in a utility class.

Signed-off-by: Christian Tzolov <[email protected]>
@tzolov
Copy link
Contributor Author

tzolov commented Oct 16, 2025

Thank you the review and great feedback @ericbottard
I've pushed an update that hopefully address most of them

Signed-off-by: Christian Tzolov <[email protected]>
Signed-off-by: Christian Tzolov <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants