Skip to content

Commit 15dfed8

Browse files
committed
[anthropic] fix issue #1370 with tool call duplication
Without this fix during the stream event handling when `EventType.MESSAGE_STOP` occurs, the latest content block was resent again and it caused to the additional tool call(if it was the latest event) Signed-off-by: Mikhail Mazurkevich <Mikhail.Mazurkevich@jetbrains.com>
1 parent fbd03b7 commit 15dfed8

File tree

2 files changed

+68
-1
lines changed

2 files changed

+68
-1
lines changed

models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,13 @@ else if (event.type().equals(EventType.MESSAGE_DELTA)) {
179179
}
180180
}
181181
else if (event.type().equals(EventType.MESSAGE_STOP)) {
182-
// pass through
182+
// Don't return the latest Content block as it was before. Instead, return it with an updated event
183+
// type and general information like: model, message type, id and usage
184+
contentBlockReference.get()
185+
.withType(event.type().name())
186+
.withContent(List.of())
187+
.withStopReason(null)
188+
.withStopSequence(null);
183189
}
184190
else {
185191
contentBlockReference.get().withType(event.type().name()).withContent(List.of());

models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/api/AnthropicApiIT.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.ai.anthropic.api;
1818

19+
import java.util.ArrayList;
1920
import java.util.List;
2021

2122
import org.junit.jupiter.api.Test;
2223
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
24+
import org.springframework.ai.model.ModelOptionsUtils;
2325
import reactor.core.publisher.Flux;
2426

2527
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
@@ -41,6 +43,25 @@ public class AnthropicApiIT {
4143

4244
AnthropicApi anthropicApi = new AnthropicApi(System.getenv("ANTHROPIC_API_KEY"));
4345

46+
List<AnthropicApi.Tool> tools = List.of(new AnthropicApi.Tool("getCurrentWeather",
47+
"Get the weather in location. Return temperature in 30°F or 30°C format.", ModelOptionsUtils.jsonToMap("""
48+
{
49+
"type": "object",
50+
"properties": {
51+
"location": {
52+
"type": "string",
53+
"description": "The city and state e.g. San Francisco, CA"
54+
},
55+
"unit": {
56+
"type": "string",
57+
"enum": ["C", "F"]
58+
}
59+
},
60+
"required": ["location", "unit"]
61+
}
62+
""")));
63+
64+
4465
@Test
4566
void chatCompletionEntity() {
4667

@@ -72,6 +93,46 @@ void chatCompletionStream() {
7293
bla.stream().forEach(r -> System.out.println(r));
7394
}
7495

96+
@Test
97+
void chatCompletionStreamWithToolCall() {
98+
List<AnthropicMessage> messageConversation = new ArrayList<>();
99+
100+
AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock(
101+
"What's the weather like in San Francisco? Show the temperature in Celsius.")),
102+
Role.USER);
103+
104+
messageConversation.add(chatCompletionMessage);
105+
106+
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
107+
.withModel(AnthropicApi.ChatModel.CLAUDE_3_OPUS)
108+
.withMessages(messageConversation)
109+
.withMaxTokens(1500)
110+
.withStream(true)
111+
.withTemperature(0.8)
112+
.withTools(tools)
113+
.build();
114+
115+
List<ChatCompletionResponse> responses = this.anthropicApi.chatCompletionStream(chatCompletionRequest)
116+
.collectList()
117+
.block();
118+
119+
// Check that tool uses response returned only once
120+
List<ChatCompletionResponse> toolCompletionResponses = responses.stream().filter(r ->
121+
r.stopReason() != null && r.stopReason().equals(ContentBlock.Type.TOOL_USE.value)).toList();
122+
assertThat(toolCompletionResponses).size().isEqualTo(1);
123+
List<ContentBlock> toolContentBlocks = toolCompletionResponses.get(0).content();
124+
assertThat(toolContentBlocks).size().isEqualTo(1);
125+
ContentBlock toolContentBlock = toolContentBlocks.get(0);
126+
assertThat(toolContentBlock.type()).isEqualTo(ContentBlock.Type.TOOL_USE);
127+
assertThat(toolContentBlock.name()).isEqualTo("getCurrentWeather");
128+
129+
// Check that message stop response also returned
130+
List<ChatCompletionResponse> messageStopEvents = responses.stream()
131+
.filter(r -> r.type().equals(AnthropicApi.EventType.MESSAGE_STOP.name()))
132+
.toList();
133+
assertThat(messageStopEvents).size().isEqualTo(1);
134+
}
135+
75136
@Test
76137
void chatCompletionStreamError() {
77138
AnthropicMessage chatCompletionMessage = new AnthropicMessage(List.of(new ContentBlock("Tell me a Joke?")),

0 commit comments

Comments
 (0)