From 4f7251795a2132577aa133b4865802ad7c431d8c Mon Sep 17 00:00:00 2001 From: atty57 <99388680+atty57@users.noreply.github.com> Date: Thu, 13 Aug 2026 14:22:17 -0400 Subject: [PATCH] Python: Fix AG-UI url source dropping attachments when the URL is in source.value The ag-ui-protocol `InputContentUrlSource` carries the URL in `source.value`, but `_extract_multimodal_source_fields` only read `source.url`/`source.uri` for url-typed sources, so attachments sent in the spec shape were dropped during the AG-UI to MAF conversion. The base64 branch already read `source.value` correctly. Read `source.value` first, keeping `url`/`uri` as fallbacks for the non-spec shape. Adds tests for both. Fixes #7653 --- .../_message_adapters.py | 9 ++--- .../tests/ag_ui/test_message_adapters.py | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py index daaa0be5c0..df196e6672 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py @@ -360,9 +360,10 @@ def _extract_multimodal_source_fields( ) -> tuple[str | None, str | None, str | None, str | None]: """Extract ``(url, data, binary_id, mime_type)`` from an AG-UI multimodal part. - Handles both the current AG-UI spec (``source.value`` for base64 payloads) and the - legacy ``source.data`` field for backward compatibility. Returned values are the - raw extracted strings (or ``None`` when absent); callers apply their own defaults. + Handles both the current AG-UI spec (``source.value`` for both URL and base64 + payloads) and the legacy ``source.url``/``source.data`` fields for backward + compatibility. Returned values are the raw extracted strings (or ``None`` when + absent); callers apply their own defaults. """ mime_type = cast(str | None, part.get("mimeType") or part.get("mime_type")) url = cast(str | None, part.get("url") or part.get("uri")) @@ -378,7 +379,7 @@ def _extract_multimodal_source_fields( mime_type = source_mime if source_type in {"url", "uri"}: - url = cast(str | None, source_dict.get("url") or source_dict.get("uri")) + url = cast(str | None, source_dict.get("value") or source_dict.get("url") or source_dict.get("uri")) elif source_type in {"base64", "data", "binary"}: data = cast(str | None, source_dict.get("value") or source_dict.get("data")) elif source_type in {"id", "file"}: diff --git a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py index 04830dfb99..89ab07f6b8 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py +++ b/python/packages/ag-ui/tests/ag_ui/test_message_adapters.py @@ -1995,3 +1995,39 @@ def test_parse_multimodal_media_part_unknown_source_value_fallback(): ) assert result is not None assert "aGVsbG8=" in result.uri # type: ignore[operator] # pyrefly: ignore[not-iterable] # ty: ignore[unsupported-operator] + + +def test_parse_multimodal_media_part_url_value_field(): + """Source with type='url' reads the URL from the 'value' field per AG-UI spec.""" + from agent_framework_ag_ui._message_adapters import _parse_multimodal_media_part + + result = _parse_multimodal_media_part( + { + "type": "document", + "source": { + "type": "url", + "value": "https://example.com/files/document.pdf", + "mime_type": "application/pdf", + }, + } + ) + assert result is not None + assert result.uri == "https://example.com/files/document.pdf" + assert result.media_type == "application/pdf" + + +def test_parse_multimodal_media_part_url_field_backward_compat(): + """Source with type='url' still supports the non-spec 'url' and 'uri' fields.""" + from agent_framework_ag_ui._message_adapters import _parse_multimodal_media_part + + from_url = _parse_multimodal_media_part( + {"type": "image", "source": {"type": "url", "url": "https://example.com/a.png"}} + ) + assert from_url is not None + assert from_url.uri == "https://example.com/a.png" + + from_uri = _parse_multimodal_media_part( + {"type": "image", "source": {"type": "uri", "uri": "https://example.com/b.png"}} + ) + assert from_uri is not None + assert from_uri.uri == "https://example.com/b.png"