Skip to content

Commit 1fa3035

Browse files
authored
fix(tool-calls): broken trace of tool call (#1954)
* fix(observation): broken trace of tool call * add license
1 parent 22821d1 commit 1fa3035

File tree

3 files changed

+77
-16
lines changed

3 files changed

+77
-16
lines changed

spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/chat/DashScopeChatModel.java

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi.FunctionTool;
3131
import com.alibaba.cloud.ai.dashscope.chat.observation.DashScopeChatModelObservationConvention;
3232
import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants;
33+
import com.alibaba.cloud.ai.tool.observation.inner.ToolCallReactiveContextHolder;
3334
import io.micrometer.observation.Observation;
3435
import io.micrometer.observation.ObservationRegistry;
3536
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
@@ -260,22 +261,25 @@ public Flux<ChatResponse> internalStream(Prompt prompt, ChatResponse previousCha
260261
// @formatter:off
261262
Flux<ChatResponse> flux = chatResponse.flatMap(response -> {
262263
if (toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {
263-
return Flux.defer(
264-
() -> {
265-
var toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
266-
if (toolExecutionResult.returnDirect()) {
267-
// Return tool execution result directly to the client.
268-
return Flux.just(ChatResponse.builder().from(response)
269-
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
270-
.build());
271-
} else {
272-
// Send the tool execution result back to the model.
273-
return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
274-
response);
275-
}
276-
}
277-
).subscribeOn(Schedulers.boundedElastic());
278-
264+
return Flux.deferContextual((ctx) -> {
265+
ToolExecutionResult toolExecutionResult;
266+
try {
267+
ToolCallReactiveContextHolder.setContext(ctx);
268+
toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);
269+
} finally {
270+
ToolCallReactiveContextHolder.clearContext();
271+
}
272+
if (toolExecutionResult.returnDirect()) {
273+
// Return tool execution result directly to the client.
274+
return Flux.just(ChatResponse.builder().from(response)
275+
.generations(ToolExecutionResult.buildGenerations(toolExecutionResult))
276+
.build());
277+
} else {
278+
// Send the tool execution result back to the model.
279+
return this.internalStream(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),
280+
response);
281+
}
282+
}).subscribeOn(Schedulers.boundedElastic());
279283
}
280284
else {
281285
return Flux.just(response);

spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/tool/ObservableToolCallingManager.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import com.alibaba.cloud.ai.tool.observation.ArmsToolCallingObservationContext;
1919
import com.alibaba.cloud.ai.tool.observation.ArmsToolCallingObservationConvention;
2020
import com.alibaba.cloud.ai.tool.observation.ArmsToolCallingObservationDocumentation;
21+
import com.alibaba.cloud.ai.tool.observation.inner.ToolCallReactiveContextHolder;
2122
import io.micrometer.observation.ObservationRegistry;
23+
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
2224
import java.util.ArrayList;
2325
import java.util.HashMap;
2426
import java.util.Iterator;
@@ -49,6 +51,7 @@
4951
import org.springframework.util.Assert;
5052
import org.springframework.util.CollectionUtils;
5153
import org.springframework.util.StringUtils;
54+
import reactor.util.context.ContextView;
5255

5356
/**
5457
* Inspired from org.springframework.ai.model.tool.DefaultToolCallingManager.
@@ -215,6 +218,12 @@ private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMess
215218
.returnDirect(returnDirect)
216219
.build();
217220

221+
ContextView contextView = ToolCallReactiveContextHolder.getContext();
222+
if (contextView != null) {
223+
observationContext
224+
.setParentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null));
225+
}
226+
218227
String toolResult = ArmsToolCallingObservationDocumentation.EXECUTE_TOOL_OPERATION
219228
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
220229
this.observationRegistry)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2024-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+
package com.alibaba.cloud.ai.tool.observation.inner;
17+
18+
import reactor.util.context.Context;
19+
import reactor.util.context.ContextView;
20+
21+
/**
22+
* Copied from <a
23+
* href=https://github.com/spring-projects/spring-ai/blob/main/spring-ai-model/src/main/java/org/springframework/ai/model/tool/internal/ToolCallReactiveContextHolder.java>upstream
24+
* (1.0.0)</a>. <br/>
25+
* This class bridges blocking Tools call and the reactive context. When calling tools, it
26+
* captures the context in a thread local, making it available to re-inject in a nested
27+
* reactive call.
28+
*
29+
* @author Daniel Garnier-Moiroux
30+
* @since 1.1.0
31+
*/
32+
public class ToolCallReactiveContextHolder {
33+
34+
private static final ThreadLocal<ContextView> context = ThreadLocal.withInitial(Context::empty);
35+
36+
public static void setContext(ContextView contextView) {
37+
context.set(contextView);
38+
}
39+
40+
public static ContextView getContext() {
41+
return context.get();
42+
}
43+
44+
public static void clearContext() {
45+
context.remove();
46+
}
47+
48+
}

0 commit comments

Comments
 (0)