Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.HintBlock;
import io.agentscope.core.message.ImageBlock;
import io.agentscope.core.message.MessageMetadataKeys;
Expand Down Expand Up @@ -134,7 +135,8 @@ protected boolean hasMediaContent(Msg msg) {
for (ContentBlock block : msg.getContent()) {
if (block instanceof ImageBlock
|| block instanceof AudioBlock
|| block instanceof VideoBlock) {
|| block instanceof VideoBlock
|| block instanceof DataBlock) {
return true;
}
}
Expand Down Expand Up @@ -218,6 +220,9 @@ protected String convertToolResultToString(List<ContentBlock> output) {
} else if (block instanceof VideoBlock vb) {
String reference = convertMediaBlockToTextReference(vb, "video");
textualOutput.add(reference);
} else if (block instanceof DataBlock db) {
String reference = convertMediaBlockToTextReference(db, "data");
textualOutput.add(reference);
}
// Other block types (e.g., ThinkingBlock) are ignored
}
Expand Down Expand Up @@ -272,6 +277,8 @@ private Source getSourceFromBlock(ContentBlock block) {
return ab.getSource();
} else if (block instanceof VideoBlock vb) {
return vb.getSource();
} else if (block instanceof DataBlock db) {
return db.getSource();
}
throw new IllegalArgumentException("Unsupported block type: " + block.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.agentscope.core.formatter.dashscope.dto.DashScopeContentPart;
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.ImageBlock;
import io.agentscope.core.message.Source;
import io.agentscope.core.message.URLSource;
Expand Down Expand Up @@ -192,4 +193,72 @@ public DashScopeContentPart convertAudioBlockToContentPart(AudioBlock audioBlock
String audioUrl = convertAudioBlockToUrl(audioBlock);
return DashScopeContentPart.audio(audioUrl);
}

/**
* Convert DataBlock to DashScopeContentPart by resolving the MIME type and routing
* to the appropriate image / audio / video slot.
*
* <p>MIME type resolution order:
* <ol>
* <li>{@code Base64Source.mediaType} — always explicit</li>
* <li>{@code URLSource.mimeType} — caller-supplied hint for extension-less URLs</li>
* <li>{@code MediaUtils.determineMediaType(url)} — extension-based inference</li>
* </ol>
*
* @param dataBlock The data block to convert
* @return DashScopeContentPart for the resolved media type
* @throws Exception If conversion fails or MIME type cannot be resolved
*/
public DashScopeContentPart convertDataBlockToContentPart(DataBlock dataBlock)
throws Exception {
Source source = dataBlock.getSource();
String mimeType = resolveMimeType(source);

if (mimeType.startsWith("image/")) {
String url = sourceToUrl(source);
return DashScopeContentPart.builder().image(url).build();
} else if (mimeType.startsWith("audio/")) {
String url = sourceToUrl(source);
return DashScopeContentPart.audio(url);
} else if (mimeType.startsWith("video/")) {
String url = sourceToUrl(source);
return DashScopeContentPart.builder().video(url).build();
} else {
throw new IllegalArgumentException(
"Cannot route DataBlock: unrecognised MIME type '" + mimeType + "'");
}
}

// resolve MIME type from any Source subtype
private String resolveMimeType(Source source) {
if (source instanceof Base64Source b64) {
return b64.getMediaType();
}
if (source instanceof URLSource urlSource) {
String hint = urlSource.getMimeType();
if (hint != null && !hint.isBlank()) {
return hint;
}
String inferred = MediaUtils.determineMediaType(urlSource.getUrl());
if (!"application/octet-stream".equals(inferred)) {
return inferred;
}
throw new IllegalArgumentException(
"Cannot determine MIME type for URL '"
+ urlSource.getUrl()
+ "'; set URLSource.mimeType explicitly");
}
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
}

// convert any Source to a URL/data-URL string
private String sourceToUrl(Source source) throws Exception {
if (source instanceof URLSource urlSource) {
return MediaUtils.urlToProtocolUrl(urlSource.getUrl());
}
if (source instanceof Base64Source b64) {
return String.format("data:%s;base64,%s", b64.getMediaType(), b64.getData());
}
throw new IllegalArgumentException("Unsupported source type: " + source.getClass());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.agentscope.core.formatter.dashscope.dto.DashScopeMessage;
import io.agentscope.core.message.AudioBlock;
import io.agentscope.core.message.ContentBlock;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.HintBlock;
import io.agentscope.core.message.ImageBlock;
import io.agentscope.core.message.MessageMetadataKeys;
Expand Down Expand Up @@ -127,6 +128,15 @@ private DashScopeMessage convertToMultimodalContent(Msg msg) {
DashScopeContentPart.text(
"[Audio - processing failed: " + e.getMessage() + "]"));
}
} else if (block instanceof DataBlock dataBlock) {
try {
contents.add(mediaConverter.convertDataBlockToContentPart(dataBlock));
} catch (Exception e) {
log.warn("Failed to process DataBlock: {}", e.getMessage());
contents.add(
DashScopeContentPart.text(
"[Media - processing failed: " + e.getMessage() + "]"));
}
} else if (block instanceof HintBlock hb) {
contents.add(DashScopeContentPart.text(hb.getHint()));
} else if (block instanceof ThinkingBlock) {
Expand Down Expand Up @@ -286,7 +296,8 @@ private boolean hasMediaContent(List<ContentBlock> blocks) {
for (ContentBlock block : blocks) {
if (block instanceof ImageBlock
|| block instanceof AudioBlock
|| block instanceof VideoBlock) {
|| block instanceof VideoBlock
|| block instanceof DataBlock) {
return true;
}
}
Expand Down Expand Up @@ -331,6 +342,15 @@ private List<DashScopeContentPart> convertContentBlocks(List<ContentBlock> block
DashScopeContentPart.text(
"[Video - processing failed: " + e.getMessage() + "]"));
}
} else if (block instanceof DataBlock db) {
try {
content.add(mediaConverter.convertDataBlockToContentPart(db));
} catch (Exception e) {
log.warn("Failed to process DataBlock in tool result: {}", e.getMessage());
content.add(
DashScopeContentPart.text(
"[Media - processing failed: " + e.getMessage() + "]"));
}
}
}
return content;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.agentscope.core.message;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;

Expand All @@ -35,20 +36,40 @@
*
* <p>Using URL sources is more efficient for large media files and allows
* the system to stream content rather than loading everything into memory.
*
* <p>When the URL has no file extension (e.g. CDN signed URLs), set {@code mimeType}
* explicitly so converters can route the content to the correct media slot without
* relying on extension-based inference.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class URLSource extends Source {

private final String url;

@JsonProperty("mime_type")
private final String mimeType;

/**
* Creates a new URL source for JSON deserialization.
*
* @param url The URL pointing to the media content
* @param mimeType Optional MIME type hint (e.g. "image/jpeg"); may be null
* @throws NullPointerException if url is null
*/
@JsonCreator
public URLSource(@JsonProperty("url") String url) {
public URLSource(@JsonProperty("url") String url, @JsonProperty("mime_type") String mimeType) {
this.url = Objects.requireNonNull(url, "url cannot be null");
this.mimeType = mimeType;
}

/**
* Creates a new URL source without a MIME type hint.
*
* @param url The URL pointing to the media content
* @throws NullPointerException if url is null
*/
public URLSource(String url) {
this(url, null);
}

/**
Expand All @@ -60,6 +81,19 @@ public String getUrl() {
return url;
}

/**
* Gets the optional MIME type hint for this URL source.
*
* <p>When present, converters use this value instead of inferring the type
* from the URL's file extension. Useful for extension-less URLs such as
* CDN signed links or API-generated media endpoints.
*
* @return The MIME type (e.g. "image/jpeg"), or null if not set
*/
public String getMimeType() {
return mimeType;
}

/**
* Creates a new builder for constructing URLSource instances.
*
Expand All @@ -76,6 +110,8 @@ public static class Builder {

private String url;

private String mimeType;

/**
* Sets the URL for the media content.
*
Expand All @@ -88,13 +124,24 @@ public Builder url(String url) {
}

/**
* Builds a new URLSource with the configured URL.
* Sets an optional MIME type hint for extension-less URLs.
*
* @param mimeType The MIME type (e.g. "video/mp4")
* @return This builder for chaining
*/
public Builder mimeType(String mimeType) {
this.mimeType = mimeType;
return this;
}

/**
* Builds a new URLSource with the configured fields.
*
* @return A new URLSource instance
* @throws NullPointerException if url is null
*/
public URLSource build() {
return new URLSource(url);
return new URLSource(url, mimeType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import io.agentscope.core.formatter.dashscope.dto.DashScopeContentPart;
import io.agentscope.core.message.Base64Source;
import io.agentscope.core.message.DataBlock;
import io.agentscope.core.message.ImageBlock;
import io.agentscope.core.message.URLSource;
import io.agentscope.core.message.VideoBlock;
Expand Down Expand Up @@ -348,4 +350,109 @@ void testVideoBlockDefaultConstructorNullParameters() {
assertNull(videoBlock.getMaxPixels());
assertNull(videoBlock.getTotalPixels());
}

@Test
void testConvertDataBlockImageRemoteUrl() throws Exception {
DataBlock block =
DataBlock.builder()
.source(URLSource.builder().url("https://example.com/photo.png").build())
.build();

DashScopeContentPart result = converter.convertDataBlockToContentPart(block);

assertNotNull(result);
assertEquals("https://example.com/photo.png", result.getImage());
}

@Test
void testConvertDataBlockImageBase64() throws Exception {
DataBlock block =
DataBlock.builder()
.source(
Base64Source.builder()
.mediaType("image/png")
.data("iVBORw0KGgo=")
.build())
.build();

DashScopeContentPart result = converter.convertDataBlockToContentPart(block);

assertNotNull(result);
assertEquals("data:image/png;base64,iVBORw0KGgo=", result.getImage());
}

@Test
void testConvertDataBlockVideoRemoteUrl() throws Exception {
DataBlock block =
DataBlock.builder()
.source(URLSource.builder().url("https://example.com/clip.mp4").build())
.build();

DashScopeContentPart result = converter.convertDataBlockToContentPart(block);

assertNotNull(result);
assertEquals("https://example.com/clip.mp4", result.getVideoAsString());
}

@Test
void testConvertDataBlockAudioBase64() throws Exception {
DataBlock block =
DataBlock.builder()
.source(
Base64Source.builder()
.mediaType("audio/mp3")
.data("ZmFrZSBhdWRpbyBkYXRh")
.build())
.build();

DashScopeContentPart result = converter.convertDataBlockToContentPart(block);

assertNotNull(result);
assertEquals("data:audio/mp3;base64,ZmFrZSBhdWRpbyBkYXRh", result.getAudio());
}

@Test
void testConvertDataBlockWithMimeTypeHintOverridesExtension() throws Exception {
// mimeType hint should take precedence over extension-based inference
DataBlock block =
DataBlock.builder()
.source(
URLSource.builder()
.url("https://cdn.example.com/media/abc123")
.mimeType("image/jpeg")
.build())
.build();

DashScopeContentPart result = converter.convertDataBlockToContentPart(block);

assertNotNull(result);
assertNotNull(result.getImage());
}

@Test
void testConvertDataBlockNoExtensionNoHintThrows() {
DataBlock block =
DataBlock.builder()
.source(
URLSource.builder()
.url("https://cdn.example.com/media/abc123")
.build())
.build();

assertThrows(Exception.class, () -> converter.convertDataBlockToContentPart(block));
}

@Test
void testConvertDataBlockUnknownMimeTypeThrows() {
DataBlock block =
DataBlock.builder()
.source(
Base64Source.builder()
.mediaType("application/octet-stream")
.data("ZmFrZQ==")
.build())
.build();

assertThrows(Exception.class, () -> converter.convertDataBlockToContentPart(block));
}
}
Loading
Loading