From 717c88aaece00036a6d8e28d8bb889ce3a4ad2a4 Mon Sep 17 00:00:00 2001 From: Charles Date: Sat, 7 Feb 2026 20:56:44 +0100 Subject: [PATCH] fix:Wrap non-JSON tool responses for Gemini compatibility Signed-off-by: Charles --- .../embabel/agent/api/tool/MatryoshkaTool.kt | 20 +++++-- .../spi/support/springai/messageConverters.kt | 16 +++++- .../agent/spi/loop/MatryoshkaToolTest.kt | 35 ++++++------ .../support/springai/MessageConversionTest.kt | 56 +++++++++++++++++++ 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/tool/MatryoshkaTool.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/tool/MatryoshkaTool.kt index 0915f42d3..ffac43ab1 100644 --- a/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/tool/MatryoshkaTool.kt +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/api/tool/MatryoshkaTool.kt @@ -559,6 +559,18 @@ interface MatryoshkaTool : Tool { } } +/** + * Returns a JSON-formatted tool response listing enabled tools. + * JSON format ensures compatibility with LLM providers that require + * valid JSON in tool responses (e.g. Google Gemini). + */ +private fun enabledToolsJson(toolNames: List): Tool.Result { + val toolNamesJson = toolNames.joinToString(", ") { "\"$it\"" } + return Tool.Result.text( + """{"enabled_tools_count": ${toolNames.size}, "enabled_tools": [$toolNamesJson]}""" + ) +} + /** * Simple implementation that exposes all inner tools. */ @@ -571,9 +583,7 @@ private class SimpleMatryoshkaTool( override fun call(input: String): Tool.Result { val toolNames = innerTools.map { it.definition.name } - return Tool.Result.text( - "Enabled ${innerTools.size} tools: ${toolNames.joinToString(", ")}" - ) + return enabledToolsJson(toolNames) } } @@ -593,8 +603,6 @@ private class SelectableMatryoshkaTool( override fun call(input: String): Tool.Result { val selected = selectTools(input) val toolNames = selected.map { it.definition.name } - return Tool.Result.text( - "Enabled ${selected.size} tools: ${toolNames.joinToString(", ")}" - ) + return enabledToolsJson(toolNames) } } diff --git a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/messageConverters.kt b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/messageConverters.kt index 5d2f84f9f..c7f732d7b 100644 --- a/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/messageConverters.kt +++ b/embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/support/springai/messageConverters.kt @@ -16,6 +16,7 @@ package com.embabel.agent.spi.support.springai import com.embabel.chat.* +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.ai.content.Media import org.springframework.core.io.ByteArrayResource import org.springframework.util.MimeTypeUtils @@ -50,7 +51,7 @@ fun Message.toSpringAiMessage(): SpringAiMessage { val toolResponse = ToolResponseMessage.ToolResponse( this.toolCallId, this.toolName, - this.textContent + this.textContent.ensureJson() ) ToolResponseMessage.builder().responses(listOf(toolResponse)).metadata(metadata).build() } @@ -123,6 +124,19 @@ internal fun List.mergeConsecutiveToolResponses(): List