From 7894d80abaec136a936c0725f7db0a23cfa7cc10 Mon Sep 17 00:00:00 2001 From: Nicolas Krier <7557886+nicolaskrier@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:02:30 +0200 Subject: [PATCH] Improve logs in StructuredOutputValidationAdvisor - Add debug log for validation success - Fix left attempts counter in existing debug log - Remove unnecessary null check in StructuredOutputValidationAdvisor - Polish StructuredOutputValidationAdvisorTests Signed-off-by: Nicolas Krier <7557886+nicolaskrier@users.noreply.github.com> --- .../StructuredOutputValidationAdvisor.java | 26 ++++++++----------- ...tructuredOutputValidationAdvisorTests.java | 10 ++++--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java index 2e16e18fad..c427b13033 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.java @@ -122,18 +122,14 @@ public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAd ChatClientResponse chatClientResponse = null; - var repeatCounter = 0; - - boolean isValidationSuccess = true; + boolean isValidationSuccess = false; var processedChatClientRequest = chatClientRequest; UsageAccumulator usageAccumulator = new UsageAccumulator(); - do { - // Before Call - repeatCounter++; - + for (var currentAttemptNumber = 1 + this.maxRepeatAttempts; currentAttemptNumber > 0 + && !isValidationSuccess; currentAttemptNumber--) { // Next Call chatClientResponse = callAdvisorChain.copy(this).nextCall(processedChatClientRequest); @@ -144,8 +140,8 @@ public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAd // We should not validate tool call requests, only the content of the final // response. if (chatResponse == null || !chatResponse.hasToolCalls()) { - - SchemaValidation validationResponse = validateOutputSchema(chatClientResponse); + SchemaValidation validationResponse = validateOutputSchema(chatClientResponse, + currentAttemptNumber - 1); isValidationSuccess = validationResponse.success(); @@ -171,18 +167,18 @@ public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAd processedChatClientRequest = chatClientRequest.mutate().prompt(augmentedPrompt).build(); } + else if (logger.isDebugEnabled()) { + logger.debug("JSON validation succeeded"); + } } } - while (!isValidationSuccess && repeatCounter <= this.maxRepeatAttempts); - return usageAccumulator.applyAccumulatedUsage(chatClientResponse); + return usageAccumulator.applyAccumulatedUsage(Objects.requireNonNull(chatClientResponse)); } - @SuppressWarnings("null") - private SchemaValidation validateOutputSchema(ChatClientResponse chatClientResponse) { + private SchemaValidation validateOutputSchema(ChatClientResponse chatClientResponse, int leftAttemptsCounter) { if (chatClientResponse.chatResponse() == null || chatClientResponse.chatResponse().getResult() == null - || chatClientResponse.chatResponse().getResult().getOutput() == null || chatClientResponse.chatResponse().getResult().getOutput().getText() == null) { logger.warn("ChatClientResponse is missing required json output for validation."); @@ -193,7 +189,7 @@ private SchemaValidation validateOutputSchema(ChatClientResponse chatClientRespo String json = chatClientResponse.chatResponse().getResult().getOutput().getText(); if (logger.isDebugEnabled()) { - logger.debug("Validating JSON output against schema. Attempts left: " + this.maxRepeatAttempts); + logger.debug("Validating JSON output against schema. Attempts left: " + leftAttemptsCounter); } return validateJsonText(json); diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java index b0370e0505..8e318d6c68 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisorTests.java @@ -55,7 +55,7 @@ * @author Jewoo Shin */ @ExtendWith(MockitoExtension.class) -public class StructuredOutputValidationAdvisorTests { +class StructuredOutputValidationAdvisorTests { @Mock private CallAdvisorChain callAdvisorChain; @@ -791,10 +791,12 @@ void testValidationWithInvalidListType() { @Test void testValidationWithWrongTypeInField() { + int maxRepeatAttempts = 3; + int lastAttemptNumber = maxRepeatAttempts + 1; StructuredOutputValidationAdvisor advisor = StructuredOutputValidationAdvisor.builder() .outputType(new TypeReference() { }) - .maxRepeatAttempts(1) + .maxRepeatAttempts(maxRepeatAttempts) .build(); ChatClientRequest request = createMockRequest(); @@ -808,7 +810,7 @@ void testValidationWithWrongTypeInField() { int[] callCount = { 0 }; CallAdvisor terminalAdvisor = terminalAdvisor((req, chain) -> { callCount[0]++; - return callCount[0] == 1 ? invalidResponse : validResponse; + return callCount[0] == lastAttemptNumber ? validResponse : invalidResponse; }); CallAdvisorChain realChain = DefaultAroundAdvisorChain.builder(ObservationRegistry.NOOP) @@ -818,7 +820,7 @@ void testValidationWithWrongTypeInField() { ChatClientResponse result = realChain.nextCall(request); assertThat(result).isEqualTo(validResponse); - assertThat(callCount[0]).isEqualTo(2); + assertThat(callCount[0]).isEqualTo(lastAttemptNumber); } @Test