diff --git a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py index be04a1f397..526430a3e1 100644 --- a/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py +++ b/python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py @@ -1258,6 +1258,22 @@ async def _to_outputs(stream: ResponseEventStream, content: Content) -> AsyncIte max_output_length=content.max_output_length, ): yield event + elif content.type == "oauth_consent_request" and content.consent_link: + label = (content.additional_properties or {}).get("server_label", "") + text = f"OAuth consent required for MCP server '{label}'. Please visit:\n{content.consent_link}" + async for event in stream.aoutput_item_message(text): + yield event + elif content.type == "function_approval_request" and content.function_call is not None: + fn = content.function_call + label = (content.additional_properties or {}).get("server_label", "") + text = ( + f"Approval required for MCP tool '{fn.name or 'unknown'}'" + + (f" on server '{label}'" if label else "") + + f" (request id: {content.id}).\n" + + f"Arguments: {_arguments_to_str(fn.arguments)}" + ) + async for event in stream.aoutput_item_message(text): + yield event else: # Log a warning for unsupported content types instead of raising an error to avoid breaking the response stream. logger.warning(f"Content type '{content.type}' is not supported yet. This is usually safe to ignore.") diff --git a/python/packages/foundry_hosting/tests/test_responses.py b/python/packages/foundry_hosting/tests/test_responses.py index e7c0599ad3..be0530c5f8 100644 --- a/python/packages/foundry_hosting/tests/test_responses.py +++ b/python/packages/foundry_hosting/tests/test_responses.py @@ -25,13 +25,15 @@ RawAgent, ResponseStream, ) -from azure.ai.agentserver.responses import InMemoryResponseProvider +from azure.ai.agentserver.responses import InMemoryResponseProvider, ResponseEventStream +from azure.ai.agentserver.responses.models import ResponseStreamEvent from typing_extensions import Any from agent_framework_foundry_hosting import ResponsesHostServer from agent_framework_foundry_hosting._responses import ( _item_to_message, # pyright: ignore[reportPrivateUsage] _output_item_to_message, # pyright: ignore[reportPrivateUsage] + _to_outputs, # pyright: ignore[reportPrivateUsage] ) # region Helpers @@ -134,6 +136,11 @@ def _sse_event_types(events: list[dict[str, Any]]) -> list[str]: return [e["event"] for e in events] +async def _empty_response_events(*args: Any, **kwargs: Any) -> AsyncIterator[ResponseStreamEvent]: + if False: + yield MagicMock() + + # endregion @@ -563,6 +570,56 @@ async def test_mcp_tool_call_streaming(self) -> None: # endregion +# region _to_outputs conversion + + +class TestToOutputs: + """Tests for _to_outputs covering streaming Content to ResponseStreamEvent conversion.""" + + async def test_oauth_consent_request_emits_consent_link_message(self, caplog: pytest.LogCaptureFixture) -> None: + stream = MagicMock(spec=ResponseEventStream) + stream.aoutput_item_message.side_effect = _empty_response_events + content = Content.from_oauth_consent_request( + "https://example.com/consent", + additional_properties={"server_label": "github"}, + ) + + async for _ in _to_outputs(stream, content): + pass + + stream.aoutput_item_message.assert_called_once() + text = stream.aoutput_item_message.call_args.args[0] + assert "OAuth consent required" in text + assert "github" in text + assert "https://example.com/consent" in text + assert "not supported yet" not in caplog.text + + async def test_function_approval_request_emits_approval_message(self, caplog: pytest.LogCaptureFixture) -> None: + stream = MagicMock(spec=ResponseEventStream) + stream.aoutput_item_message.side_effect = _empty_response_events + function_call = Content.from_function_call("call-1", "my_tool", arguments='{"x": 1}') + content = Content.from_function_approval_request( + "req-123", + function_call, + additional_properties={"server_label": "github"}, + ) + + async for _ in _to_outputs(stream, content): + pass + + stream.aoutput_item_message.assert_called_once() + text = stream.aoutput_item_message.call_args.args[0] + assert "Approval required" in text + assert "my_tool" in text + assert "github" in text + assert "req-123" in text + assert '{"x": 1}' in text + assert "not supported yet" not in caplog.text + + +# endregion + + # region _output_item_to_message conversion