diff --git a/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/MultimodalToolCallResultConverter.java b/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/MultimodalToolCallResultConverter.java index e4ac46185..be40b286d 100644 --- a/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/MultimodalToolCallResultConverter.java +++ b/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/MultimodalToolCallResultConverter.java @@ -120,7 +120,9 @@ private Map mediaToMap(Media media) { } Object data = media.getData(); - if (data instanceof ToolMultimodalResult.MediaFormats formats) { + ToolMultimodalResult.MediaFormats formats = data instanceof String stringData + ? ToolMultimodalResult.MediaFormats.deserialize(stringData) : null; + if (formats != null) { handleMediaFormats(formats, mimeTypeStr, itemMap); } else if (data instanceof URI uri) { diff --git a/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResult.java b/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResult.java index 2d01c5cd4..b59575183 100644 --- a/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResult.java +++ b/agentic-spring-ai-agent-framework/src/main/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResult.java @@ -123,7 +123,7 @@ public static Media mediaFromResource(Resource resource, MimeType mimeType) { * @param base64 Base64-encoded data (raw string, not data URL; may be null) */ public static Media mediaFromUrlAndBase64(String url, String base64, MimeType mimeType) { - return Media.builder().mimeType(mimeType).data(new MediaFormats(url, base64)).build(); + return Media.builder().mimeType(mimeType).data(new MediaFormats(url, base64).serialize()).build(); } /** @@ -132,14 +132,49 @@ public static Media mediaFromUrlAndBase64(String url, String base64, MimeType mi * @param base64 Base64-encoded data (raw string, not data URL) */ public static Media mediaFromBase64(String base64, MimeType mimeType) { - return Media.builder().mimeType(mimeType).data(new MediaFormats(null, base64)).build(); + return Media.builder().mimeType(mimeType).data(new MediaFormats(null, base64).serialize()).build(); } /** - * Holds both URL and base64 when available. Used by {@link MultimodalToolCallResultConverter} - * to prefer the format matching OutputFormat without unnecessary conversion. + * Holds both URL and Base64 data so {@link MultimodalToolCallResultConverter} + * can select the requested output format without unnecessary downloads or encoding. + * + *

The length-prefixed string representation is accepted and preserved by Spring AI + * 2.0.1 while avoiding another Base64 encoding pass for potentially large payloads. */ public record MediaFormats(String url, String base64) { + + private static final String PREFIX = "agentic-media-formats:"; + + private String serialize() { + String serializedUrl = this.url != null ? this.url : ""; + String serializedBase64 = this.base64 != null ? this.base64 : ""; + return PREFIX + serializedUrl.length() + ":" + serializedUrl + serializedBase64; + } + + static MediaFormats deserialize(String data) { + if (data == null || !data.startsWith(PREFIX)) { + return null; + } + int lengthEnd = data.indexOf(':', PREFIX.length()); + if (lengthEnd < 0) { + return null; + } + try { + int urlLength = Integer.parseInt(data.substring(PREFIX.length(), lengthEnd)); + int urlStart = lengthEnd + 1; + int base64Start = urlStart + urlLength; + if (urlLength < 0 || base64Start > data.length()) { + return null; + } + String url = data.substring(urlStart, base64Start); + String base64 = data.substring(base64Start); + return new MediaFormats(url.isEmpty() ? null : url, base64.isEmpty() ? null : base64); + } + catch (NumberFormatException ex) { + return null; + } + } } public static class Builder { diff --git a/agentic-spring-ai-agent-framework/src/test/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResultTest.java b/agentic-spring-ai-agent-framework/src/test/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResultTest.java new file mode 100644 index 000000000..397a3eb1c --- /dev/null +++ b/agentic-spring-ai-agent-framework/src/test/java/io/github/agentic/spring/ai/graph/agent/tool/multimodal/ToolMultimodalResultTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024-2026 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.agentic.spring.ai.graph.agent.tool.multimodal; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.content.Media; +import org.springframework.util.MimeTypeUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ToolMultimodalResultTest { + + @Test + void preservesUrlAndBase64InSpringAiMedia() { + String url = "https://example.com/image.png"; + String base64 = "aW1hZ2U="; + + Media media = ToolMultimodalResult.mediaFromUrlAndBase64(url, base64, MimeTypeUtils.IMAGE_PNG); + String serialized = assertInstanceOf(String.class, media.getData()); + ToolMultimodalResult.MediaFormats formats = ToolMultimodalResult.MediaFormats.deserialize(serialized); + + assertNotNull(formats); + assertEquals(url, formats.url()); + assertEquals(base64, formats.base64()); + } + + @Test + void convertsCombinedMediaToRequestedOutputFormat() { + String url = "https://example.com/image.png"; + String base64 = "aW1hZ2U="; + ToolMultimodalResult result = ToolMultimodalResult.of("image", + ToolMultimodalResult.mediaFromUrlAndBase64(url, base64, MimeTypeUtils.IMAGE_PNG)); + + String urlJson = new MultimodalToolCallResultConverter(OutputFormat.url).convert(result, + ToolMultimodalResult.class); + String base64Json = new MultimodalToolCallResultConverter(OutputFormat.base64).convert(result, + ToolMultimodalResult.class); + + assertTrue(urlJson.contains("\"url\":\"" + url + "\"")); + assertTrue(base64Json.contains("\"data\":\"data:image/png;base64," + base64 + "\"")); + } + + @Test + void convertsBase64OnlyMediaWithoutUrl() { + String base64 = "YXVkaW8="; + ToolMultimodalResult result = ToolMultimodalResult.of("audio", + ToolMultimodalResult.mediaFromBase64(base64, MimeTypeUtils.parseMimeType("audio/mpeg"))); + + String json = new MultimodalToolCallResultConverter(OutputFormat.base64).convert(result, + ToolMultimodalResult.class); + + assertTrue(json.contains("\"data\":\"data:audio/mpeg;base64," + base64 + "\"")); + } +} diff --git a/agentic-spring-ai-bom/pom.xml b/agentic-spring-ai-bom/pom.xml index e82fc9829..90262b7f0 100644 --- a/agentic-spring-ai-bom/pom.xml +++ b/agentic-spring-ai-bom/pom.xml @@ -71,18 +71,18 @@ 3.11.0 3.1.2 - 3.1.2 + 3.5.6 3.5.0 - 3.3.0 - 0.8.10 - 1.5.0 - 3.1.1 + 3.4.0 + 0.8.15 + 1.8.0 + 3.1.4 2.2.3 3.7.0 3.5.0 - 4.0.0-M13 + 4.0.0-M16 3.4.5 - 3.3.0 + 3.5.1 0.0.39 @@ -238,7 +238,7 @@ com.diffplug.spotless spotless-maven-plugin - 2.44.5 + 2.46.1 @@ -273,7 +273,7 @@ com.mycila license-maven-plugin - 4.1 + 4.6 validate @@ -289,32 +289,36 @@ 2024 true -

HEADER
- - **/.antlr/** - **/aot.factories - **/.sdkmanrc - **/*.adoc - **/*.puml - **/pom.xml - **/*.properties - **/*.yaml - **/*.yml - **/*.map - **/*.html - **/*.xhtml - **/*.jsp - **/*.js - **/*.css - **/*.txt - **/*.xjb - **/*.ftl - **/*.xsd - **/*.xml - **/*.sh - **/generated/** - **/Dockerfile - + + +
com/mycila/maven/plugin/license/templates/APACHE-2.txt
+ + **/.antlr/** + **/aot.factories + **/.sdkmanrc + **/*.adoc + **/*.puml + **/pom.xml + **/*.properties + **/*.yaml + **/*.yml + **/*.map + **/*.html + **/*.xhtml + **/*.jsp + **/*.js + **/*.css + **/*.txt + **/*.xjb + **/*.ftl + **/*.xsd + **/*.xml + **/*.sh + **/generated/** + **/Dockerfile + +
+
diff --git a/pom.xml b/pom.xml index bf3f0f7cf..36a99647f 100644 --- a/pom.xml +++ b/pom.xml @@ -95,15 +95,15 @@ ${java.version} - 2.0.0 + 2.0.1 2.1.0-dev - 4.1.0 + 4.1.1 2.15.1 - 3.1.0 + 3.2.3 3.52.0 @@ -117,17 +117,17 @@ 1.0.6 - 2.21.0 + 2.22.0 3.20.0 - 2.0.0 + 2.0.1 0.2.5.Beta2 - 1.0.0 + 1.0.1 - 24.2.1 + 24.2.2 -Xms512m -Xmx1024m @@ -136,26 +136,26 @@ 3.6.2 [17,) - 3.3.0 - 4.0.0-M13 - 3.1.1 - 3.3.0 + 3.5.1 + 4.0.0-M16 + 3.1.4 + 3.4.0 0.8.10 - 1.5.0 + 1.8.0 3.5.0 3.1.2 3.1.2 3.14.0 - 1.4 + 1.5 0.0.39 1.0.1 3.2.8 false - 3.5.0 + 3.6.0 9.3 - 2.44.5 + 2.46.1 0.11.0 diff --git a/spring-boot-starters/agentic-spring-ai-starter-builtin-nodes/pom.xml b/spring-boot-starters/agentic-spring-ai-starter-builtin-nodes/pom.xml index 0e69ff193..cf9627008 100644 --- a/spring-boot-starters/agentic-spring-ai-starter-builtin-nodes/pom.xml +++ b/spring-boot-starters/agentic-spring-ai-starter-builtin-nodes/pom.xml @@ -55,7 +55,7 @@ - 3.5.3 + 3.7.1 diff --git a/spring-boot-starters/agentic-spring-ai-starter-config-nacos/src/main/java/io/github/agentic/spring/ai/agent/nacos/ObservationMetadataOpenAiChatOptions.java b/spring-boot-starters/agentic-spring-ai-starter-config-nacos/src/main/java/io/github/agentic/spring/ai/agent/nacos/ObservationMetadataOpenAiChatOptions.java index 3bc8892a4..5732fd9f3 100644 --- a/spring-boot-starters/agentic-spring-ai-starter-config-nacos/src/main/java/io/github/agentic/spring/ai/agent/nacos/ObservationMetadataOpenAiChatOptions.java +++ b/spring-boot-starters/agentic-spring-ai-starter-config-nacos/src/main/java/io/github/agentic/spring/ai/agent/nacos/ObservationMetadataOpenAiChatOptions.java @@ -44,7 +44,7 @@ private ObservationMetadataOpenAiChatOptions(OpenAiChatOptions options, Map/dev/null && pwd || true)}" +extensions_dir="${1:-$(cd "$core_dir/../agentic-spring-ai-extensions" 2>/dev/null && pwd || true)}" if [[ -z "$extensions_dir" || ! -f "$extensions_dir/pom.xml" ]]; then echo "Usage: $0 /path/to/agentic-spring-ai-extensions" >&2