Skip to content

Commit 3cf3db1

Browse files
ThomasVitalejonatan-ivanov
authored andcommitted
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. chore: Mark deprecated methods for removal Signed-off-by: Thomas Vitale <[email protected]> Set ChatClientResponse on AdvisorObservationContext Co-authored-by: Jonatan Ivanov <[email protected]>
1 parent 13de219 commit 3cf3db1

File tree

8 files changed

+134
-29
lines changed

8 files changed

+134
-29
lines changed

auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.ai.chat.client.ChatClient;
2525
import org.springframework.ai.chat.client.ChatClientCustomizer;
26+
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
2627
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
2728
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
2829
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
@@ -90,11 +91,12 @@ ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClien
9091
@ConditionalOnMissingBean
9192
ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel,
9293
ObjectProvider<ObservationRegistry> observationRegistry,
93-
ObjectProvider<ChatClientObservationConvention> observationConvention) {
94-
94+
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
95+
ObjectProvider<AdvisorObservationConvention> advisorObservationConvention) {
9596
ChatClient.Builder builder = ChatClient.builder(chatModel,
9697
observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
97-
observationConvention.getIfUnique(() -> null));
98+
chatClientObservationConvention.getIfUnique(() -> null),
99+
advisorObservationConvention.getIfUnique(() -> null));
98100
return chatClientBuilderConfigurer.configure(builder);
99101
}
100102

spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/ChatClient.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Flux;
2727

2828
import org.springframework.ai.chat.client.advisor.api.Advisor;
29+
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
2930
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
3031
import org.springframework.ai.chat.messages.Message;
3132
import org.springframework.ai.chat.model.ChatModel;
@@ -65,22 +66,46 @@ static ChatClient create(ChatModel chatModel, ObservationRegistry observationReg
6566
return create(chatModel, observationRegistry, null);
6667
}
6768

69+
/**
70+
* @deprecated in favor of
71+
* {@link #create(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}.
72+
*/
73+
@Deprecated(since = "1.1.0", forRemoval = true)
74+
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry,
75+
@Nullable ChatClientObservationConvention chatClientObservationConvention) {
76+
return create(chatModel, observationRegistry, chatClientObservationConvention, null);
77+
}
78+
6879
static ChatClient create(ChatModel chatModel, ObservationRegistry observationRegistry,
69-
@Nullable ChatClientObservationConvention observationConvention) {
80+
@Nullable ChatClientObservationConvention chatClientObservationConvention,
81+
@Nullable AdvisorObservationConvention advisorObservationConvention) {
7082
Assert.notNull(chatModel, "chatModel cannot be null");
7183
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
72-
return builder(chatModel, observationRegistry, observationConvention).build();
84+
return builder(chatModel, observationRegistry, chatClientObservationConvention, advisorObservationConvention)
85+
.build();
7386
}
7487

7588
static Builder builder(ChatModel chatModel) {
7689
return builder(chatModel, ObservationRegistry.NOOP, null);
7790
}
7891

92+
/**
93+
* @deprecated in favor of
94+
* {@link #builder(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}.
95+
*/
96+
@Deprecated(since = "1.1.0", forRemoval = true)
97+
static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry,
98+
@Nullable ChatClientObservationConvention chatClientObservationConvention) {
99+
return builder(chatModel, observationRegistry, chatClientObservationConvention, null);
100+
}
101+
79102
static Builder builder(ChatModel chatModel, ObservationRegistry observationRegistry,
80-
@Nullable ChatClientObservationConvention customObservationConvention) {
103+
@Nullable ChatClientObservationConvention chatClientObservationConvention,
104+
@Nullable AdvisorObservationConvention advisorObservationConvention) {
81105
Assert.notNull(chatModel, "chatModel cannot be null");
82106
Assert.notNull(observationRegistry, "observationRegistry cannot be null");
83-
return new DefaultChatClientBuilder(chatModel, observationRegistry, customObservationConvention);
107+
return new DefaultChatClientBuilder(chatModel, observationRegistry, chatClientObservationConvention,
108+
advisorObservationConvention);
84109
}
85110

86111
ChatClientRequestSpec prompt();

spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.ai.chat.client.advisor.DefaultAroundAdvisorChain;
3939
import org.springframework.ai.chat.client.advisor.api.Advisor;
4040
import org.springframework.ai.chat.client.advisor.api.BaseAdvisorChain;
41+
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
4142
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
4243
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
4344
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation;
@@ -615,7 +616,10 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe
615616

616617
private final ObservationRegistry observationRegistry;
617618

618-
private final ChatClientObservationConvention observationConvention;
619+
private final ChatClientObservationConvention chatClientObservationConvention;
620+
621+
@Nullable
622+
private final AdvisorObservationConvention advisorObservationConvention;
619623

620624
private final ChatModel chatModel;
621625

@@ -659,18 +663,36 @@ public static class DefaultChatClientRequestSpec implements ChatClientRequestSpe
659663
this(ccr.chatModel, ccr.userText, ccr.userParams, ccr.userMetadata, ccr.systemText, ccr.systemParams,
660664
ccr.systemMetadata, ccr.toolCallbacks, ccr.toolCallbackProviders, ccr.messages, ccr.toolNames,
661665
ccr.media, ccr.chatOptions, ccr.advisors, ccr.advisorParams, ccr.observationRegistry,
662-
ccr.observationConvention, ccr.toolContext, ccr.templateRenderer);
666+
ccr.chatClientObservationConvention, ccr.toolContext, ccr.templateRenderer,
667+
ccr.advisorObservationConvention);
663668
}
664669

670+
/**
671+
* @deprecated in favor of the other constructor.
672+
*/
673+
@Deprecated(since = "1.1.0", forRemoval = true)
665674
public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText,
666675
Map<String, Object> userParams, Map<String, Object> userMetadata, @Nullable String systemText,
667676
Map<String, Object> systemParams, Map<String, Object> systemMetadata, List<ToolCallback> toolCallbacks,
668677
List<ToolCallbackProvider> toolCallbackProviders, List<Message> messages, List<String> toolNames,
669678
List<Media> media, @Nullable ChatOptions chatOptions, List<Advisor> advisors,
670679
Map<String, Object> advisorParams, ObservationRegistry observationRegistry,
671-
@Nullable ChatClientObservationConvention observationConvention, Map<String, Object> toolContext,
672-
@Nullable TemplateRenderer templateRenderer) {
680+
@Nullable ChatClientObservationConvention chatClientObservationConvention,
681+
Map<String, Object> toolContext, @Nullable TemplateRenderer templateRenderer) {
682+
this(chatModel, userText, userParams, userMetadata, systemText, systemParams, systemMetadata, toolCallbacks,
683+
toolCallbackProviders, messages, toolNames, media, chatOptions, advisors, advisorParams,
684+
observationRegistry, chatClientObservationConvention, toolContext, templateRenderer, null);
685+
}
673686

687+
public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userText,
688+
Map<String, Object> userParams, Map<String, Object> userMetadata, @Nullable String systemText,
689+
Map<String, Object> systemParams, Map<String, Object> systemMetadata, List<ToolCallback> toolCallbacks,
690+
List<ToolCallbackProvider> toolCallbackProviders, List<Message> messages, List<String> toolNames,
691+
List<Media> media, @Nullable ChatOptions chatOptions, List<Advisor> advisors,
692+
Map<String, Object> advisorParams, ObservationRegistry observationRegistry,
693+
@Nullable ChatClientObservationConvention chatClientObservationConvention,
694+
Map<String, Object> toolContext, @Nullable TemplateRenderer templateRenderer,
695+
@Nullable AdvisorObservationConvention advisorObservationConvention) {
674696
Assert.notNull(chatModel, "chatModel cannot be null");
675697
Assert.notNull(userParams, "userParams cannot be null");
676698
Assert.notNull(userMetadata, "userMetadata cannot be null");
@@ -706,10 +728,11 @@ public DefaultChatClientRequestSpec(ChatModel chatModel, @Nullable String userTe
706728
this.advisors.addAll(advisors);
707729
this.advisorParams.putAll(advisorParams);
708730
this.observationRegistry = observationRegistry;
709-
this.observationConvention = observationConvention != null ? observationConvention
710-
: DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION;
731+
this.chatClientObservationConvention = chatClientObservationConvention != null
732+
? chatClientObservationConvention : DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION;
711733
this.toolContext.putAll(toolContext);
712734
this.templateRenderer = templateRenderer != null ? templateRenderer : DEFAULT_TEMPLATE_RENDERER;
735+
this.advisorObservationConvention = advisorObservationConvention;
713736
}
714737

715738
@Nullable
@@ -786,7 +809,8 @@ public TemplateRenderer getTemplateRenderer() {
786809
@Override
787810
public Builder mutate() {
788811
DefaultChatClientBuilder builder = (DefaultChatClientBuilder) ChatClient
789-
.builder(this.chatModel, this.observationRegistry, this.observationConvention)
812+
.builder(this.chatModel, this.observationRegistry, this.chatClientObservationConvention,
813+
this.advisorObservationConvention)
790814
.defaultTemplateRenderer(this.templateRenderer)
791815
.defaultToolCallbacks(this.toolCallbacks)
792816
.defaultToolCallbacks(this.toolCallbackProviders.toArray(new ToolCallback[0]))
@@ -1005,14 +1029,14 @@ public ChatClientRequestSpec templateRenderer(TemplateRenderer templateRenderer)
10051029
public CallResponseSpec call() {
10061030
BaseAdvisorChain advisorChain = buildAdvisorChain();
10071031
return new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
1008-
this.observationRegistry, this.observationConvention);
1032+
this.observationRegistry, this.chatClientObservationConvention);
10091033
}
10101034

10111035
@Override
10121036
public StreamResponseSpec stream() {
10131037
BaseAdvisorChain advisorChain = buildAdvisorChain();
10141038
return new DefaultStreamResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,
1015-
this.observationRegistry, this.observationConvention);
1039+
this.observationRegistry, this.chatClientObservationConvention);
10161040
}
10171041

10181042
private BaseAdvisorChain buildAdvisorChain() {
@@ -1021,7 +1045,10 @@ private BaseAdvisorChain buildAdvisorChain() {
10211045
this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());
10221046
this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());
10231047

1024-
return DefaultAroundAdvisorChain.builder(this.observationRegistry).pushAll(this.advisors).build();
1048+
return DefaultAroundAdvisorChain.builder(this.observationRegistry)
1049+
.observationConvention(this.advisorObservationConvention)
1050+
.pushAll(this.advisors)
1051+
.build();
10251052
}
10261053

10271054
}

spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClientBuilder.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.ai.chat.client.ChatClient.PromptUserSpec;
3030
import org.springframework.ai.chat.client.DefaultChatClient.DefaultChatClientRequestSpec;
3131
import org.springframework.ai.chat.client.advisor.api.Advisor;
32+
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationConvention;
3233
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
3334
import org.springframework.ai.chat.messages.Message;
3435
import org.springframework.ai.chat.model.ChatModel;
@@ -60,13 +61,24 @@ public class DefaultChatClientBuilder implements Builder {
6061
this(chatModel, ObservationRegistry.NOOP, null);
6162
}
6263

64+
/**
65+
* @deprecated in favor of
66+
* {@link #DefaultChatClientBuilder(ChatModel, ObservationRegistry, ChatClientObservationConvention, AdvisorObservationConvention)}.
67+
*/
68+
@Deprecated(since = "1.1.0", forRemoval = true)
6369
public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observationRegistry,
64-
@Nullable ChatClientObservationConvention customObservationConvention) {
70+
@Nullable ChatClientObservationConvention chatClientObservationConvention) {
71+
this(chatModel, observationRegistry, chatClientObservationConvention, null);
72+
}
73+
74+
public DefaultChatClientBuilder(ChatModel chatModel, ObservationRegistry observationRegistry,
75+
@Nullable ChatClientObservationConvention chatClientObservationConvention,
76+
@Nullable AdvisorObservationConvention advisorObservationConvention) {
6577
Assert.notNull(chatModel, "the " + ChatModel.class.getName() + " must be non-null");
6678
Assert.notNull(observationRegistry, "the " + ObservationRegistry.class.getName() + " must be non-null");
6779
this.defaultRequest = new DefaultChatClientRequestSpec(chatModel, null, Map.of(), Map.of(), null, Map.of(),
6880
Map.of(), List.of(), List.of(), List.of(), List.of(), List.of(), null, List.of(), Map.of(),
69-
observationRegistry, customObservationConvention, Map.of(), null);
81+
observationRegistry, chatClientObservationConvention, Map.of(), null, advisorObservationConvention);
7082
}
7183

7284
public ChatClient build() {

spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
2626
import reactor.core.publisher.Flux;
2727

28+
import org.springframework.ai.chat.client.ChatClientMessageAggregator;
2829
import org.springframework.ai.chat.client.ChatClientRequest;
2930
import org.springframework.ai.chat.client.ChatClientResponse;
3031
import org.springframework.ai.chat.client.advisor.api.Advisor;
@@ -37,6 +38,7 @@
3738
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationDocumentation;
3839
import org.springframework.ai.chat.client.advisor.observation.DefaultAdvisorObservationConvention;
3940
import org.springframework.core.OrderComparator;
41+
import org.springframework.lang.Nullable;
4042
import org.springframework.util.Assert;
4143
import org.springframework.util.CollectionUtils;
4244

@@ -54,6 +56,8 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain {
5456

5557
public static final AdvisorObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultAdvisorObservationConvention();
5658

59+
private static final ChatClientMessageAggregator CHAT_CLIENT_MESSAGE_AGGREGATOR = new ChatClientMessageAggregator();
60+
5761
private final List<CallAdvisor> originalCallAdvisors;
5862

5963
private final List<StreamAdvisor> originalStreamAdvisors;
@@ -64,8 +68,10 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain {
6468

6569
private final ObservationRegistry observationRegistry;
6670

71+
private final AdvisorObservationConvention observationConvention;
72+
6773
DefaultAroundAdvisorChain(ObservationRegistry observationRegistry, Deque<CallAdvisor> callAdvisors,
68-
Deque<StreamAdvisor> streamAdvisors) {
74+
Deque<StreamAdvisor> streamAdvisors, @Nullable AdvisorObservationConvention observationConvention) {
6975

7076
Assert.notNull(observationRegistry, "the observationRegistry must be non-null");
7177
Assert.notNull(callAdvisors, "the callAdvisors must be non-null");
@@ -76,6 +82,8 @@ public class DefaultAroundAdvisorChain implements BaseAdvisorChain {
7682
this.streamAdvisors = streamAdvisors;
7783
this.originalCallAdvisors = List.copyOf(callAdvisors);
7884
this.originalStreamAdvisors = List.copyOf(streamAdvisors);
85+
this.observationConvention = observationConvention != null ? observationConvention
86+
: DEFAULT_OBSERVATION_CONVENTION;
7987
}
8088

8189
public static Builder builder(ObservationRegistry observationRegistry) {
@@ -99,8 +107,13 @@ public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {
99107
.build();
100108

101109
return AdvisorObservationDocumentation.AI_ADVISOR
102-
.observation(null, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry)
103-
.observe(() -> advisor.adviseCall(chatClientRequest, this));
110+
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
111+
this.observationRegistry)
112+
.observe(() -> {
113+
var chatClientResponse = advisor.adviseCall(chatClientRequest, this);
114+
observationContext.setChatClientResponse(chatClientResponse);
115+
return chatClientResponse;
116+
});
104117
}
105118

106119
@Override
@@ -120,17 +133,19 @@ public Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest)
120133
.order(advisor.getOrder())
121134
.build();
122135

123-
var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(null,
136+
var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention,
124137
DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);
125138

126139
observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();
127140

128141
// @formatter:off
129-
return Flux.defer(() -> advisor.adviseStream(chatClientRequest, this)
142+
Flux<ChatClientResponse> chatClientResponse = Flux.defer(() -> advisor.adviseStream(chatClientRequest, this)
130143
.doOnError(observation::error)
131144
.doFinally(s -> observation.stop())
132145
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation)));
133146
// @formatter:on
147+
return CHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse,
148+
observationContext::setChatClientResponse);
134149
});
135150
}
136151

@@ -175,12 +190,20 @@ public static final class Builder {
175190

176191
private final Deque<StreamAdvisor> streamAdvisors;
177192

193+
@Nullable
194+
private AdvisorObservationConvention observationConvention;
195+
178196
public Builder(ObservationRegistry observationRegistry) {
179197
this.observationRegistry = observationRegistry;
180198
this.callAdvisors = new ConcurrentLinkedDeque<>();
181199
this.streamAdvisors = new ConcurrentLinkedDeque<>();
182200
}
183201

202+
public Builder observationConvention(@Nullable AdvisorObservationConvention observationConvention) {
203+
this.observationConvention = observationConvention;
204+
return this;
205+
}
206+
184207
public Builder push(Advisor advisor) {
185208
Assert.notNull(advisor, "the advisor must be non-null");
186209
return this.pushAll(List.of(advisor));
@@ -229,7 +252,8 @@ private void reOrder() {
229252
}
230253

231254
public DefaultAroundAdvisorChain build() {
232-
return new DefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors);
255+
return new DefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors,
256+
this.observationConvention);
233257
}
234258

235259
}

spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/DefaultChatClientBuilderTests.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.nio.charset.Charset;
2020

21+
import io.micrometer.observation.ObservationRegistry;
2122
import org.junit.jupiter.api.Test;
2223

2324
import org.springframework.ai.chat.model.ChatModel;
@@ -63,6 +64,12 @@ void whenObservationRegistryIsNullThenThrows() {
6364
.hasMessage("the io.micrometer.observation.ObservationRegistry must be non-null");
6465
}
6566

67+
@Test
68+
void whenAdvisorObservationConventionIsNullThenReturn() {
69+
var builder = new DefaultChatClientBuilder(mock(ChatModel.class), mock(ObservationRegistry.class), null, null);
70+
assertThat(builder).isNotNull();
71+
}
72+
6673
@Test
6774
void whenUserResourceIsNullThenThrows() {
6875
DefaultChatClientBuilder builder = new DefaultChatClientBuilder(mock(ChatModel.class));

0 commit comments

Comments
 (0)