From 65392821e0bc0a2f59f871e7982bb0a242106429 Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Tue, 9 Sep 2025 22:45:44 +0200 Subject: [PATCH 1/3] feat: Make advisors observations configurable * Add possibility to provide a custom AdvisorObservationConvention to the ChatClient for customizing the conventions for advisor spans and metrics. * Add ChatClientResponse to ChatClientObservationContext for achieving full visibility into both request and response. Signed-off-by: Thomas Vitale --- .../ChatClientAutoConfiguration.java | 8 ++-- .../ai/chat/client/ChatClient.java | 33 +++++++++++-- .../ai/chat/client/DefaultChatClient.java | 47 +++++++++++++++---- .../chat/client/DefaultChatClientBuilder.java | 16 ++++++- .../advisor/DefaultAroundAdvisorChain.java | 29 ++++++++++-- .../client/DefaultChatClientBuilderTests.java | 9 +++- .../chat/client/DefaultChatClientTests.java | 6 +-- .../DefaultAroundAdvisorChainTests.java | 8 ++++ 8 files changed, 128 insertions(+), 28 deletions(-) diff --git a/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java index b5d94a49cb9..6fcd603437a 100644 --- a/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java +++ b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClientCustomizer; +import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention; import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler; import org.springframework.ai.chat.client.observation.ChatClientObservationContext; import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; @@ -90,11 +91,12 @@ ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - + ObjectProvider chatClientObservationConvention, + ObjectProvider advisorObservationConvention) { ChatClient.Builder builder = ChatClient.builder(chatModel, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP), - observationConvention.getIfUnique(() -> null)); + chatClientObservationConvention.getIfUnique(() -> null), + advisorObservationConvention.getIfUnique(() -> null)); return chatClientBuilderConfigurer.configure(builder); } diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java index 2f7450d9443..b34b89784db 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Flux; import org.springframework.ai.chat.client.advisor.api.Advisor; +import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention; import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; @@ -65,22 +66,46 @@ static ChatClient create(ChatModel chatModel, ObservationRegistry observationReg return create(chatModel, observationRegistry, null); } + /** + * @deprecated in favor of + * {@link #create(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}. + */ + @Deprecated + static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry, + @Nullable ChatClientObservationConvention chatClientObservationConvention) { + return create(chatModel, observationRegistry, chatClientObservationConvention, null); + } + static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry, - @Nullable ChatClientObservationConvention observationConvention) { + @Nullable ChatClientObservationConvention chatClientObservationConvention, + @Nullable AdvisorObservationConvention advisorObservationConvention) { Assert.notNull(chatModel, "chatModel cannot be null"); Assert.notNull(observationRegistry, "observationRegistry cannot be null"); - return builder(chatModel, observationRegistry, observationConvention).build(); + return builder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention) + .build(); } static Builder builder(ChatModel chatModel) { return builder(chatModel, ObservationRegistry.NOOP, null); } + /** + * @deprecated in favor of + * {@link #builder(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}. + */ + @Deprecated + static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry, + @Nullable ChatClientObservationConvention chatClientObservationConvention) { + return builder(chatModel, observationRegistry, chatClientObservationConvention, null); + } + static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry, - @Nullable ChatClientObservationConvention customObservationConvention) { + @Nullable ChatClientObservationConvention chatClientObservationConvention, + @Nullable AdvisorObservationConvention advisorObservationConvention) { Assert.notNull(chatModel, "chatModel cannot be null"); Assert.notNull(observationRegistry, "observationRegistry cannot be null"); - return new DefaultChatClientBuilder(chatModel, observationRegistry, customObservationConvention); + return new DefaultChatClientBuilder(chatModel, observationRegistry, chatClientObservationConvention, + advisorObservationConvention); } ChatClientRequestSpec prompt(); diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java index b811c414600..cca67e16d94 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java @@ -38,6 +38,7 @@ import org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain; import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.client.advisor.api.BaseAdvisorChain; +import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention; import org.springframework.ai.chat.client.observation.ChatClientObservationContext; import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation; @@ -615,7 +616,10 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe private final ObservationRegistry observationRegistry; - private final ChatClientObservationConvention observationConvention; + private final ChatClientObservationConvention chatClientObservationConvention; + + @Nullable + private final AdvisorObservationConvention advisorObservationConvention; private final ChatModel chatModel; @@ -659,18 +663,36 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams, ccr.systemMetadata, ccr.toolCallbacks, ccr.toolCallbackProviders, ccr.messages, ccr.toolNames, ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams, ccr.observationRegistry, - ccr.observationConvention, ccr.toolContext, ccr.templateRenderer); + ccr.chatClientObservationConvention, ccr.toolContext, ccr.templateRenderer, + ccr.advisorObservationConvention); } + /** + * @deprecated in favor of the other constructor. + */ + @Deprecated(since = "1.1.0", forRemoval = true) public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText, Map userParams, Map userMetadata, @Nullable String systemText, Map systemParams, Map systemMetadata, List toolCallbacks, List toolCallbackProviders, List messages, List toolNames, List media, @Nullable ChatOptions chatOptions, List advisors, Map advisorParams, ObservationRegistry observationRegistry, - @Nullable ChatClientObservationConvention observationConvention, Map toolContext, - @Nullable TemplateRenderer templateRenderer) { + @Nullable ChatClientObservationConvention chatClientObservationConvention, + Map toolContext, @Nullable TemplateRenderer templateRenderer) { + this(chatModel, userText, userParams, userMetadata, systemText, systemParams, systemMetadata, toolCallbacks, + toolCallbackProviders, messages, toolNames, media, chatOptions, advisors, advisorParams, + observationRegistry, chatClientObservationConvention, toolContext, templateRenderer, null); + } + public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText, + Map userParams, Map userMetadata, @Nullable String systemText, + Map systemParams, Map systemMetadata, List toolCallbacks, + List toolCallbackProviders, List messages, List toolNames, + List media, @Nullable ChatOptions chatOptions, List advisors, + Map advisorParams, ObservationRegistry observationRegistry, + @Nullable ChatClientObservationConvention chatClientObservationConvention, + Map toolContext, @Nullable TemplateRenderer templateRenderer, + @Nullable AdvisorObservationConvention advisorObservationConvention) { Assert.notNull(chatModel, "chatModel cannot be null"); Assert.notNull(userParams, "userParams cannot be null"); Assert.notNull(userMetadata, "userMetadata cannot be null"); @@ -706,10 +728,11 @@ public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userTe this.advisors.addAll(advisors); this.advisorParams.putAll(advisorParams); this.observationRegistry = observationRegistry; - this.observationConvention = observationConvention != null ? observationConvention - : DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION; + this.chatClientObservationConvention = chatClientObservationConvention != null + ? chatClientObservationConvention : DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION; this.toolContext.putAll(toolContext); this.templateRenderer = templateRenderer != null ? templateRenderer : DEFAULT_TEMPLATE_RENDERER; + this.advisorObservationConvention = advisorObservationConvention; } @Nullable @@ -786,7 +809,8 @@ public TemplateRenderer getTemplateRenderer() { @Override public Builder mutate() { DefaultChatClientBuilder builder = (DefaultChatClientBuilder) ChatClient - .builder(this.chatModel, this.observationRegistry, this.observationConvention) + .builder(this.chatModel, this.observationRegistry, this.chatClientObservationConvention, + this.advisorObservationConvention) .defaultTemplateRenderer(this.templateRenderer) .defaultToolCallbacks(this.toolCallbacks) .defaultToolCallbacks(this.toolCallbackProviders.toArray(new ToolCallback[0])) @@ -1005,14 +1029,14 @@ public ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer) public CallResponseSpec call() { BaseAdvisorChain advisorChain = buildAdvisorChain(); return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, - this.observationRegistry, this.observationConvention); + this.observationRegistry, this.chatClientObservationConvention); } @Override public StreamResponseSpec stream() { BaseAdvisorChain advisorChain = buildAdvisorChain(); return new DefaultStreamResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain, - this.observationRegistry, this.observationConvention); + this.observationRegistry, this.chatClientObservationConvention); } private BaseAdvisorChain buildAdvisorChain() { @@ -1021,7 +1045,10 @@ private BaseAdvisorChain buildAdvisorChain() { this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build()); this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build()); - return DefaultAroundAdvisorChain.builder(this.observationRegistry).pushAll(this.advisors).build(); + return DefaultAroundAdvisorChain.builder(this.observationRegistry) + .observationConvention(this.advisorObservationConvention) + .pushAll(this.advisors) + .build(); } } diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java index 6778dc222e5..dc77c74f066 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java @@ -29,6 +29,7 @@ import org.springframework.ai.chat.client.ChatClient.PromptUserSpec; import org.springframework.ai.chat.client.DefaultChatClient.DefaultChatClientRequestSpec; import org.springframework.ai.chat.client.advisor.api.Advisor; +import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention; import org.springframework.ai.chat.client.observation.ChatClientObservationConvention; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.model.ChatModel; @@ -60,13 +61,24 @@ public class DefaultChatClientBuilder implements Builder { this(chatModel, ObservationRegistry.NOOP, null); } + /** + * @deprecated in favor of + * {@link #DefaultChatClientBuilder(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}. + */ + @Deprecated(since = "1.1.0", forRemoval = true) public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observationRegistry, - @Nullable ChatClientObservationConvention customObservationConvention) { + @Nullable ChatClientObservationConvention chatClientObservationConvention) { + this(chatModel, observationRegistry, chatClientObservationConvention, null); + } + + public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observationRegistry, + @Nullable ChatClientObservationConvention chatClientObservationConvention, + @Nullable AdvisorObservationConvention advisorObservationConvention) { Assert.notNull(chatModel, "the " + ChatModel.class.getName() + " must be non-null"); Assert.notNull(observationRegistry, "the " + ObservationRegistry.class.getName() + " must be non-null"); this.defaultRequest = new DefaultChatClientRequestSpec(chatModel, null, Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(), - observationRegistry, customObservationConvention, Map.of(), null); + observationRegistry, chatClientObservationConvention, Map.of(), null, advisorObservationConvention); } public ChatClient build() { diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java index 97beb4fbe6c..cb04d01dde1 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java @@ -37,6 +37,7 @@ import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationDocumentation; import org.springframework.ai.chat.client.advisor.observation.DefaultAdvisorObservationConvention; import org.springframework.core.OrderComparator; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -64,8 +65,10 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain { private final ObservationRegistry observationRegistry; + private final AdvisorObservationConvention observationConvention; + DefaultAroundAdvisorChain(ObservationRegistry observationRegistry, Deque callAdvisors, - Deque streamAdvisors) { + Deque streamAdvisors, @Nullable AdvisorObservationConvention observationConvention) { Assert.notNull(observationRegistry, "the observationRegistry must be non-null"); Assert.notNull(callAdvisors, "the callAdvisors must be non-null"); @@ -76,6 +79,8 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain { this.streamAdvisors = streamAdvisors; this.originalCallAdvisors = List.copyOf(callAdvisors); this.originalStreamAdvisors = List.copyOf(streamAdvisors); + this.observationConvention = observationConvention != null ? observationConvention + : DEFAULT_OBSERVATION_CONVENTION; } public static Builder builder(ObservationRegistry observationRegistry) { @@ -99,8 +104,13 @@ public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) { .build(); return AdvisorObservationDocumentation.AI_ADVISOR - .observation(null, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry) - .observe(() -> advisor.adviseCall(chatClientRequest, this)); + .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry) + .observe(() -> { + var chatClientResponse = advisor.adviseCall(chatClientRequest, this); + observationContext.setChatClientResponse(chatClientResponse); + return chatClientResponse; + }); } @Override @@ -120,7 +130,7 @@ public Flux nextStream(ChatClientRequest chatClientRequest) .order(advisor.getOrder()) .build(); - var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(null, + var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start(); @@ -175,12 +185,20 @@ public static final class Builder { private final Deque streamAdvisors; + @Nullable + private AdvisorObservationConvention observationConvention; + public Builder(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; this.callAdvisors = new ConcurrentLinkedDeque<>(); this.streamAdvisors = new ConcurrentLinkedDeque<>(); } + public Builder observationConvention(@Nullable AdvisorObservationConvention observationConvention) { + this.observationConvention = observationConvention; + return this; + } + public Builder push(Advisor advisor) { Assert.notNull(advisor, "the advisor must be non-null"); return this.pushAll(List.of(advisor)); @@ -229,7 +247,8 @@ private void reOrder() { } public DefaultAroundAdvisorChain build() { - return new DefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors); + return new DefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors, + this.observationConvention); } } diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientBuilderTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientBuilderTests.java index 6fcde4557ea..d302ebf26f7 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientBuilderTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -18,6 +18,7 @@ import java.nio.charset.Charset; +import io.micrometer.observation.ObservationRegistry; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.model.ChatModel; @@ -63,6 +64,12 @@ void whenObservationRegistryIsNullThenThrows() { .hasMessage("the io.micrometer.observation.ObservationRegistry must be non-null"); } + @Test + void whenAdvisorObservationConventionIsNullThenReturn() { + var builder = new DefaultChatClientBuilder(mock(ChatModel.class), mock(ObservationRegistry.class), null, null); + assertThat(builder).isNotNull(); + } + @Test void whenUserResourceIsNullThenThrows() { DefaultChatClientBuilder builder = new DefaultChatClientBuilder(mock(ChatModel.class)); diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java index fa9cc61d6b6..20ea2e11bcd 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientTests.java @@ -1548,7 +1548,7 @@ void buildChatClientRequestSpec() { ChatModel chatModel = mock(ChatModel.class); DefaultChatClient.DefaultChatClientRequestSpec spec = new DefaultChatClient.DefaultChatClientRequestSpec( chatModel, null, Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), - List.of(), List.of(), null, List.of(), Map.of(), ObservationRegistry.NOOP, null, Map.of(), null); + List.of(), List.of(), null, List.of(), Map.of(), ObservationRegistry.NOOP, null, Map.of(), null, null); assertThat(spec).isNotNull(); } @@ -1556,7 +1556,7 @@ void buildChatClientRequestSpec() { void whenChatModelIsNullThenThrow() { assertThatThrownBy(() -> new DefaultChatClient.DefaultChatClientRequestSpec(null, null, Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), - Map.of(), ObservationRegistry.NOOP, null, Map.of(), null)) + Map.of(), ObservationRegistry.NOOP, null, Map.of(), null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("chatModel cannot be null"); } @@ -1565,7 +1565,7 @@ void whenChatModelIsNullThenThrow() { void whenObservationRegistryIsNullThenThrow() { assertThatThrownBy(() -> new DefaultChatClient.DefaultChatClientRequestSpec(mock(ChatModel.class), null, Map.of(), Map.of(), null, Map.of(), Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), - null, List.of(), Map.of(), null, null, Map.of(), null)) + null, List.of(), Map.of(), null, null, Map.of(), null, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("observationRegistry cannot be null"); } diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChainTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChainTests.java index 5e0c2b54119..6be8840dc2a 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChainTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChainTests.java @@ -76,6 +76,14 @@ void whenAdvisorListContainsNullElementsThenThrow() { .hasMessage("the advisors must not contain null elements"); } + @Test + void getObservationConventionIsNullThenUseDefault() { + AdvisorChain chain = DefaultAroundAdvisorChain.builder(ObservationRegistry.create()) + .observationConvention(null) + .build(); + assertThat(chain).isNotNull(); + } + @Test void getObservationRegistry() { ObservationRegistry observationRegistry = ObservationRegistry.create(); From 1aa1ed0b6e30d6210c7e1b7a28f2377cb829f001 Mon Sep 17 00:00:00 2001 From: Thomas Vitale Date: Wed, 17 Sep 2025 18:13:40 +0200 Subject: [PATCH 2/3] chore: Mark deprecated methods for removal Signed-off-by: Thomas Vitale --- .../java/org/springframework/ai/chat/client/ChatClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java index b34b89784db..3fcdeb9b9c1 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java @@ -70,7 +70,7 @@ static ChatClient create(ChatModel chatModel, ObservationRegistry observationReg * @deprecated in favor of * {@link #create(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}. */ - @Deprecated + @Deprecated(since = "1.1.0", forRemoval = true) static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention) { return create(chatModel, observationRegistry, chatClientObservationConvention, null); @@ -93,7 +93,7 @@ static Builder builder(ChatModel chatModel) { * @deprecated in favor of * {@link #builder(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}. */ - @Deprecated + @Deprecated(since = "1.1.0", forRemoval = true) static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry, @Nullable ChatClientObservationConvention chatClientObservationConvention) { return builder(chatModel, observationRegistry, chatClientObservationConvention, null); From 6d235a0e12f94bbd69f990749290c4966a02ed84 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Thu, 30 Oct 2025 14:57:01 -0700 Subject: [PATCH 3/3] Set ChatClientResponse on AdvisorObservationContext --- .../ai/chat/client/advisor/DefaultAroundAdvisorChain.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java index cb04d01dde1..5583c219fc6 100644 --- a/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java +++ b/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java @@ -25,6 +25,7 @@ import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import reactor.core.publisher.Flux; +import org.springframework.ai.chat.client.ChatClientMessageAggregator; import org.springframework.ai.chat.client.ChatClientRequest; import org.springframework.ai.chat.client.ChatClientResponse; import org.springframework.ai.chat.client.advisor.api.Advisor; @@ -55,6 +56,8 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain { public static final AdvisorObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultAdvisorObservationConvention(); + private static final ChatClientMessageAggregator CHAT_CLIENT_MESSAGE_AGGREGATOR = new ChatClientMessageAggregator(); + private final List originalCallAdvisors; private final List originalStreamAdvisors; @@ -136,11 +139,13 @@ public Flux nextStream(ChatClientRequest chatClientRequest) observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start(); // @formatter:off - return Flux.defer(() -> advisor.adviseStream(chatClientRequest, this) + Flux chatClientResponse = Flux.defer(() -> advisor.adviseStream(chatClientRequest, this) .doOnError(observation::error) .doFinally(s -> observation.stop()) .contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation))); // @formatter:on + return CHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse, + observationContext::setChatClientResponse); }); }