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 @@ -294,12 +294,12 @@ def _parse_multimodal_media_part(part: dict[str, Any]) -> Content | None:
if source_type in {"url", "uri"}:
url = cast(str | None, source_dict.get("url") or source_dict.get("uri"))
elif source_type in {"base64", "data", "binary"}:
data = cast(str | None, source_dict.get("data"))
data = cast(str | None, source_dict.get("value") or source_dict.get("data"))
elif source_type in {"id", "file"}:
binary_id = cast(str | None, source_dict.get("id"))
else:
url = cast(str | None, source_dict.get("url") or source_dict.get("uri") or url)
data = cast(str | None, source_dict.get("data") or data)
data = cast(str | None, source_dict.get("value") or source_dict.get("data") or data)
binary_id = cast(str | None, source_dict.get("id") or binary_id)

if isinstance(url, str) and url:
Expand Down
4 changes: 2 additions & 2 deletions python/packages/ag-ui/agent_framework_ag_ui/_run_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ def _emit_text_reasoning(content: Content, flow: FlowState | None = None) -> lis
events.extend(_close_reasoning_block(flow))
# Open new reasoning block.
events.append(ReasoningStartEvent(message_id=message_id))
events.append(ReasoningMessageStartEvent(message_id=message_id, role="assistant"))
events.append(ReasoningMessageStartEvent(message_id=message_id, role="reasoning"))
flow.reasoning_message_id = message_id

if text:
Expand All @@ -613,7 +613,7 @@ def _emit_text_reasoning(content: Content, flow: FlowState | None = None) -> lis
else:
# No flow -- backward-compatible full sequence per call.
events.append(ReasoningStartEvent(message_id=message_id))
events.append(ReasoningMessageStartEvent(message_id=message_id, role="assistant"))
events.append(ReasoningMessageStartEvent(message_id=message_id, role="reasoning"))

if text:
events.append(ReasoningMessageContentEvent(message_id=message_id, delta=text))
Expand Down
2 changes: 1 addition & 1 deletion python/packages/ag-ui/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.1.0,<2",
"ag-ui-protocol==0.1.13",
"ag-ui-protocol>=0.1.16,<0.2",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<0.42.0"
]
Expand Down
64 changes: 64 additions & 0 deletions python/packages/ag-ui/tests/ag_ui/test_message_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1760,3 +1760,67 @@ def test_multi_turn_with_reasoning_in_prior_snapshot(self):
assert "First answer" in texts
assert "Follow-up question" in texts
assert "Prior reasoning" not in texts


def test_parse_multimodal_media_part_base64_value_field():
"""Source with type='base64' reads data 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": "image", "source": {"type": "base64", "value": "aGVsbG8=", "mimeType": "image/png"}}
)
assert result is not None
assert "aGVsbG8=" in result.uri


def test_parse_multimodal_media_part_data_source_value_field():
"""Source with type='data' reads data 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": "image", "source": {"type": "data", "value": "aGVsbG8=", "mimeType": "image/png"}}
)
assert result is not None
assert "aGVsbG8=" in result.uri


def test_parse_multimodal_media_part_base64_data_field_backward_compat():
"""Source with type='base64' still supports deprecated 'data' field."""
from agent_framework_ag_ui._message_adapters import _parse_multimodal_media_part

result = _parse_multimodal_media_part(
{"type": "image", "source": {"type": "base64", "data": "aGVsbG8=", "mimeType": "image/png"}}
)
assert result is not None
assert "aGVsbG8=" in result.uri


def test_parse_multimodal_media_part_value_preferred_over_data():
"""When both 'value' and 'data' are present, 'value' takes precedence."""
from agent_framework_ag_ui._message_adapters import _parse_multimodal_media_part

result = _parse_multimodal_media_part(
{
"type": "image",
"source": {
"type": "base64",
"value": "dmFsdWU=",
"data": "ZGF0YQ==",
"mimeType": "image/png",
},
}
)
assert result is not None
# 'value' field content should be used (base64 of "value")
assert "dmFsdWU=" in result.uri


def test_parse_multimodal_media_part_unknown_source_value_fallback():
"""Unknown source type falls back to 'value' field before 'data' field."""
from agent_framework_ag_ui._message_adapters import _parse_multimodal_media_part

result = _parse_multimodal_media_part(
{"type": "image", "source": {"type": "custom", "value": "aGVsbG8=", "mimeType": "image/png"}}
)
assert result is not None
assert "aGVsbG8=" in result.uri
33 changes: 32 additions & 1 deletion python/packages/ag-ui/tests/ag_ui/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -1244,7 +1244,7 @@ def test_produces_reasoning_events(self):
assert events[0].message_id == "reason_1"
assert isinstance(events[1], ReasoningMessageStartEvent)
assert events[1].message_id == "reason_1"
assert events[1].role == "assistant"
assert events[1].role == "reasoning"
assert isinstance(events[2], ReasoningMessageContentEvent)
assert events[2].message_id == "reason_1"
assert events[2].delta == "The user is asking about weather, so I should call the weather tool."
Expand Down Expand Up @@ -1640,3 +1640,34 @@ def test_reasoning_distinct_ids_close_previous_block(self):
# close: MsgEnd(block2) + End(block2)
assert isinstance(close[0], ReasoningMessageEndEvent)
assert close[0].message_id == "block2"


class TestReasoningEventRole:
"""Tests that reasoning events use role='reasoning' per AG-UI spec."""

def test_reasoning_role_without_flow(self):
"""ReasoningMessageStartEvent uses role='reasoning' in non-flow mode."""
content = Content.from_text_reasoning(
id="reason_role_1",
text="Thinking about the question.",
)

events = _emit_text_reasoning(content)

msg_starts = [e for e in events if isinstance(e, ReasoningMessageStartEvent)]
assert len(msg_starts) == 1
assert msg_starts[0].role == "reasoning"

def test_reasoning_role_with_flow(self):
"""ReasoningMessageStartEvent uses role='reasoning' in streaming flow mode."""
flow = FlowState()
content = Content.from_text_reasoning(
id="reason_role_2",
text="Reasoning in streaming mode.",
)

events = _emit_text_reasoning(content, flow)

msg_starts = [e for e in events if isinstance(e, ReasoningMessageStartEvent)]
assert len(msg_starts) == 1
assert msg_starts[0].role == "reasoning"
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from pydantic import Field

try:
import orjson
import orjson # pyright: ignore[reportMissingImports]
except ImportError:
orjson = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from pydantic import Field

try:
import orjson
import orjson # pyright: ignore[reportMissingImports]
except ImportError:
orjson = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import os
import subprocess
from random import randint
from typing import Annotated

from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
from typing import Annotated

# Load environment variables from .env file
load_dotenv()
Expand Down
8 changes: 4 additions & 4 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading