diff --git a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java index 6295666e07f..3b762a6d58d 100644 --- a/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java +++ b/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/DeepSeekChatModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,7 @@ * backed by {@link DeepSeekApi}. * * @author Geng Rong + * @author Dongha Koo */ public class DeepSeekChatModel implements ChatModel { @@ -338,7 +339,9 @@ private Generation buildGeneration(Choice choice, Map metadata) String textContent = choice.message().content(); String reasoningContent = choice.message().reasoningContent(); - + if (textContent == null && !toolCalls.isEmpty()) { + textContent = "__tool_call__"; + } DeepSeekAssistantMessage assistantMessage = new DeepSeekAssistantMessage(textContent, reasoningContent, metadata, toolCalls); return new Generation(assistantMessage, generationMetadataBuilder.build()); diff --git a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java index 234309a02c8..e3f5a5e3f76 100644 --- a/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java +++ b/spring-ai-client-chat/src/test/java/org/springframework/ai/chat/client/ChatClientResponseTests.java @@ -17,10 +17,14 @@ package org.springframework.ai.chat.client; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.model.Generation; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -28,6 +32,7 @@ * Unit tests for {@link ChatClientResponse}. * * @author Thomas Vitale + * @author Dongha Koo */ class ChatClientResponseTests { @@ -82,4 +87,15 @@ void whenMutateThenImmutableContext() { assertThat(response.context()).containsEntry("key", "value"); } + @Test + void whenAssistantMessageHasOnlyToolCalls_thenContentIsToolCallMarker() { + var toolCall = new AssistantMessage.ToolCall("tool-1", "function", "doSomething", "{\"foo\":\"bar\"}"); + var assistantMessage = new AssistantMessage(null, Map.of(), List.of(toolCall), List.of()); + + assertThat(assistantMessage.getDisplayText()).isEqualTo("__tool_call__"); + + var generation = new Generation(assistantMessage); + assertThat(generation.getOutput().getDisplayText()).isEqualTo("__tool_call__"); + } + } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java index b092de2d6da..c63ad1dba46 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/chat/messages/AssistantMessage.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ * * @author Mark Pollack * @author Christian Tzolov + * @author Dongha Koo * @since 1.0.0 */ public class AssistantMessage extends AbstractMessage implements MediaContent { @@ -104,4 +105,16 @@ public record ToolCall(String id, String type, String name, String arguments) { } + /** + * Returns a safe, non-null text representation. If text is null or empty and + * toolCalls exist, returns "__tool_call__". + */ + public String getDisplayText() { + String text = super.getText(); + if ((text == null || text.trim().isEmpty()) && this.hasToolCalls()) { + return "__tool_call__"; + } + return (text != null) ? text : ""; + } + }