From 53f76d6260fbe893be5ae1c50319b9276e38ccb7 Mon Sep 17 00:00:00 2001 From: yxloveforever Date: Fri, 10 Oct 2025 10:44:08 +0800 Subject: [PATCH 1/2] safely merge empty tool call name case(streaming mode) Signed-off-by: Feng Xie Signed-off-by: yxloveforever --- .../model/tool/DefaultToolCallingManager.java | 43 ++++++++++++--- .../tool/DefaultToolCallingManagerTest.java | 55 +++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java b/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java index 02c35462857..cce161e4d8f 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java @@ -17,6 +17,7 @@ package org.springframework.ai.model.tool; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -128,15 +129,15 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp Assert.notNull(chatResponse, "chatResponse cannot be null"); Optional toolCallGeneration = chatResponse.getResults() - .stream() - .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())) - .findFirst(); + .stream() + .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())) + .findFirst(); if (toolCallGeneration.isEmpty()) { throw new IllegalStateException("No tool call requested by the chat model"); } - AssistantMessage assistantMessage = toolCallGeneration.get().getOutput(); + AssistantMessage assistantMessage = safelyMergeAssistantMessageIfEmptyToolCallPresent(toolCallGeneration); ToolContext toolContext = buildToolContext(prompt, assistantMessage); @@ -147,9 +148,37 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp assistantMessage, internalToolExecutionResult.toolResponseMessage()); return ToolExecutionResult.builder() - .conversationHistory(conversationHistory) - .returnDirect(internalToolExecutionResult.returnDirect()) - .build(); + .conversationHistory(conversationHistory) + .returnDirect(internalToolExecutionResult.returnDirect()) + .build(); + } + + private AssistantMessage safelyMergeAssistantMessageIfEmptyToolCallPresent(Optional toolCallGeneration) { + if (toolCallGeneration.isEmpty()) { + throw new IllegalStateException("No tool call requested by the chat model"); + } + AssistantMessage assistantMessage = toolCallGeneration.get().getOutput(); + List toolCalls = assistantMessage.getToolCalls(); + List reversedToolCalls = new ArrayList<>(toolCalls); + Collections.reverse(reversedToolCalls); + List newToolCalls = new ArrayList<>(); + StringBuilder args = new StringBuilder(); + for (AssistantMessage.ToolCall toolCall : reversedToolCalls) { + args.append(toolCall.arguments()); + if (StringUtils.hasText(toolCall.name())) { + AssistantMessage.ToolCall newToolCall = new AssistantMessage.ToolCall( + toolCall.id(), toolCall.type(), toolCall.name(), args.toString()); + newToolCalls.add(newToolCall); + args = new StringBuilder(); + } + } + Collections.reverse(newToolCalls); + return AssistantMessage.builder() + .content(assistantMessage.getText()) + .toolCalls(newToolCalls) + .media(assistantMessage.getMedia()) + .properties(assistantMessage.getMetadata()) + .build(); } private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) { diff --git a/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java b/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java index bd60639c323..c70155ceb45 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java @@ -423,4 +423,59 @@ public String call(String toolInput) { assertThatNoException().isThrownBy(() -> manager.executeToolCalls(prompt, chatResponse)); } + @Test + void shouldHandleMultipleGenerationsWithToolCallsWhenNameIsEmpty() { + ToolCallback toolCallback = new ToolCallback() { + @Override + public ToolDefinition getToolDefinition() { + return DefaultToolDefinition.builder() + .name("multiGenTool") + .description("Tool for multiple generations") + .inputSchema("{}") + .build(); + } + + @Override + public ToolMetadata getToolMetadata() { + return ToolMetadata.builder().build(); + } + + @Override + public String call(String toolInput) { + return "{\"result\": \"success\"}"; + } + }; + + // Create multiple generations with tool calls + AssistantMessage.ToolCall toolCall1 = new AssistantMessage.ToolCall("1", "function", "multiGenTool", "{}"); + AssistantMessage.ToolCall toolCall2 = new AssistantMessage.ToolCall("2", "function", "multiGenTool", "{}"); + AssistantMessage.ToolCall toolCall3 = new AssistantMessage.ToolCall("3", "function", "", "{}"); + + AssistantMessage assistantMessage1 = AssistantMessage.builder() + .content("") + .properties(Map.of()) + .toolCalls(List.of(toolCall1)) + .build(); + + AssistantMessage assistantMessage2 = AssistantMessage.builder() + .content("") + .properties(Map.of()) + .toolCalls(List.of(toolCall2, toolCall3)) + .build(); + + Generation generation1 = new Generation(assistantMessage1); + Generation generation2 = new Generation(assistantMessage2); + + ChatResponse chatResponse = new ChatResponse(List.of(generation1, generation2)); + + Prompt prompt = new Prompt(List.of(new UserMessage("test multiple generations"))); + + DefaultToolCallingManager manager = DefaultToolCallingManager.builder() + .observationRegistry(ObservationRegistry.NOOP) + .toolCallbackResolver(toolName -> "multiGenTool".equals(toolName) ? toolCallback : null) + .build(); + + assertThatNoException().isThrownBy(() -> manager.executeToolCalls(prompt, chatResponse)); + } + } From 9e6141751b5a8289ce179792b8362c948542f1c9 Mon Sep 17 00:00:00 2001 From: yxloveforever Date: Fri, 10 Oct 2025 23:42:43 +0800 Subject: [PATCH 2/2] reformat code Signed-off-by: Feng Xie Signed-off-by: yxloveforever --- .../model/tool/DefaultToolCallingManager.java | 29 +++++++++--------- .../tool/DefaultToolCallingManagerTest.java | 30 +++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java b/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java index cce161e4d8f..b6575ec1def 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/model/tool/DefaultToolCallingManager.java @@ -129,9 +129,9 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp Assert.notNull(chatResponse, "chatResponse cannot be null"); Optional toolCallGeneration = chatResponse.getResults() - .stream() - .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())) - .findFirst(); + .stream() + .filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())) + .findFirst(); if (toolCallGeneration.isEmpty()) { throw new IllegalStateException("No tool call requested by the chat model"); @@ -148,12 +148,13 @@ public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResp assistantMessage, internalToolExecutionResult.toolResponseMessage()); return ToolExecutionResult.builder() - .conversationHistory(conversationHistory) - .returnDirect(internalToolExecutionResult.returnDirect()) - .build(); + .conversationHistory(conversationHistory) + .returnDirect(internalToolExecutionResult.returnDirect()) + .build(); } - private AssistantMessage safelyMergeAssistantMessageIfEmptyToolCallPresent(Optional toolCallGeneration) { + private AssistantMessage safelyMergeAssistantMessageIfEmptyToolCallPresent( + Optional toolCallGeneration) { if (toolCallGeneration.isEmpty()) { throw new IllegalStateException("No tool call requested by the chat model"); } @@ -166,19 +167,19 @@ private AssistantMessage safelyMergeAssistantMessageIfEmptyToolCallPresent(Optio for (AssistantMessage.ToolCall toolCall : reversedToolCalls) { args.append(toolCall.arguments()); if (StringUtils.hasText(toolCall.name())) { - AssistantMessage.ToolCall newToolCall = new AssistantMessage.ToolCall( - toolCall.id(), toolCall.type(), toolCall.name(), args.toString()); + AssistantMessage.ToolCall newToolCall = new AssistantMessage.ToolCall(toolCall.id(), toolCall.type(), + toolCall.name(), args.toString()); newToolCalls.add(newToolCall); args = new StringBuilder(); } } Collections.reverse(newToolCalls); return AssistantMessage.builder() - .content(assistantMessage.getText()) - .toolCalls(newToolCalls) - .media(assistantMessage.getMedia()) - .properties(assistantMessage.getMetadata()) - .build(); + .content(assistantMessage.getText()) + .toolCalls(newToolCalls) + .media(assistantMessage.getMedia()) + .properties(assistantMessage.getMetadata()) + .build(); } private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) { diff --git a/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java b/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java index c70155ceb45..7f4d928ea58 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/model/tool/DefaultToolCallingManagerTest.java @@ -429,10 +429,10 @@ void shouldHandleMultipleGenerationsWithToolCallsWhenNameIsEmpty() { @Override public ToolDefinition getToolDefinition() { return DefaultToolDefinition.builder() - .name("multiGenTool") - .description("Tool for multiple generations") - .inputSchema("{}") - .build(); + .name("multiGenTool") + .description("Tool for multiple generations") + .inputSchema("{}") + .build(); } @Override @@ -452,16 +452,16 @@ public String call(String toolInput) { AssistantMessage.ToolCall toolCall3 = new AssistantMessage.ToolCall("3", "function", "", "{}"); AssistantMessage assistantMessage1 = AssistantMessage.builder() - .content("") - .properties(Map.of()) - .toolCalls(List.of(toolCall1)) - .build(); + .content("") + .properties(Map.of()) + .toolCalls(List.of(toolCall1)) + .build(); AssistantMessage assistantMessage2 = AssistantMessage.builder() - .content("") - .properties(Map.of()) - .toolCalls(List.of(toolCall2, toolCall3)) - .build(); + .content("") + .properties(Map.of()) + .toolCalls(List.of(toolCall2, toolCall3)) + .build(); Generation generation1 = new Generation(assistantMessage1); Generation generation2 = new Generation(assistantMessage2); @@ -471,9 +471,9 @@ public String call(String toolInput) { Prompt prompt = new Prompt(List.of(new UserMessage("test multiple generations"))); DefaultToolCallingManager manager = DefaultToolCallingManager.builder() - .observationRegistry(ObservationRegistry.NOOP) - .toolCallbackResolver(toolName -> "multiGenTool".equals(toolName) ? toolCallback : null) - .build(); + .observationRegistry(ObservationRegistry.NOOP) + .toolCallbackResolver(toolName -> "multiGenTool".equals(toolName) ? toolCallback : null) + .build(); assertThatNoException().isThrownBy(() -> manager.executeToolCalls(prompt, chatResponse)); }