Python: Fix function_approval_response extraction in AG-UI workflow path (#4550)

* Extract function_approval_response from workflow messages (#4546)

_extract_responses_from_messages now handles function_approval_response
content in addition to function_result content. Previously, approval
responses sent via the messages field were silently dropped because the
function only checked for content.type == "function_result".

The approval response is keyed by content.id and includes the approved
status, id, and serialized function_call — consistent with how
_coerce_content identifies approval response payloads.

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

* Apply pre-commit auto-fixes

* Fix #4546: Update docstring and add integration tests for message-based approvals

- Update _extract_responses_from_messages docstring to reflect that it
  now handles function_approval_response content in addition to
  function_result content.
- Add integration tests for run_workflow_stream across two turns with
  approval responses provided via messages (function_approvals) rather
  than resume.interrupts, covering both approved and denied scenarios.

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

* Address PR review feedback for #4546

- Use safer 'not .get("interrupt")' assertion instead of 'not in'
  to handle Pydantic v2 model_dump() including keys with None values
- Add unit test for mixed function_result and function_approval_response
  in the same message to TestExtractResponsesFromMessages

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-03-11 22:54:16 +00:00
committed by GitHub
co-authored by Copilot
parent e5d6e8ca98
commit 2f2495e196
2 changed files with 264 additions and 5 deletions
@@ -124,14 +124,28 @@ def _request_payload_from_request_event(request_event: Any) -> dict[str, Any] |
def _extract_responses_from_messages(messages: list[Message]) -> dict[str, Any]:
"""Extract request-info responses from incoming tool/function-result messages."""
"""Extract request-info responses from incoming messages.
Handles both ``function_result`` content (keyed by ``call_id``) and
``function_approval_response`` content (keyed by ``id``), so that
approval decisions sent via messages are forwarded into the workflow
responses map.
"""
responses: dict[str, Any] = {}
for message in messages:
for content in message.contents:
if content.type != "function_result" or not content.call_id:
continue
value = _coerce_json_value(content.result)
responses[str(content.call_id)] = value
if content.type == "function_result" and content.call_id:
value = _coerce_json_value(content.result)
responses[str(content.call_id)] = value
elif content.type == "function_approval_response" and getattr(content, "id", None):
approval_value: dict[str, Any] = {
"approved": getattr(content, "approved", False),
"id": str(content.id), # type: ignore[union-attr]
}
func_call = getattr(content, "function_call", None)
if func_call is not None:
approval_value["function_call"] = make_json_safe(func_call.to_dict())
responses[str(content.id)] = approval_value # type: ignore[union-attr]
return responses