From 6d2ae7ecac0132289ca1c91ca3bb8ab38af83c7a Mon Sep 17 00:00:00 2001 From: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> Date: Mon, 21 Sep 2026 23:08:46 +0530 Subject: [PATCH] fix(ag-ui): allow text events when response_format is a json schema dictionary When response_format is configured as a json schema dict rather than a Pydantic BaseModel, skip_text was previously set to True, which suppressed text content events. This change ensures skip_text is only enabled for Pydantic models whose fields map directly to state or message fields. Non-model formats keep emitting text content events as expected. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com> --- .../ag-ui/agent_framework_ag_ui/_agent_run.py | 16 +++++---- .../tests/ag_ui/test_structured_output.py | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py b/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py index 1baf001c640..e6250c1e374 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_agent_run.py @@ -2956,14 +2956,18 @@ async def _run_agent_stream( snapshot_seed_messages, protected_tool_call_ids=protected_tool_call_ids, ) - # Check for structured output mode (skip text content) + # Check for structured output mode (skip text content only for Pydantic models whose fields map to state/message) skip_text = False - response_format: type[Any] | None = None + response_format: Any | None = None + is_pydantic_response_format = False default_options = getattr(agent, "default_options", None) if isinstance(default_options, dict): + from pydantic import BaseModel + typed_default_options = cast(dict[str, Any], default_options) - response_format = cast(type[Any] | None, typed_default_options.get("response_format")) - skip_text = response_format is not None + response_format = typed_default_options.get("response_format") + is_pydantic_response_format = isinstance(response_format, type) and issubclass(response_format, BaseModel) + skip_text = is_pydantic_response_format # Handle empty messages (emit RunStarted immediately since no agent response) if not messages and not only_cancelled_resume: @@ -3578,8 +3582,8 @@ async def _run_agent_stream( from agent_framework import AgentResponse from pydantic import BaseModel - if not (isinstance(response_format, type) and issubclass(response_format, BaseModel)): - logger.warning("Skipping structured output parsing: response_format is not a Pydantic model type.") + if not is_pydantic_response_format: + logger.debug("Skipping structured output parsing: response_format is not a Pydantic model type.") else: logger.info(f"Processing structured output, update count: {len(all_updates)}") final_response = AgentResponse.from_updates(all_updates, output_format_type=response_format) diff --git a/python/packages/ag-ui/tests/ag_ui/test_structured_output.py b/python/packages/ag-ui/tests/ag_ui/test_structured_output.py index df612ef68c2..db7cf09ecc9 100644 --- a/python/packages/ag-ui/tests/ag_ui/test_structured_output.py +++ b/python/packages/ag-ui/tests/ag_ui/test_structured_output.py @@ -261,3 +261,37 @@ async def stream_fn( # Should only have start and end events assert len(events) == 2 # RunStarted, RunFinished + + +async def test_structured_output_with_json_schema_mapping_emits_text( + streaming_chat_client_stub, stream_from_updates_fixture +): + """Test that JSON Schema mapping response_format still emits text content events.""" + from agent_framework.ag_ui import AgentFrameworkAgent + + json_payload = '{"name": "Alice", "age": 30}' + updates = [ChatResponseUpdate(contents=[Content.from_text(text=json_payload)])] + + agent = Agent( + name="extractor", + instructions="Extract person info as JSON.", + client=streaming_chat_client_stub(stream_from_updates_fixture(updates)), + ) + agent.default_options = { + "response_format": { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + "required": ["name", "age"], + } + } + + wrapper = AgentFrameworkAgent(agent=agent) + input_data = {"messages": [{"role": "user", "content": "Extract info for Alice aged 30"}]} + + events: list[Any] = [] + async for event in wrapper.run(input_data): + events.append(event) + + text_events = [e for e in events if e.type == "TEXT_MESSAGE_CONTENT"] + assert len(text_events) > 0 + assert text_events[0].delta == json_payload