Skip to content

Commit 30eb3ce

Browse files
ThomasVitaletzolov
authored andcommitted
Micrometer instrumentation for tool calling
Introduce instrumentation for tool calling using the Micrometer Observation API. By default, metadata about tool calling are exported as metrics and traces. Optionally, the actual tool call input and result can be exported as well by enabling the dedicated feature flag. Signed-off-by: Thomas Vitale <[email protected]>
1 parent 8f879aa commit 30eb3ce

File tree

21 files changed

+1187
-19
lines changed

21 files changed

+1187
-19
lines changed

auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java

+29-6
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616

1717
package org.springframework.ai.model.tool.autoconfigure;
1818

19-
import java.util.ArrayList;
20-
import java.util.List;
21-
2219
import io.micrometer.observation.ObservationRegistry;
23-
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
2422
import org.springframework.ai.chat.model.ChatModel;
2523
import org.springframework.ai.model.tool.ToolCallingManager;
2624
import org.springframework.ai.tool.ToolCallback;
2725
import org.springframework.ai.tool.ToolCallbackProvider;
2826
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
2927
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
28+
import org.springframework.ai.tool.observation.ToolCallingContentObservationFilter;
29+
import org.springframework.ai.tool.observation.ToolCallingObservationConvention;
3030
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
3131
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
3232
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
@@ -35,9 +35,14 @@
3535
import org.springframework.boot.autoconfigure.AutoConfiguration;
3636
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3737
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
38+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
39+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3840
import org.springframework.context.annotation.Bean;
3941
import org.springframework.context.support.GenericApplicationContext;
4042

43+
import java.util.ArrayList;
44+
import java.util.List;
45+
4146
/**
4247
* Auto-configuration for common tool calling features of {@link ChatModel}.
4348
*
@@ -47,8 +52,11 @@
4752
*/
4853
@AutoConfiguration
4954
@ConditionalOnClass(ChatModel.class)
55+
@EnableConfigurationProperties(ToolCallingProperties.class)
5056
public class ToolCallingAutoConfiguration {
5157

58+
private static final Logger logger = LoggerFactory.getLogger(ToolCallingAutoConfiguration.class);
59+
5260
@Bean
5361
@ConditionalOnMissingBean
5462
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
@@ -76,12 +84,27 @@ ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
7684
@ConditionalOnMissingBean
7785
ToolCallingManager toolCallingManager(ToolCallbackResolver toolCallbackResolver,
7886
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor,
79-
ObjectProvider<ObservationRegistry> observationRegistry) {
80-
return ToolCallingManager.builder()
87+
ObjectProvider<ObservationRegistry> observationRegistry,
88+
ObjectProvider<ToolCallingObservationConvention> observationConvention) {
89+
var toolCallingManager = ToolCallingManager.builder()
8190
.observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
8291
.toolCallbackResolver(toolCallbackResolver)
8392
.toolExecutionExceptionProcessor(toolExecutionExceptionProcessor)
8493
.build();
94+
95+
observationConvention.ifAvailable(toolCallingManager::setObservationConvention);
96+
97+
return toolCallingManager;
98+
}
99+
100+
@Bean
101+
@ConditionalOnMissingBean
102+
@ConditionalOnProperty(prefix = ToolCallingProperties.CONFIG_PREFIX + ".observations", name = "include-content",
103+
havingValue = "true")
104+
ToolCallingContentObservationFilter toolCallingContentObservationFilter() {
105+
logger.warn(
106+
"You have enabled the inclusion of the tool call arguments and result in the observations, with the risk of exposing sensitive or private information. Please, be careful!");
107+
return new ToolCallingContentObservationFilter();
85108
}
86109

87110
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.tool.autoconfigure;
18+
19+
import org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* Configuration properties for tool calling.
23+
*
24+
* @author Thomas Vitale
25+
* @since 1.0.0
26+
*/
27+
@ConfigurationProperties(ToolCallingProperties.CONFIG_PREFIX)
28+
public class ToolCallingProperties {
29+
30+
public static final String CONFIG_PREFIX = "spring.ai.tools";
31+
32+
private final Observations observations = new Observations();
33+
34+
public static class Observations {
35+
36+
/**
37+
* Whether to include the tool call content in the observations.
38+
*/
39+
private boolean includeContent = false;
40+
41+
public boolean isIncludeContent() {
42+
return includeContent;
43+
}
44+
45+
public void setIncludeContent(boolean includeContent) {
46+
this.includeContent = includeContent;
47+
}
48+
49+
}
50+
51+
}

auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java

+20
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.ai.tool.function.FunctionToolCallback;
3232
import org.springframework.ai.tool.method.MethodToolCallback;
3333
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
34+
import org.springframework.ai.tool.observation.ToolCallingContentObservationFilter;
3435
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
3536
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
3637
import org.springframework.ai.tool.support.ToolDefinitions;
@@ -111,6 +112,25 @@ void resolveMissingToolCallbacks() {
111112
});
112113
}
113114

115+
@Test
116+
void observationFilterDefault() {
117+
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
118+
.withUserConfiguration(Config.class)
119+
.run(context -> {
120+
assertThat(context).doesNotHaveBean(ToolCallingContentObservationFilter.class);
121+
});
122+
}
123+
124+
@Test
125+
void observationFilterEnabled() {
126+
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
127+
.withPropertyValues("spring.ai.tools.observations.include-content=true")
128+
.withUserConfiguration(Config.class)
129+
.run(context -> {
130+
assertThat(context).hasSingleBean(ToolCallingContentObservationFilter.class);
131+
});
132+
}
133+
114134
static class WeatherService {
115135

116136
@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")

spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/AiObservationAttributes.java

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ public enum AiObservationAttributes {
6767
* The temperature setting for the model request.
6868
*/
6969
REQUEST_TEMPERATURE("gen_ai.request.temperature"),
70+
/**
71+
* List of tool definitions provided to the model in the request.
72+
*/
73+
REQUEST_TOOL_NAMES("spring.ai.model.request.tool.names"),
7074
/**
7175
* The top_k sampling setting for the model request.
7276
*/

spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/SpringAiKind.java

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public enum SpringAiKind {
3737
*/
3838
CHAT_CLIENT("chat_client"),
3939

40+
/**
41+
* Spring AI kind for tool calling.
42+
*/
43+
TOOL_CALL("tool_call"),
44+
4045
/**
4146
* Spring AI kind for vector store.
4247
*/

spring-ai-docs/src/main/antora/modules/ROOT/pages/observability/index.adoc

+41
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ IMPORTANT: The `gen_ai.client.token.usage` metrics measures number of input and
155155
|`gen_ai.usage.total_tokens` | The total number of tokens used in the model exchange.
156156
|`gen_ai.prompt` | The full prompt sent to the model. Optional.
157157
|`gen_ai.completion` | The full response received from the model. Optional.
158+
|`spring.ai.model.request.tool.names` | List of tool definitions provided to the model in the request.
158159
|===
159160

160161
NOTE: For measuring user tokens, the previous table lists the values present in an observation trace.
@@ -179,6 +180,46 @@ Spring AI supports logging chat prompt and completion data, useful for troublesh
179180

180181
WARNING: If you enable logging of the chat prompt and completion data, there's a risk of exposing sensitive or private information. Please, be careful!
181182

183+
== Tool Calling
184+
185+
The `spring.ai.tool` observations are recorded when performing tool calling in the context of a chat model interaction. They measure the time spent on toll call completion and propagate the related tracing information.
186+
187+
.Low Cardinality Keys
188+
[cols="a,a", stripes=even]
189+
|===
190+
|Name | Description
191+
192+
|`gen_ai.operation.name` | The name of the operation being performed. It's always `framework`.
193+
|`gen_ai.system` | The provider responsible for the operation. It's always `spring_ai`.
194+
|`spring.ai.kind` | The kind of operation performed by Spring AI. It's always `tool_call`.
195+
|`spring.ai.tool.definition.name` | The name of the tool.
196+
|===
197+
198+
.High Cardinality Keys
199+
[cols="a,a", stripes=even]
200+
|===
201+
|Name | Description
202+
|`spring.ai.tool.definition.description` | Description of the tool.
203+
|`spring.ai.tool.definition.schema` | Schema of the parameters used to call the tool.
204+
|`spring.ai.tool.call.arguments` | The input arguments to the tool call. (Only when enabled)
205+
|`spring.ai.tool.call.result` | Schema of the parameters used to call the tool. (Only when enabled)
206+
|===
207+
208+
=== Tool Call Arguments and Result Data
209+
210+
The input arguments and result from the tool call are not exported by default, as they can be potentially sensitive.
211+
212+
Spring AI supports exporting tool call arguments and result data as span attributes.
213+
214+
[cols="6,3,1", stripes=even]
215+
|====
216+
| Property | Description | Default
217+
218+
| `spring.ai.tools.observations.include-content` | Include the tool call content in observations. `true` or `false` | `false`
219+
|====
220+
221+
WARNING: If you enable the inclusion of the tool call arguments and result in the observations, there's a risk of exposing sensitive or private information. Please, be careful!
222+
182223
== EmbeddingModel
183224

184225
NOTE: Observability features are currently supported only for `EmbeddingModel` implementations from the following

spring-ai-model/src/main/java/org/springframework/ai/chat/observation/ChatModelObservationDocumentation.java

+10
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ public String asString() {
150150
}
151151
},
152152

153+
/**
154+
* List of tool definitions provided to the model in the request.
155+
*/
156+
REQUEST_TOOL_NAMES {
157+
@Override
158+
public String asString() {
159+
return AiObservationAttributes.REQUEST_TOOL_NAMES.value();
160+
}
161+
},
162+
153163
/**
154164
* The top_k sampling setting for the model request.
155165
*/

spring-ai-model/src/main/java/org/springframework/ai/chat/observation/DefaultChatModelObservationConvention.java

+22-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
package org.springframework.ai.chat.observation;
1818

19-
import java.util.Objects;
19+
import java.util.HashSet;
20+
import java.util.Set;
2021
import java.util.StringJoiner;
2122

2223
import io.micrometer.common.KeyValue;
2324
import io.micrometer.common.KeyValues;
2425

2526
import org.springframework.ai.chat.prompt.ChatOptions;
27+
import org.springframework.ai.model.tool.ToolCallingChatOptions;
2628
import org.springframework.util.CollectionUtils;
2729
import org.springframework.util.StringUtils;
2830

@@ -100,6 +102,7 @@ public KeyValues getHighCardinalityKeyValues(ChatModelObservationContext context
100102
keyValues = requestPresencePenalty(keyValues, context);
101103
keyValues = requestStopSequences(keyValues, context);
102104
keyValues = requestTemperature(keyValues, context);
105+
keyValues = requestTools(keyValues, context);
103106
keyValues = requestTopK(keyValues, context);
104107
keyValues = requestTopP(keyValues, context);
105108
// Response
@@ -148,8 +151,6 @@ protected KeyValues requestStopSequences(KeyValues keyValues, ChatModelObservati
148151
if (!CollectionUtils.isEmpty(options.getStopSequences())) {
149152
StringJoiner stopSequencesJoiner = new StringJoiner(", ", "[", "]");
150153
options.getStopSequences().forEach(value -> stopSequencesJoiner.add("\"" + value + "\""));
151-
KeyValue.of(ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES,
152-
options.getStopSequences(), Objects::nonNull);
153154
return keyValues.and(
154155
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_STOP_SEQUENCES.asString(),
155156
stopSequencesJoiner.toString());
@@ -167,6 +168,24 @@ protected KeyValues requestTemperature(KeyValues keyValues, ChatModelObservation
167168
return keyValues;
168169
}
169170

171+
protected KeyValues requestTools(KeyValues keyValues, ChatModelObservationContext context) {
172+
if (!(context.getRequest().getOptions() instanceof ToolCallingChatOptions options)) {
173+
return keyValues;
174+
}
175+
176+
Set<String> toolNames = new HashSet<>(options.getToolNames());
177+
toolNames.addAll(options.getToolCallbacks().stream().map(tc -> tc.getToolDefinition().name()).toList());
178+
179+
if (!CollectionUtils.isEmpty(toolNames)) {
180+
StringJoiner toolNamesJoiner = new StringJoiner(", ", "[", "]");
181+
toolNames.forEach(value -> toolNamesJoiner.add("\"" + value + "\""));
182+
return keyValues.and(
183+
ChatModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_TOOL_NAMES.asString(),
184+
toolNamesJoiner.toString());
185+
}
186+
return keyValues;
187+
}
188+
170189
protected KeyValues requestTopK(KeyValues keyValues, ChatModelObservationContext context) {
171190
ChatOptions options = context.getRequest().getOptions();
172191
if (options.getTopK() != null) {

0 commit comments

Comments
 (0)