Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
Expand Down Expand Up @@ -71,6 +72,11 @@ private static void logPromptContentWarning() {
"You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
}

private static void logCompletionWarning() {
logger.warn(
"You have enabled logging out the ChatClient completion content with the risk of exposing sensitive or private information. Please, be careful!");
}

@Bean
@ConditionalOnMissingBean
ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClientCustomizer> customizerProvider) {
Expand Down Expand Up @@ -108,6 +114,17 @@ TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientPr
return new TracingAwareLoggingObservationHandler<>(new ChatClientPromptContentObservationHandler(), tracer);
}

@Bean
@ConditionalOnMissingBean(value = ChatClientCompletionObservationHandler.class,
name = "chatClientCompletionObservationHandler")
@ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX + ".observations",
name = "log-completion", havingValue = "true")
TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientCompletionObservationHandler(
Tracer tracer) {
logCompletionWarning();
return new TracingAwareLoggingObservationHandler<>(new ChatClientCompletionObservationHandler(), tracer);
}

}

@Configuration(proxyBeanMethods = false)
Expand All @@ -123,6 +140,15 @@ ChatClientPromptContentObservationHandler chatClientPromptContentObservationHand
return new ChatClientPromptContentObservationHandler();
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX + ".observations",
name = "log-completion", havingValue = "true")
ChatClientCompletionObservationHandler chatClientCompletionObservationHandler() {
logCompletionWarning();
return new ChatClientCompletionObservationHandler();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* @author Josh Long
* @author Arjen Poutsma
* @author Thomas Vitale
* @author Jonatan Ivanov
* @since 1.0.0
*/
@ConfigurationProperties(ChatClientBuilderProperties.CONFIG_PREFIX)
Expand Down Expand Up @@ -59,14 +60,36 @@ public static class Observations {
*/
private boolean logPrompt = false;

/**
* Whether to log the completion content in the observations.
* @since 1.1.0
*/
private boolean logCompletion = false;

public boolean isLogPrompt() {
return this.logPrompt;
}

/**
* @return Whether logging completion data is enabled or not.
* @since 1.1.0
*/
public boolean isLogCompletion() {
return this.logCompletion;
}

public void setLogPrompt(boolean logPrompt) {
this.logPrompt = logPrompt;
}

/**
* @param logCompletion should completion data logging be enabled or not.
* @since 1.1.0
*/
public void setLogCompletion(boolean logCompletion) {
this.logCompletion = logCompletion;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
Expand Down Expand Up @@ -48,16 +49,18 @@ class ChatClientObservationAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class));

@Test
void promptContentHandlerNoTracer() {
void handlersNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void promptContentHandlerWithTracer() {
void handlersWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

Expand All @@ -66,6 +69,7 @@ void promptContentHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
Expand All @@ -76,6 +80,7 @@ void promptContentHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!");
Expand All @@ -86,6 +91,7 @@ void promptContentHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

Expand All @@ -94,6 +100,47 @@ void promptContentHandlerDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void completionHandlerEnabledNoTracer(CapturedOutput output) {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.hasSingleBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient completion content with the risk of exposing sensitive or private information. Please, be careful!");
}

@Test
void completionHandlerEnabledWithTracer(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.hasSingleBean(TracingAwareLoggingObservationHandler.class));
assertThat(output).contains(
"You have enabled logging out the ChatClient completion content with the risk of exposing sensitive or private information. Please, be careful!");
}

@Test
void completionHandlerDisabledNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withPropertyValues("spring.ai.chat.client.observations.log-completion=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void completionDisabledWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-completion=false")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

Expand All @@ -104,6 +151,7 @@ void customChatClientPromptContentObservationHandlerNoTracer() {
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.hasBean("customChatClientPromptContentObservationHandler")
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

Expand All @@ -114,20 +162,61 @@ void customChatClientPromptContentObservationHandlerWithTracer() {
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> assertThat(context).hasSingleBean(ChatClientPromptContentObservationHandler.class)
.hasBean("customChatClientPromptContentObservationHandler")
.doesNotHaveBean(ChatClientCompletionObservationHandler.class)
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void customTracingAwareLoggingObservationHandler() {
void customTracingAwareLoggingObservationHandlerForChatClientPromptContent() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomTracingAwareLoggingObservationHandlerConfiguration.class)
.withUserConfiguration(
CustomTracingAwareLoggingObservationHandlerForChatClientPromptContentConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-prompt=true")
.run(context -> {
assertThat(context).hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("chatClientPromptContentObservationHandler")
.doesNotHaveBean(ChatClientPromptContentObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class))
.isSameAs(CustomTracingAwareLoggingObservationHandlerConfiguration.handlerInstance);
.doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class)).isSameAs(
CustomTracingAwareLoggingObservationHandlerForChatClientPromptContentConfiguration.handlerInstance);
});
}

@Test
void customChatClientCompletionObservationHandlerNoTracer() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Tracer.class))
.withUserConfiguration(CustomChatClientCompletionObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.hasSingleBean(ChatClientCompletionObservationHandler.class)
.hasBean("customChatClientCompletionObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void customChatClientCompletionObservationHandlerWithTracer() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(CustomChatClientCompletionObservationHandlerConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-completion=true")
.run(context -> assertThat(context).doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.hasSingleBean(ChatClientCompletionObservationHandler.class)
.hasBean("customChatClientCompletionObservationHandler")
.doesNotHaveBean(TracingAwareLoggingObservationHandler.class));
}

@Test
void customTracingAwareLoggingObservationHandlerForChatClientCompletion() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withUserConfiguration(
CustomTracingAwareLoggingObservationHandlerForChatClientChatClientCompletionConfiguration.class)
.withPropertyValues("spring.ai.chat.client.observations.log-completion=true")
.run(context -> {
assertThat(context).hasSingleBean(TracingAwareLoggingObservationHandler.class)
.hasBean("chatClientCompletionObservationHandler")
.doesNotHaveBean(ChatClientPromptContentObservationHandler.class)
.doesNotHaveBean(ChatClientCompletionObservationHandler.class);
assertThat(context.getBean(TracingAwareLoggingObservationHandler.class)).isSameAs(
CustomTracingAwareLoggingObservationHandlerForChatClientChatClientCompletionConfiguration.handlerInstance);
});
}

Expand All @@ -152,7 +241,7 @@ ChatClientPromptContentObservationHandler customChatClientPromptContentObservati
}

@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerConfiguration {
static class CustomTracingAwareLoggingObservationHandlerForChatClientPromptContentConfiguration {

static TracingAwareLoggingObservationHandler<ChatClientObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ChatClientPromptContentObservationHandler(), null);
Expand All @@ -164,4 +253,27 @@ TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientPr

}

@Configuration(proxyBeanMethods = false)
static class CustomChatClientCompletionObservationHandlerConfiguration {

@Bean
ChatClientCompletionObservationHandler customChatClientCompletionObservationHandler() {
return new ChatClientCompletionObservationHandler();
}

}

@Configuration(proxyBeanMethods = false)
static class CustomTracingAwareLoggingObservationHandlerForChatClientChatClientCompletionConfiguration {

static TracingAwareLoggingObservationHandler<ChatClientObservationContext> handlerInstance = new TracingAwareLoggingObservationHandler<>(
new ChatClientCompletionObservationHandler(), null);

@Bean
TracingAwareLoggingObservationHandler<ChatClientObservationContext> chatClientCompletionObservationHandler() {
return handlerInstance;
}

}

}
6 changes: 6 additions & 0 deletions spring-ai-client-chat/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-observation-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
* @author Soby Chacko
* @author Dariusz Jedrzejczyk
* @author Thomas Vitale
* @author Jonatan Ivanov
* @since 1.0.0
*/
public class DefaultChatClient implements ChatClient {
Expand All @@ -84,6 +85,8 @@ public class DefaultChatClient implements ChatClient {

private static final TemplateRenderer DEFAULT_TEMPLATE_RENDERER = StTemplateRenderer.builder().build();

private static final ChatClientMessageAggregator CHAT_CLIENT_MESSAGE_AGGREGATOR = new ChatClientMessageAggregator();

private final DefaultChatClientRequestSpec defaultChatClientRequest;

public DefaultChatClient(DefaultChatClientRequestSpec defaultChatClientRequest) {
Expand Down Expand Up @@ -513,7 +516,9 @@ private ChatClientResponse doGetObservableChatClientResponse(ChatClientRequest c
// CHECKSTYLE:OFF
var chatClientResponse = observation.observe(() -> {
// Apply the advisor chain that terminates with the ChatModelCallAdvisor.
return this.advisorChain.nextCall(chatClientRequest);
var response = this.advisorChain.nextCall(chatClientRequest);
observationContext.setResponse(response);
return response;
});
// CHECKSTYLE:ON
return chatClientResponse != null ? chatClientResponse : ChatClientResponse.builder().build();
Expand Down Expand Up @@ -571,11 +576,13 @@ private Flux<ChatClientResponse> doGetObservableFluxChatResponse(ChatClientReque

// @formatter:off
// Apply the advisor chain that terminates with the ChatModelStreamAdvisor.
return this.advisorChain.nextStream(chatClientRequest)
Flux<ChatClientResponse> chatClientResponse = this.advisorChain.nextStream(chatClientRequest)
.doOnError(observation::error)
.doFinally(s -> observation.stop())
.contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
// @formatter:on
return CHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse,
observationContext::setResponse);
});
}

Expand Down
Loading