Python: Fix: Parse oauth_consent_request events in Azure AI client (#4197)

* Fix: Parse oauth_consent_request events in Azure AI client (#3950)

When Azure AI Agent Service returns an oauth_consent_request output item
for OAuth-protected MCP tools, the base OpenAI responses parser drops it
(hits case _ default branch). This causes agent runs to complete silently
with zero content.

Changes:
- Add oauth_consent_request ContentType and Content.from_oauth_consent_request()
  factory with consent_link field and user_input_request=True
- Override _parse_response_from_openai and _parse_chunk_from_openai in
  RawAzureAIClient to intercept Azure-specific oauth_consent_request items
- Add _emit_oauth_consent helper in AG-UI to emit CustomEvent for frontends
- Add tests proving base parser drops the event and Azure AI override catches it

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* addressed comment

* addressed comments

* addressed comments

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-03-03 15:02:03 -08:00
committed by GitHub
Unverified
parent 7135ed13eb
commit 2b3c401848
6 changed files with 259 additions and 0 deletions
@@ -345,6 +345,7 @@ ContentType = Literal[
"shell_command_output",
"function_approval_request",
"function_approval_response",
"oauth_consent_request",
]
@@ -498,6 +499,8 @@ class Content:
function_call: Content | None = None,
user_input_request: bool | None = None,
approved: bool | None = None,
# OAuth consent fields
consent_link: str | None = None,
# Common fields
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
@@ -546,6 +549,7 @@ class Content:
self.function_call = function_call
self.user_input_request = user_input_request
self.approved = approved
self.consent_link = consent_link
@classmethod
def from_text(
@@ -1122,6 +1126,37 @@ class Content:
raw_representation=raw_representation,
)
@classmethod
def from_oauth_consent_request(
cls: type[ContentT],
consent_link: str,
*,
annotations: Sequence[Annotation] | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
raw_representation: Any = None,
) -> ContentT:
"""Create OAuth consent request content.
Args:
consent_link: The URL the user must visit to complete OAuth consent.
Keyword Args:
annotations: Optional annotations.
additional_properties: Optional additional properties.
raw_representation: Optional raw representation from the provider.
Returns:
A new Content instance with type ``oauth_consent_request``.
"""
return cls(
"oauth_consent_request",
consent_link=consent_link,
user_input_request=True,
annotations=annotations,
additional_properties=additional_properties,
raw_representation=raw_representation,
)
def to_function_approval_response(
self,
approved: bool,
@@ -1176,6 +1211,7 @@ class Content:
"user_input_request",
"approved",
"id",
"consent_link",
"additional_properties",
)
@@ -3424,3 +3424,30 @@ class TestResponseStreamEdgeCases:
# endregion
# region OAuth Consent Content
def test_oauth_consent_request_creation():
"""Test Content.from_oauth_consent_request creates the correct content."""
content = Content.from_oauth_consent_request(
consent_link="https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc",
)
assert content.type == "oauth_consent_request"
assert content.consent_link == "https://login.microsoftonline.com/common/oauth2/authorize?client_id=abc"
assert content.user_input_request is True
def test_oauth_consent_request_serialization_roundtrip():
"""Test that oauth_consent_request content serializes and includes consent_link."""
content = Content.from_oauth_consent_request(
consent_link="https://login.microsoftonline.com/consent",
)
d = content.to_dict()
assert d["type"] == "oauth_consent_request"
assert d["consent_link"] == "https://login.microsoftonline.com/consent"
assert d["user_input_request"] is True
# endregion