diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java index eaac6e586be..ce8df959f08 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java @@ -340,7 +340,10 @@ private static Struct jsonToStruct(String json) { Struct.Builder structBuilder = Struct.newBuilder(); - if (rootNode.isArray()) { + if (rootNode.isTextual()) { + structBuilder.putFields("result", Value.newBuilder().setStringValue(json).build()); + } + else if (rootNode.isArray()) { // Handle JSON array List values = new ArrayList<>(); diff --git a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java index 27d58241244..a0a706dd277 100644 --- a/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java +++ b/models/spring-ai-vertex-ai-gemini/src/test/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModelIT.java @@ -361,6 +361,38 @@ void jsonArrayToolCallingTest() { } + @Test + void jsonTextToolCallingTest() { + // Test for the improved jsonToStruct method that handles JSON texts in tool + // calling + + ToolCallingManager toolCallingManager = ToolCallingManager.builder() + .observationRegistry(ObservationRegistry.NOOP) + .build(); + + VertexAiGeminiChatModel chatModelWithTools = VertexAiGeminiChatModel.builder() + .vertexAI(vertexAiApi()) + .toolCallingManager(toolCallingManager) + .defaultOptions(VertexAiGeminiChatOptions.builder() + .model(VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH) + .temperature(0.1) + .build()) + .build(); + + ChatClient chatClient = ChatClient.builder(chatModelWithTools).build(); + + // Create a prompt that will trigger the tool call with a specific request that + // should invoke the tool + String response = chatClient.prompt() + .tools(new CurrentTimeTools()) + .user("Get the current time. Make sure to use the tool to get this information.") + .call() + .content(); + + assertThat(response).isNotEmpty(); + assertThat(response).contains("2025-05-08T10:10:10+02:00[Europe/Berlin]"); + } + /** * Tool class that returns a JSON array to test the jsonToStruct method's ability to * handle JSON arrays. This specifically tests the PR changes that improve the @@ -378,6 +410,21 @@ public List> getScientists() { } + /** + * Tool class that returns a String to test the jsonToStruct method's ability to + * handle JSON texts. This specifically tests the PR changes that improve the + * jsonToStruct method to handle JSON texts in addition to JSON objects and JSON + * arrays. + */ + public static class CurrentTimeTools { + + @Tool(description = "Get the current date and time in the user's timezone") + String getCurrentDateTime() { + return "2025-05-08T10:10:10+02:00[Europe/Berlin]"; + } + + } + record ActorsFilmsRecord(String actor, List movies) { } diff --git a/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverter.java b/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverter.java index d2331a3c14f..dcbc5fcab97 100644 --- a/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverter.java +++ b/spring-ai-model/src/main/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverter.java @@ -45,7 +45,7 @@ public final class DefaultToolCallResultConverter implements ToolCallResultConve public String convert(@Nullable Object result, @Nullable Type returnType) { if (returnType == Void.TYPE) { logger.debug("The tool has no return type. Converting to conventional response."); - return "Done"; + return JsonParser.toJson("Done"); } if (result instanceof RenderedImage) { final var buf = new ByteArrayOutputStream(1024 * 4); diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverterTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverterTests.java index b6d2ee20c57..5b1e908fe48 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverterTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/execution/DefaultToolCallResultConverterTests.java @@ -50,9 +50,9 @@ void convertWithNullReturnTypeShouldReturn() { } @Test - void convertVoidReturnTypeShouldReturnDone() { + void convertVoidReturnTypeShouldReturnDoneJson() { String result = this.converter.convert(null, void.class); - assertThat(result).isEqualTo("Done"); + assertThat(result).isEqualTo("\"Done\""); } @Test