Improve chat client and advisor observations
* Make ChatClient and Advisor observation logic null-safe * Simplify naming for Advisor observations * Include high-cardinality attributes only if a value is present * Fix condition to include system test to chat client observations * Add Advisor order information to context * Streamline usage of enums and utils to reduce hard-coded/duplications * Fix pending Chroma integration test Signed-off-by: Thomas Vitale <ThomasVitale@users.noreply.github.com>
This commit is contained in:
committed by
Christian Tzolov
parent
8ea56329ac
commit
1ee90944de
@@ -77,6 +77,7 @@ import reactor.core.scheduler.Schedulers;
|
||||
* @author Arjen Poutsma
|
||||
* @author Soby Chacko
|
||||
* @author Dariusz Jedrzejczyk
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class DefaultChatClient implements ChatClient {
|
||||
@@ -360,8 +361,11 @@ public class DefaultChatClient implements ChatClient {
|
||||
private ChatResponse doGetObservableChatResponse(DefaultChatClientRequestSpec inputRequest,
|
||||
String formatParam) {
|
||||
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(inputRequest,
|
||||
formatParam, false);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(inputRequest)
|
||||
.withFormat(formatParam)
|
||||
.withStream(false)
|
||||
.build();
|
||||
|
||||
var observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(
|
||||
inputRequest.getCustomObservationConvention(), DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION,
|
||||
@@ -407,8 +411,10 @@ public class DefaultChatClient implements ChatClient {
|
||||
private Flux<ChatResponse> doGetObservableFluxChatResponse(DefaultChatClientRequestSpec inputRequest) {
|
||||
return Flux.deferContextual(contextView -> {
|
||||
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(inputRequest, "",
|
||||
true);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(inputRequest)
|
||||
.withStream(true)
|
||||
.build();
|
||||
|
||||
Observation observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(
|
||||
inputRequest.getCustomObservationConvention(), DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION,
|
||||
|
||||
@@ -85,6 +85,7 @@ public class DefaultAroundAdvisorChain implements CallAroundAdvisorChain, Stream
|
||||
.withAdvisorType(AdvisorObservationContext.Type.AROUND)
|
||||
.withAdvisedRequest(advisedRequest)
|
||||
.withAdvisorRequestContext(advisedRequest.adviseContext())
|
||||
.withOrder(advisor.getOrder())
|
||||
.build();
|
||||
|
||||
return AdvisorObservationDocumentation.AI_ADVISOR
|
||||
@@ -106,6 +107,7 @@ public class DefaultAroundAdvisorChain implements CallAroundAdvisorChain, Stream
|
||||
.withAdvisorType(AdvisorObservationContext.Type.AROUND)
|
||||
.withAdvisedRequest(advisedRequest)
|
||||
.withAdvisorRequestContext(advisedRequest.adviseContext())
|
||||
.withOrder(advisor.getOrder())
|
||||
.build();
|
||||
|
||||
var observation = AdvisorObservationDocumentation.AI_ADVISOR.observation(null,
|
||||
|
||||
@@ -20,15 +20,18 @@ import java.util.Map;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
|
||||
/**
|
||||
* Context used to store metadata for chat client advisors.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
public class AdvisorObservationContext extends Observation.Context {
|
||||
|
||||
public enum Type {
|
||||
@@ -37,35 +40,48 @@ public class AdvisorObservationContext extends Observation.Context {
|
||||
|
||||
}
|
||||
|
||||
private String advisorName;
|
||||
private final String advisorName;
|
||||
|
||||
private Type advisorType;
|
||||
private final Type advisorType;
|
||||
|
||||
/**
|
||||
* The {@link AdvisedRequest} data to be advised. Represents the row
|
||||
* {@link ChatClient.ChatClientRequestSpec} data before sealed into a {@link Prompt}.
|
||||
*/
|
||||
@Nullable
|
||||
private AdvisedRequest advisorRequest;
|
||||
|
||||
/**
|
||||
* The shared data between the advisors in the chain. It is shared between all request
|
||||
* and response advising points of all advisors in the chain.
|
||||
*/
|
||||
@Nullable
|
||||
private Map<String, Object> advisorRequestContext;
|
||||
|
||||
/**
|
||||
* the shared data between the advisors in the chain. It is shared between all request
|
||||
* and response advising points of all advisors in the chain.
|
||||
*/
|
||||
@Nullable
|
||||
private Map<String, Object> advisorResponseContext;
|
||||
|
||||
/**
|
||||
* The order of the advisor in the advisor chain.
|
||||
*/
|
||||
private int order;
|
||||
private final int order;
|
||||
|
||||
public AdvisorObservationContext(String advisorName, Type advisorType, @Nullable AdvisedRequest advisorRequest,
|
||||
@Nullable Map<String, Object> advisorRequestContext, @Nullable Map<String, Object> advisorResponseContext,
|
||||
int order) {
|
||||
Assert.hasText(advisorName, "advisorName must not be null or empty");
|
||||
Assert.notNull(advisorType, "advisorType must not be null");
|
||||
|
||||
public void setAdvisorName(String advisorName) {
|
||||
this.advisorName = advisorName;
|
||||
this.advisorType = advisorType;
|
||||
this.advisorRequest = advisorRequest;
|
||||
this.advisorRequestContext = advisorRequestContext;
|
||||
this.advisorResponseContext = advisorResponseContext;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public String getAdvisorName() {
|
||||
@@ -76,31 +92,30 @@ public class AdvisorObservationContext extends Observation.Context {
|
||||
return this.advisorType;
|
||||
}
|
||||
|
||||
public void setAdvisorType(Type type) {
|
||||
this.advisorType = type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public AdvisedRequest getAdvisedRequest() {
|
||||
return this.advisorRequest;
|
||||
}
|
||||
|
||||
public void setAdvisedRequest(AdvisedRequest advisedRequest) {
|
||||
public void setAdvisedRequest(@Nullable AdvisedRequest advisedRequest) {
|
||||
this.advisorRequest = advisedRequest;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, Object> getAdvisorRequestContext() {
|
||||
return this.advisorRequestContext;
|
||||
}
|
||||
|
||||
public void setAdvisorRequestContext(Map<String, Object> advisorRequestContext) {
|
||||
public void setAdvisorRequestContext(@Nullable Map<String, Object> advisorRequestContext) {
|
||||
this.advisorRequestContext = advisorRequestContext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, Object> getAdvisorResponseContext() {
|
||||
return this.advisorResponseContext;
|
||||
}
|
||||
|
||||
public void setAdvisorResponseContext(Map<String, Object> advisorResponseContext) {
|
||||
public void setAdvisorResponseContext(@Nullable Map<String, Object> advisorResponseContext) {
|
||||
this.advisorResponseContext = advisorResponseContext;
|
||||
}
|
||||
|
||||
@@ -108,52 +123,57 @@ public class AdvisorObservationContext extends Observation.Context {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final AdvisorObservationContext context = new AdvisorObservationContext();
|
||||
private String advisorName;
|
||||
|
||||
private Type advisorType;
|
||||
|
||||
private AdvisedRequest advisorRequest;
|
||||
|
||||
private Map<String, Object> advisorRequestContext;
|
||||
|
||||
private Map<String, Object> advisorResponseContext;
|
||||
|
||||
private int order = 0;
|
||||
|
||||
public Builder withAdvisorName(String advisorName) {
|
||||
this.context.setAdvisorName(advisorName);
|
||||
this.advisorName = advisorName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAdvisorType(Type advisorType) {
|
||||
this.context.setAdvisorType(advisorType);
|
||||
this.advisorType = advisorType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAdvisedRequest(AdvisedRequest advisedRequest) {
|
||||
this.context.setAdvisedRequest(advisedRequest);
|
||||
this.advisorRequest = advisedRequest;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAdvisorRequestContext(Map<String, Object> advisorRequestContext) {
|
||||
this.context.setAdvisorRequestContext(advisorRequestContext);
|
||||
this.advisorRequestContext = advisorRequestContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withAdvisorResponseContext(Map<String, Object> advisorResponseContext) {
|
||||
this.context.setAdvisorResponseContext(advisorResponseContext);
|
||||
this.advisorResponseContext = advisorResponseContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withOrder(int order) {
|
||||
this.context.setOrder(order);
|
||||
this.order = order;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvisorObservationContext build() {
|
||||
Assert.hasText(this.context.advisorName, "The advisorName must not be empty!");
|
||||
Assert.notNull(this.context.advisorType, "The advisorType must not be null!");
|
||||
return this.context;
|
||||
return new AdvisorObservationContext(advisorName, advisorType, advisorRequest, advisorRequestContext,
|
||||
advisorResponseContext, order);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
|
||||
/**
|
||||
* Interface for an {@link ObservationConvention} for chat client advisors.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ import io.micrometer.observation.docs.ObservationDocumentation;
|
||||
public enum AdvisorObservationDocumentation implements ObservationDocumentation {
|
||||
|
||||
/**
|
||||
* AI Chat Client observations
|
||||
* AI Advisor observations
|
||||
*/
|
||||
AI_ADVISOR {
|
||||
@Override
|
||||
@@ -65,7 +65,7 @@ public enum AdvisorObservationDocumentation implements ObservationDocumentation
|
||||
ADVISOR_TYPE {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "spring.ai.chat.client.advisor.type";
|
||||
return "spring.ai.advisor.type";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +74,12 @@ public enum AdvisorObservationDocumentation implements ObservationDocumentation
|
||||
public enum HighCardinalityKeyNames implements KeyName {
|
||||
|
||||
/**
|
||||
* Chat Model name.
|
||||
* Advisor name.
|
||||
*/
|
||||
ADVISOR_NAME {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "spring.ai.chat.client.advisor.name";
|
||||
return "spring.ai.advisor.name";
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -88,7 +88,7 @@ public enum AdvisorObservationDocumentation implements ObservationDocumentation
|
||||
ADVISOR_ORDER {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "spring.ai.chat.client.advisor.order";
|
||||
return "spring.ai.advisor.order";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.ai.chat.client.advisor.observation;
|
||||
|
||||
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationDocumentation.HighCardinalityKeyNames;
|
||||
import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationDocumentation.LowCardinalityKeyNames;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
import org.springframework.ai.util.ParsingUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -27,18 +28,9 @@ import io.micrometer.common.KeyValues;
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
public class DefaultAdvisorObservationConvention implements AdvisorObservationConvention {
|
||||
|
||||
public static final String DEFAULT_NAME = "spring.ai.chat.client.advisor";
|
||||
|
||||
private static final String CHAT_CLIENT_ADVISOR_SPRING_AI_KIND = "chat_client_advisor";
|
||||
|
||||
private static final KeyValue ADVISOR_TYPE_NONE = KeyValue.of(LowCardinalityKeyNames.ADVISOR_TYPE,
|
||||
KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue ADVISOR_NAME_NONE = KeyValue.of(HighCardinalityKeyNames.ADVISOR_NAME,
|
||||
KeyValue.NONE_VALUE);
|
||||
public static final String DEFAULT_NAME = "spring.ai.advisor";
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -73,15 +65,12 @@ public class DefaultAdvisorObservationConvention implements AdvisorObservationCo
|
||||
}
|
||||
|
||||
protected KeyValue advisorType(AdvisorObservationContext context) {
|
||||
if (context.getAdvisorType() != null) {
|
||||
return KeyValue.of(LowCardinalityKeyNames.ADVISOR_TYPE, context.getAdvisorType().name());
|
||||
}
|
||||
return ADVISOR_TYPE_NONE;
|
||||
return KeyValue.of(LowCardinalityKeyNames.ADVISOR_TYPE, context.getAdvisorType().name());
|
||||
}
|
||||
|
||||
protected KeyValue springAiKind() {
|
||||
return KeyValue.of(AdvisorObservationDocumentation.LowCardinalityKeyNames.SPRING_AI_KIND,
|
||||
CHAT_CLIENT_ADVISOR_SPRING_AI_KIND);
|
||||
SpringAiKind.ADVISOR.value());
|
||||
}
|
||||
|
||||
// ------------------------
|
||||
@@ -94,14 +83,11 @@ public class DefaultAdvisorObservationConvention implements AdvisorObservationCo
|
||||
}
|
||||
|
||||
protected KeyValue advisorName(AdvisorObservationContext context) {
|
||||
if (context.getAdvisorType() != null) {
|
||||
return KeyValue.of(HighCardinalityKeyNames.ADVISOR_NAME, context.getAdvisorName());
|
||||
}
|
||||
return ADVISOR_NAME_NONE;
|
||||
return KeyValue.of(HighCardinalityKeyNames.ADVISOR_NAME, context.getAdvisorName());
|
||||
}
|
||||
|
||||
protected KeyValue advisorOrder(AdvisorObservationContext context) {
|
||||
return KeyValue.of(HighCardinalityKeyNames.ADVISOR_ORDER, "" + context.getOrder());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.ai.chat.client.advisor.observation;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
package org.springframework.ai.chat.client.observation;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.ai.observation.tracing.TracingHelper;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -32,72 +31,54 @@ import io.micrometer.observation.ObservationFilter;
|
||||
*/
|
||||
public class ChatClientInputContentObservationFilter implements ObservationFilter {
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_SYSTEM_TEXT_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_TEXT, KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_SYSTEM_PARAM_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_PARAM, KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_USER_TEXT_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_TEXT, KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_USER_PARAM_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_PARAMS, KeyValue.NONE_VALUE);
|
||||
|
||||
@Override
|
||||
public Observation.Context map(Observation.Context context) {
|
||||
if (!(context instanceof ChatClientObservationContext chatClientObservationContext)) {
|
||||
return context;
|
||||
}
|
||||
|
||||
chatClientObservationContext.addHighCardinalityKeyValue(chatClientSystemText(chatClientObservationContext))
|
||||
.addHighCardinalityKeyValue(chatClientSystemParam(chatClientObservationContext))
|
||||
.addHighCardinalityKeyValue(chatClientUserText(chatClientObservationContext))
|
||||
.addHighCardinalityKeyValue(chatClientUserParam(chatClientObservationContext));
|
||||
chatClientSystemText(chatClientObservationContext);
|
||||
chatClientSystemParams(chatClientObservationContext);
|
||||
chatClientUserText(chatClientObservationContext);
|
||||
chatClientUserParams(chatClientObservationContext);
|
||||
|
||||
return chatClientObservationContext;
|
||||
}
|
||||
|
||||
protected KeyValue chatClientSystemText(ChatClientObservationContext context) {
|
||||
if (!StringUtils.hasText(context.getRequest().getUserText())) {
|
||||
return CHAT_CLIENT_SYSTEM_TEXT_NONE;
|
||||
protected void chatClientSystemText(ChatClientObservationContext context) {
|
||||
if (!StringUtils.hasText(context.getRequest().getSystemText())) {
|
||||
return;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_TEXT,
|
||||
context.getRequest().getSystemText());
|
||||
context.addHighCardinalityKeyValue(
|
||||
KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_TEXT,
|
||||
context.getRequest().getSystemText()));
|
||||
}
|
||||
|
||||
protected KeyValue chatClientSystemParam(ChatClientObservationContext context) {
|
||||
protected void chatClientSystemParams(ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getSystemParams())) {
|
||||
return CHAT_CLIENT_SYSTEM_PARAM_NONE;
|
||||
return;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_PARAM,
|
||||
context.getRequest()
|
||||
.getSystemParams()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> "\"" + e.getKey() + "\":\"" + e.getValue() + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
context.addHighCardinalityKeyValue(
|
||||
KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_SYSTEM_PARAM,
|
||||
TracingHelper.concatenateMaps(context.getRequest().getSystemParams())));
|
||||
}
|
||||
|
||||
protected KeyValue chatClientUserText(ChatClientObservationContext context) {
|
||||
protected void chatClientUserText(ChatClientObservationContext context) {
|
||||
if (!StringUtils.hasText(context.getRequest().getUserText())) {
|
||||
return CHAT_CLIENT_USER_TEXT_NONE;
|
||||
return;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_TEXT,
|
||||
context.getRequest().getUserText());
|
||||
context.addHighCardinalityKeyValue(
|
||||
KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_TEXT,
|
||||
context.getRequest().getUserText()));
|
||||
}
|
||||
|
||||
protected KeyValue chatClientUserParam(ChatClientObservationContext context) {
|
||||
protected void chatClientUserParams(ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getUserParams())) {
|
||||
return CHAT_CLIENT_USER_PARAM_NONE;
|
||||
return;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_PARAMS,
|
||||
context.getRequest()
|
||||
.getUserParams()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> "\"" + e.getKey() + "\":\"" + e.getValue() + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
context.addHighCardinalityKeyValue(
|
||||
KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_USER_PARAMS,
|
||||
TracingHelper.concatenateMaps(context.getRequest().getUserParams())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,25 +21,30 @@ import org.springframework.ai.observation.conventions.AiOperationType;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
|
||||
import io.micrometer.observation.Observation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Context used to store metadata for chat client workflows.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
public class ChatClientObservationContext extends Observation.Context {
|
||||
|
||||
private final boolean stream;
|
||||
|
||||
private final String format;
|
||||
|
||||
private final DefaultChatClientRequestSpec request;
|
||||
|
||||
private final AiOperationMetadata operationMetadata = new AiOperationMetadata(AiOperationType.FRAMEWORK.value(),
|
||||
AiProvider.SPRING_AI.value());
|
||||
|
||||
public ChatClientObservationContext(DefaultChatClientRequestSpec requestSpec, String format, Boolean isStream) {
|
||||
private final boolean stream;
|
||||
|
||||
@Nullable
|
||||
private String format;
|
||||
|
||||
ChatClientObservationContext(DefaultChatClientRequestSpec requestSpec, String format, boolean isStream) {
|
||||
Assert.notNull(requestSpec, "requestSpec cannot be null");
|
||||
this.request = requestSpec;
|
||||
this.format = format;
|
||||
this.stream = isStream;
|
||||
@@ -57,8 +62,49 @@ public class ChatClientObservationContext extends Observation.Context {
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getFormat() {
|
||||
return this.format;
|
||||
}
|
||||
|
||||
}
|
||||
public void setFormat(@Nullable String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private DefaultChatClientRequestSpec request;
|
||||
|
||||
private String format;
|
||||
|
||||
private boolean isStream = false;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
public Builder withRequest(DefaultChatClientRequestSpec request) {
|
||||
this.request = request;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withFormat(String format) {
|
||||
this.format = format;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withStream(boolean isStream) {
|
||||
this.isStream = isStream;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatClientObservationContext build() {
|
||||
return new ChatClientObservationContext(this.request, this.format, this.isStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationConvention;
|
||||
|
||||
/**
|
||||
* Interface for an {@link ObservationConvention} for chat client workflows.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
public interface ChatClientObservationConvention extends ObservationConvention<ChatClientObservationContext> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,6 +21,8 @@ import io.micrometer.observation.ObservationConvention;
|
||||
import io.micrometer.observation.docs.ObservationDocumentation;
|
||||
|
||||
/**
|
||||
* Documented conventions for chat client observations.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@@ -88,7 +90,7 @@ public enum ChatClientObservationDocumentation implements ObservationDocumentati
|
||||
CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS {
|
||||
@Override
|
||||
public String asString() {
|
||||
return "spring.ai.chat.client.tool.functioncallbacks";
|
||||
return "spring.ai.chat.client.tool.function.callbacks";
|
||||
}
|
||||
},
|
||||
|
||||
@@ -149,4 +151,4 @@ public enum ChatClientObservationDocumentation implements ObservationDocumentati
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.springframework.ai.chat.client.observation;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.client.observation.ChatClientObservationDocumentation.LowCardinalityKeyNames;
|
||||
import org.springframework.ai.chat.observation.ChatModelObservationDocumentation;
|
||||
import org.springframework.ai.model.function.FunctionCallback;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
import org.springframework.ai.observation.tracing.TracingHelper;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@@ -26,30 +28,16 @@ import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.common.KeyValues;
|
||||
|
||||
/**
|
||||
* Default conventions to populate observations for chat client workflows.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
public class DefaultChatClientObservationConvention implements ChatClientObservationConvention {
|
||||
|
||||
public static final String DEFAULT_NAME = "spring.ai.chat.client";
|
||||
|
||||
private static final String CHAT_CLIENT_SPRING_AI_KIND = "chat_client";
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS_NONE = KeyValue.of(
|
||||
ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS,
|
||||
KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_TOOL_FUNCTION_NAMES_NONE = KeyValue.of(
|
||||
ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_NAMES,
|
||||
KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_ADVISOR_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISORS, KeyValue.NONE_VALUE);
|
||||
|
||||
private static final KeyValue CHAT_CLIENT_ADVISOR_PARAM_NONE = KeyValue
|
||||
.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISOR_PARAMS, KeyValue.NONE_VALUE);
|
||||
|
||||
private final String name;
|
||||
|
||||
public DefaultChatClientObservationConvention() {
|
||||
@@ -68,27 +56,12 @@ public class DefaultChatClientObservationConvention implements ChatClientObserva
|
||||
@Override
|
||||
@Nullable
|
||||
public String getContextualName(ChatClientObservationContext context) {
|
||||
return "%s %s".formatted(context.getOperationMetadata().provider(), CHAT_CLIENT_SPRING_AI_KIND);
|
||||
return "%s %s".formatted(context.getOperationMetadata().provider(), SpringAiKind.CHAT_CLIENT.value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ChatClientObservationContext context) {
|
||||
return KeyValues.of(springAiKind(), aiOperationType(context), aiProvider(context), stream(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context) {
|
||||
return KeyValues.of(toolFunctionNames(context), toolFunctionCallbacks(context), chatClientAdvisor(context),
|
||||
chatClientAdvisorParam(context));
|
||||
}
|
||||
|
||||
protected KeyValue springAiKind() {
|
||||
return KeyValue.of(ChatClientObservationDocumentation.LowCardinalityKeyNames.SPRING_AI_KIND,
|
||||
CHAT_CLIENT_SPRING_AI_KIND);
|
||||
}
|
||||
|
||||
protected KeyValue stream(ChatClientObservationContext context) {
|
||||
return KeyValue.of(LowCardinalityKeyNames.STREAM, "" + context.isStream());
|
||||
return KeyValues.of(aiOperationType(context), aiProvider(context), springAiKind(), stream(context));
|
||||
}
|
||||
|
||||
protected KeyValue aiOperationType(ChatClientObservationContext context) {
|
||||
@@ -101,55 +74,66 @@ public class DefaultChatClientObservationConvention implements ChatClientObserva
|
||||
context.getOperationMetadata().provider());
|
||||
}
|
||||
|
||||
protected KeyValue toolFunctionNames(ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getFunctionNames())) {
|
||||
return CHAT_CLIENT_TOOL_FUNCTION_NAMES_NONE;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_NAMES,
|
||||
context.getRequest()
|
||||
.getFunctionNames()
|
||||
.stream()
|
||||
.map(v -> "\"" + v + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
protected KeyValue springAiKind() {
|
||||
return KeyValue.of(ChatClientObservationDocumentation.LowCardinalityKeyNames.SPRING_AI_KIND,
|
||||
SpringAiKind.CHAT_CLIENT.value());
|
||||
}
|
||||
|
||||
protected KeyValue toolFunctionCallbacks(ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getFunctionCallbacks())) {
|
||||
return CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS_NONE;
|
||||
}
|
||||
|
||||
return KeyValue.of(
|
||||
ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS,
|
||||
context.getRequest()
|
||||
.getFunctionCallbacks()
|
||||
.stream()
|
||||
.map(fc -> "\"" + fc.getName() + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
protected KeyValue stream(ChatClientObservationContext context) {
|
||||
return KeyValue.of(LowCardinalityKeyNames.STREAM, "" + context.isStream());
|
||||
}
|
||||
|
||||
protected KeyValue chatClientAdvisor(ChatClientObservationContext context) {
|
||||
@Override
|
||||
public KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context) {
|
||||
var keyValues = KeyValues.empty();
|
||||
keyValues = chatClientAdvisorNames(keyValues, context);
|
||||
keyValues = chatClientAdvisorParams(keyValues, context);
|
||||
keyValues = toolFunctionNames(keyValues, context);
|
||||
keyValues = toolFunctionCallbacks(keyValues, context);
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
protected KeyValues chatClientAdvisorNames(KeyValues keyValues, ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getAdvisors())) {
|
||||
return CHAT_CLIENT_ADVISOR_NONE;
|
||||
return keyValues;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISORS,
|
||||
context.getRequest()
|
||||
.getAdvisors()
|
||||
.stream()
|
||||
.map(a -> "\"" + a.getName() + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
var advisorNames = context.getRequest().getAdvisors().stream().map(Advisor::getName).toList();
|
||||
return keyValues.and(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISORS.asString(),
|
||||
TracingHelper.concatenateStrings(advisorNames));
|
||||
}
|
||||
|
||||
protected KeyValue chatClientAdvisorParam(ChatClientObservationContext context) {
|
||||
protected KeyValues chatClientAdvisorParams(KeyValues keyValues, ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getAdvisorParams())) {
|
||||
return CHAT_CLIENT_ADVISOR_PARAM_NONE;
|
||||
return keyValues;
|
||||
}
|
||||
return KeyValue.of(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISOR_PARAMS,
|
||||
context.getRequest()
|
||||
.getAdvisorParams()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e -> "\"" + e.getKey() + "\":\"" + e.getValue() + "\"")
|
||||
.collect(Collectors.joining(",", "[", "]")));
|
||||
var advisorParams = context.getRequest().getAdvisorParams();
|
||||
return keyValues.and(
|
||||
ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_ADVISOR_PARAMS.asString(),
|
||||
TracingHelper.concatenateMaps(advisorParams));
|
||||
}
|
||||
|
||||
protected KeyValues toolFunctionNames(KeyValues keyValues, ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getFunctionNames())) {
|
||||
return keyValues;
|
||||
}
|
||||
var functionNames = context.getRequest().getFunctionNames();
|
||||
return keyValues.and(
|
||||
ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_NAMES.asString(),
|
||||
TracingHelper.concatenateStrings(functionNames));
|
||||
}
|
||||
|
||||
protected KeyValues toolFunctionCallbacks(KeyValues keyValues, ChatClientObservationContext context) {
|
||||
if (CollectionUtils.isEmpty(context.getRequest().getFunctionCallbacks())) {
|
||||
return keyValues;
|
||||
}
|
||||
var functionCallbacks = context.getRequest()
|
||||
.getFunctionCallbacks()
|
||||
.stream()
|
||||
.map(FunctionCallback::getName)
|
||||
.toList();
|
||||
return keyValues
|
||||
.and(ChatClientObservationDocumentation.HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS
|
||||
.asString(), TracingHelper.concatenateStrings(functionCallbacks));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.ai.chat.client.observation;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -25,8 +25,9 @@ public enum SpringAiKind {
|
||||
|
||||
// @formatter:off
|
||||
|
||||
// Please, keep the alphabetical sorting.
|
||||
ADVISOR("advisor"),
|
||||
CHAT_CLIENT("chat_client"),
|
||||
CHAT_CLIENT_ADVISOR("chat_client_advisor"),
|
||||
VECTOR_STORE("vector_store");
|
||||
|
||||
private final String value;
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.util.StringUtils;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
@@ -64,10 +65,16 @@ public final class TracingHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String concatenateMaps(Map<String, Object> keyValues) {
|
||||
var keyValuesJoiner = new StringJoiner(", ", "[", "]");
|
||||
keyValues.forEach((key, value) -> keyValuesJoiner.add("\"" + key + "\":\"" + value + "\""));
|
||||
return keyValuesJoiner.toString();
|
||||
}
|
||||
|
||||
public static String concatenateStrings(List<String> strings) {
|
||||
var promptMessagesJoiner = new StringJoiner(", ", "[", "]");
|
||||
strings.forEach(string -> promptMessagesJoiner.add("\"" + string + "\""));
|
||||
return promptMessagesJoiner.toString();
|
||||
var stringsJoiner = new StringJoiner(", ", "[", "]");
|
||||
strings.forEach(string -> stringsJoiner.add("\"" + string + "\""));
|
||||
return stringsJoiner.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||
* Unit tests for {@link AdvisorObservationContext}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
class AdvisorObservationContextTests {
|
||||
|
||||
@@ -42,14 +43,14 @@ class AdvisorObservationContextTests {
|
||||
assertThatThrownBy(() -> AdvisorObservationContext.builder()
|
||||
.withAdvisorType(AdvisorObservationContext.Type.BEFORE)
|
||||
.build()).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("The advisorName must not be empty!");
|
||||
.hasMessageContaining("advisorName must not be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingAdvisorType() {
|
||||
assertThatThrownBy(() -> AdvisorObservationContext.builder().withAdvisorName("MyName").build())
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("The advisorType must not be null!");
|
||||
.hasMessageContaining("advisorType must not be null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ import org.springframework.ai.chat.client.advisor.observation.AdvisorObservation
|
||||
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.observation.Observation;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultAdvisorObservationConvention}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
class DefaultAdvisorObservationConventionTests {
|
||||
|
||||
@@ -64,8 +66,9 @@ class DefaultAdvisorObservationConventionTests {
|
||||
.withAdvisorType(AdvisorObservationContext.Type.AROUND)
|
||||
.build();
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
|
||||
KeyValue.of(LowCardinalityKeyNames.ADVISOR_TYPE.asString(), "AROUND"),
|
||||
KeyValue.of(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "chat_client_advisor"));
|
||||
KeyValue.of(LowCardinalityKeyNames.ADVISOR_TYPE.asString(),
|
||||
AdvisorObservationContext.Type.AROUND.name()),
|
||||
KeyValue.of(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), SpringAiKind.ADVISOR.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -33,9 +33,10 @@ import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ChatClientImportContentObservationFilter}.
|
||||
* Unit tests for {@link ChatClientInputContentObservationFilter}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChatClientInputContentObservationFilterTests {
|
||||
@@ -55,14 +56,13 @@ class ChatClientInputContentObservationFilterTests {
|
||||
|
||||
@Test
|
||||
void whenEmptyInputContentThenReturnOriginalContext() {
|
||||
|
||||
ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
|
||||
ChatClientObservationConvention customObservationConvention = null;
|
||||
|
||||
var request = new DefaultChatClientRequestSpec(chatModel, "", Map.of(), "", Map.of(), List.of(), List.of(),
|
||||
List.of(), List.of(), null, List.of(), Map.of(), observationRegistry, customObservationConvention);
|
||||
|
||||
var expectedContext = new ChatClientObservationContext(request, "", false);
|
||||
var expectedContext = ChatClientObservationContext.builder().withRequest(request).build();
|
||||
|
||||
var actualContext = observationFilter.map(expectedContext);
|
||||
|
||||
@@ -71,7 +71,6 @@ class ChatClientInputContentObservationFilterTests {
|
||||
|
||||
@Test
|
||||
void whenWithTextThenAugmentContext() {
|
||||
|
||||
ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
|
||||
ChatClientObservationConvention customObservationConvention = null;
|
||||
|
||||
@@ -79,7 +78,7 @@ class ChatClientInputContentObservationFilterTests {
|
||||
"sample system text", Map.of("sp1", "sp1v"), List.of(), List.of(), List.of(), List.of(), null,
|
||||
List.of(), Map.of(), observationRegistry, customObservationConvention);
|
||||
|
||||
var originalContext = new ChatClientObservationContext(request, "", false);
|
||||
var originalContext = ChatClientObservationContext.builder().withRequest(request).build();
|
||||
|
||||
var augmentedContext = observationFilter.map(originalContext);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import io.micrometer.observation.ObservationRegistry;
|
||||
* Unit tests for {@link ChatClientObservationContext}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ChatClientObservationContextTests {
|
||||
@@ -46,7 +47,7 @@ class ChatClientObservationContextTests {
|
||||
var request = new DefaultChatClientRequestSpec(chatModel, "", Map.of(), "", Map.of(), List.of(), List.of(),
|
||||
List.of(), List.of(), null, List.of(), Map.of(), ObservationRegistry.NOOP, null);
|
||||
|
||||
var observationContext = new ChatClientObservationContext(request, "", true);
|
||||
var observationContext = ChatClientObservationContext.builder().withRequest(request).withStream(true).build();
|
||||
|
||||
assertThat(observationContext).isNotNull();
|
||||
}
|
||||
|
||||
@@ -38,11 +38,14 @@ import org.springframework.ai.model.function.FunctionCallback;
|
||||
import io.micrometer.common.KeyValue;
|
||||
import io.micrometer.observation.Observation;
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import org.springframework.ai.observation.conventions.AiProvider;
|
||||
import org.springframework.ai.observation.conventions.SpringAiKind;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultChatClientObservationConvention}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
* @author Thomas Vitale
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultChatClientObservationConventionTests {
|
||||
@@ -67,14 +70,21 @@ class DefaultChatClientObservationConventionTests {
|
||||
|
||||
@Test
|
||||
void shouldHaveContextualName() {
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(request, "", true);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(request)
|
||||
.withStream(true)
|
||||
.build();
|
||||
|
||||
assertThat(this.observationConvention.getContextualName(observationContext)).isEqualTo("spring_ai chat_client");
|
||||
assertThat(this.observationConvention.getContextualName(observationContext))
|
||||
.isEqualTo("%s %s".formatted(AiProvider.SPRING_AI.value(), SpringAiKind.CHAT_CLIENT.value()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsOnlyChatClientObservationContext() {
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(request, "", true);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(request)
|
||||
.withStream(true)
|
||||
.build();
|
||||
|
||||
assertThat(this.observationConvention.supportsContext(observationContext)).isTrue();
|
||||
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse();
|
||||
@@ -82,7 +92,10 @@ class DefaultChatClientObservationConventionTests {
|
||||
|
||||
@Test
|
||||
void shouldHaveRequiredKeyValues() {
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(request, "", true);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(request)
|
||||
.withStream(true)
|
||||
.build();
|
||||
|
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(observationContext)).contains(
|
||||
KeyValue.of(LowCardinalityKeyNames.SPRING_AI_KIND.asString(), "chat_client"),
|
||||
@@ -143,38 +156,27 @@ class DefaultChatClientObservationConventionTests {
|
||||
|
||||
@Test
|
||||
void shouldHaveOptionalKeyValues() {
|
||||
|
||||
var request = new DefaultChatClientRequestSpec(chatModel, "", Map.of(), "", Map.of(),
|
||||
List.of(dummyFunction("functionCallback1"), dummyFunction("functionCallback2")), List.of(),
|
||||
List.of("function1", "function2"), List.of(), null,
|
||||
List.of(dummyAdvisor("advisor1"), dummyAdvisor("advisor2")), Map.of("advParam1", "advisorParam1Value"),
|
||||
ObservationRegistry.NOOP, null);
|
||||
|
||||
ChatClientObservationContext observationContext = new ChatClientObservationContext(request, "json", true);
|
||||
ChatClientObservationContext observationContext = ChatClientObservationContext.builder()
|
||||
.withRequest(request)
|
||||
.withFormat("json")
|
||||
.withStream(true)
|
||||
.build();
|
||||
|
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(observationContext)).contains(
|
||||
KeyValue.of(HighCardinalityKeyNames.CHAT_CLIENT_ADVISORS.asString(),
|
||||
"[\"advisor1\",\"advisor2\",\"CallAroundAdvisor\",\"StreamAroundAdvisor\"]"),
|
||||
"[\"advisor1\", \"advisor2\", \"CallAroundAdvisor\", \"StreamAroundAdvisor\"]"),
|
||||
KeyValue.of(HighCardinalityKeyNames.CHAT_CLIENT_ADVISOR_PARAMS.asString(),
|
||||
"[\"advParam1\":\"advisorParam1Value\"]"),
|
||||
KeyValue.of(HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_NAMES.asString(),
|
||||
"[\"function1\",\"function2\"]"),
|
||||
"[\"function1\", \"function2\"]"),
|
||||
KeyValue.of(HighCardinalityKeyNames.CHAT_CLIENT_TOOL_FUNCTION_CALLBACKS.asString(),
|
||||
"[\"functionCallback1\",\"functionCallback2\"]"));
|
||||
}
|
||||
|
||||
static class TestUsage implements Usage {
|
||||
|
||||
@Override
|
||||
public Long getPromptTokens() {
|
||||
return 1000L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getGenerationTokens() {
|
||||
return 500L;
|
||||
}
|
||||
|
||||
"[\"functionCallback1\", \"functionCallback2\"]"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -118,7 +118,6 @@ public class ChromaVectorStoreAutoConfigurationIT {
|
||||
.hasObservationWithNameEqualTo(DefaultVectorStoreObservationConvention.DEFAULT_NAME)
|
||||
.that()
|
||||
.hasContextualNameEqualTo("chroma delete")
|
||||
.hasHighCardinalityKeyValue(HighCardinalityKeyNames.DB_VECTOR_QUERY_FILTER.asString(), "none")
|
||||
.hasBeenStarted()
|
||||
.hasBeenStopped();
|
||||
observationRegistry.clear();
|
||||
|
||||
Reference in New Issue
Block a user