Skip to content

Commit 50698db

Browse files
committed
safely merge empty tool call name case(streaming mode)
Signed-off-by: Feng Xie<yxloveforever@gmail.com> Signed-off-by: yxloveforever <yxloveforever@gmail.com>
1 parent 133eb40 commit 50698db

File tree

2 files changed

+91
-7
lines changed

2 files changed

+91
-7
lines changed

spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.model.tool;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collections;
2021
import java.util.HashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -128,15 +129,15 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp
128129
Assert.notNull(chatResponse, "chatResponse cannot be null");
129130

130131
Optional<Generation> toolCallGeneration = chatResponse.getResults()
131-
.stream()
132-
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
133-
.findFirst();
132+
.stream()
133+
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
134+
.findFirst();
134135

135136
if (toolCallGeneration.isEmpty()) {
136137
throw new IllegalStateException("No tool call requested by the chat model");
137138
}
138139

139-
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
140+
AssistantMessage assistantMessage = safelyMergeAssistantMessageIfEmptyToolCallPresent(toolCallGeneration);
140141

141142
ToolContext toolContext = buildToolContext(prompt, assistantMessage);
142143

@@ -147,9 +148,37 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp
147148
assistantMessage, internalToolExecutionResult.toolResponseMessage());
148149

149150
return ToolExecutionResult.builder()
150-
.conversationHistory(conversationHistory)
151-
.returnDirect(internalToolExecutionResult.returnDirect())
152-
.build();
151+
.conversationHistory(conversationHistory)
152+
.returnDirect(internalToolExecutionResult.returnDirect())
153+
.build();
154+
}
155+
156+
private AssistantMessage safelyMergeAssistantMessageIfEmptyToolCallPresent(Optional<Generation> toolCallGeneration) {
157+
if (toolCallGeneration.isEmpty()) {
158+
throw new IllegalStateException("No tool call requested by the chat model");
159+
}
160+
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
161+
List<AssistantMessage.ToolCall> toolCalls = assistantMessage.getToolCalls();
162+
List<AssistantMessage.ToolCall> reversedToolCalls = new ArrayList<>(toolCalls);
163+
Collections.reverse(reversedToolCalls);
164+
List<AssistantMessage.ToolCall> newToolCalls = new ArrayList<>();
165+
StringBuilder args = new StringBuilder();
166+
for (AssistantMessage.ToolCall toolCall : reversedToolCalls) {
167+
args.append(toolCall.arguments());
168+
if (StringUtils.hasText(toolCall.name())) {
169+
AssistantMessage.ToolCall newToolCall = new AssistantMessage.ToolCall(
170+
toolCall.id(), toolCall.type(), toolCall.name(), args.toString());
171+
newToolCalls.add(newToolCall);
172+
args = new StringBuilder();
173+
}
174+
}
175+
Collections.reverse(newToolCalls);
176+
return AssistantMessage.builder()
177+
.content(assistantMessage.getText())
178+
.toolCalls(newToolCalls)
179+
.media(assistantMessage.getMedia())
180+
.properties(assistantMessage.getMetadata())
181+
.build();
153182
}
154183

155184
private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) {

spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,4 +423,59 @@ public String call(String toolInput) {
423423
assertThatNoException().isThrownBy(() -> manager.executeToolCalls(prompt, chatResponse));
424424
}
425425

426+
@Test
427+
void shouldHandleMultipleGenerationsWithToolCallsWhenNameIsEmpty() {
428+
ToolCallback toolCallback = new ToolCallback() {
429+
@Override
430+
public ToolDefinition getToolDefinition() {
431+
return DefaultToolDefinition.builder()
432+
.name("multiGenTool")
433+
.description("Tool for multiple generations")
434+
.inputSchema("{}")
435+
.build();
436+
}
437+
438+
@Override
439+
public ToolMetadata getToolMetadata() {
440+
return ToolMetadata.builder().build();
441+
}
442+
443+
@Override
444+
public String call(String toolInput) {
445+
return "{\"result\": \"success\"}";
446+
}
447+
};
448+
449+
// Create multiple generations with tool calls
450+
AssistantMessage.ToolCall toolCall1 = new AssistantMessage.ToolCall("1", "function", "multiGenTool", "{}");
451+
AssistantMessage.ToolCall toolCall2 = new AssistantMessage.ToolCall("2", "function", "multiGenTool", "{}");
452+
AssistantMessage.ToolCall toolCall3 = new AssistantMessage.ToolCall("3", "function", "", "{}");
453+
454+
AssistantMessage assistantMessage1 = AssistantMessage.builder()
455+
.content("")
456+
.properties(Map.of())
457+
.toolCalls(List.of(toolCall1))
458+
.build();
459+
460+
AssistantMessage assistantMessage2 = AssistantMessage.builder()
461+
.content("")
462+
.properties(Map.of())
463+
.toolCalls(List.of(toolCall2, toolCall3))
464+
.build();
465+
466+
Generation generation1 = new Generation(assistantMessage1);
467+
Generation generation2 = new Generation(assistantMessage2);
468+
469+
ChatResponse chatResponse = new ChatResponse(List.of(generation1, generation2));
470+
471+
Prompt prompt = new Prompt(List.of(new UserMessage("test multiple generations")));
472+
473+
DefaultToolCallingManager manager = DefaultToolCallingManager.builder()
474+
.observationRegistry(ObservationRegistry.NOOP)
475+
.toolCallbackResolver(toolName -> "multiGenTool".equals(toolName) ? toolCallback : null)
476+
.build();
477+
478+
assertThatNoException().isThrownBy(() -> manager.executeToolCalls(prompt, chatResponse));
479+
}
480+
426481
}

0 commit comments

Comments
 (0)