Python: [Breaking] Python: Respond with AgentRunResponse with serialized structured output (#2285)

* Respond with AgentRunResponse

* Fix response_Format type

* Address comments

* Fix tests

* Fix log

* Addressed comments

* Code cleanup

* Use AgentTask vs Generator

* Address comments

* use lazy logging

* fix mypy errors
This commit is contained in:
Laveesh Rohra
2025-11-26 18:44:28 +00:00
committed by GitHub
parent 0f2c5e6cb8
commit 306c81aef8
12 changed files with 484 additions and 432 deletions
@@ -12,7 +12,7 @@ from typing import Any, TypeVar
from unittest.mock import AsyncMock, Mock, patch
import pytest
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage, Role
from agent_framework import AgentRunResponse, AgentRunResponseUpdate, ChatMessage, ErrorContent, Role
from pydantic import BaseModel
from agent_framework_azurefunctions._durable_agent_state import (
@@ -133,10 +133,8 @@ class TestAgentEntityRunAgent:
assert getattr(sent_message.role, "value", sent_message.role) == "user"
# Verify result
assert result["status"] == "success"
assert result["response"] == "Test response"
assert result["message"] == "Test message"
assert result["thread_id"] == "conv-123"
assert isinstance(result, AgentRunResponse)
assert result.text == "Test response"
async def test_run_agent_streaming_callbacks_invoked(self) -> None:
"""Ensure streaming updates trigger callbacks and run() is not used."""
@@ -168,8 +166,8 @@ class TestAgentEntityRunAgent:
},
)
assert result["status"] == "success"
assert "Hello" in result.get("response", "")
assert isinstance(result, AgentRunResponse)
assert "Hello" in result.text
assert callback.stream_mock.await_count == len(updates)
assert callback.response_mock.await_count == 1
mock_agent.run.assert_not_called()
@@ -215,8 +213,8 @@ class TestAgentEntityRunAgent:
},
)
assert result["status"] == "success"
assert result.get("response") == "Final response"
assert isinstance(result, AgentRunResponse)
assert result.text == "Final response"
assert callback.stream_mock.await_count == 0
assert callback.response_mock.await_count == 1
@@ -294,44 +292,6 @@ class TestAgentEntityRunAgent:
mock_context, {"message": "Message", "thread_id": None, "correlationId": "corr-entity-5"}
)
async def test_run_agent_handles_response_without_text_attribute(self) -> None:
"""Test that run_agent handles responses without a text attribute."""
mock_agent = Mock()
class NoTextResponse(AgentRunResponse):
@property
def text(self) -> str: # type: ignore[override]
raise AttributeError("text attribute missing")
mock_response = NoTextResponse(messages=[ChatMessage(role="assistant", text="ignored")])
mock_agent.run = AsyncMock(return_value=mock_response)
entity = AgentEntity(mock_agent)
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-6"}
)
# Should handle gracefully
assert result["status"] == "success"
assert result["response"] == "Error extracting response"
async def test_run_agent_handles_none_response_text(self) -> None:
"""Test that run_agent handles responses with None text."""
mock_agent = Mock()
mock_agent.run = AsyncMock(return_value=_agent_response(None))
entity = AgentEntity(mock_agent)
mock_context = Mock()
result = await entity.run_agent(
mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-7"}
)
assert result["status"] == "success"
assert result["response"] == "No response"
async def test_run_agent_multiple_conversations(self) -> None:
"""Test that run_agent maintains history across multiple messages."""
mock_agent = Mock()
@@ -621,10 +581,12 @@ class TestErrorHandling:
mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-1"}
)
assert result["status"] == "error"
assert "error" in result
assert "Agent failed" in result["error"]
assert result["error_type"] == "Exception"
assert isinstance(result, AgentRunResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, ErrorContent)
assert "Agent failed" in (content.message or "")
assert content.error_code == "Exception"
async def test_run_agent_handles_value_error(self) -> None:
"""Test that run_agent handles ValueError instances."""
@@ -638,9 +600,12 @@ class TestErrorHandling:
mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-2"}
)
assert result["status"] == "error"
assert result["error_type"] == "ValueError"
assert "Invalid input" in result["error"]
assert isinstance(result, AgentRunResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, ErrorContent)
assert content.error_code == "ValueError"
assert "Invalid input" in str(content.message)
async def test_run_agent_handles_timeout_error(self) -> None:
"""Test that run_agent handles TimeoutError instances."""
@@ -654,8 +619,11 @@ class TestErrorHandling:
mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-3"}
)
assert result["status"] == "error"
assert result["error_type"] == "TimeoutError"
assert isinstance(result, AgentRunResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, ErrorContent)
assert content.error_code == "TimeoutError"
def test_entity_function_handles_exception_in_operation(self) -> None:
"""Test that the entity function handles exceptions gracefully."""
@@ -690,9 +658,10 @@ class TestErrorHandling:
)
# Even on error, message info should be preserved
assert result["message"] == "Test message"
assert result["thread_id"] == "conv-123"
assert result["status"] == "error"
assert isinstance(result, AgentRunResponse)
assert len(result.messages) == 1
content = result.messages[0].contents[0]
assert isinstance(content, ErrorContent)
class TestConversationHistory:
@@ -800,10 +769,8 @@ class TestRunRequestSupport:
result = await entity.run_agent(mock_context, request)
assert result["status"] == "success"
assert result["response"] == "Response"
assert result["message"] == "Test message"
assert result["thread_id"] == "conv-123"
assert isinstance(result, AgentRunResponse)
assert result.text == "Response"
async def test_run_agent_with_dict_request(self) -> None:
"""Test run_agent with a dictionary request."""
@@ -823,9 +790,8 @@ class TestRunRequestSupport:
result = await entity.run_agent(mock_context, request_dict)
assert result["status"] == "success"
assert result["message"] == "Test message"
assert result["thread_id"] == "conv-456"
assert isinstance(result, AgentRunResponse)
assert result.text == "Response"
async def test_run_agent_with_string_raises_without_correlation(self) -> None:
"""Test that run_agent rejects legacy string input without correlation ID."""
@@ -879,10 +845,9 @@ class TestRunRequestSupport:
result = await entity.run_agent(mock_context, request)
assert result["status"] == "success"
# Should have structured_response
if "structured_response" in result:
assert result["structured_response"]["answer"] == 42
assert isinstance(result, AgentRunResponse)
assert result.text == '{"answer": 42}'
assert result.value is None
async def test_run_agent_disable_tool_calls(self) -> None:
"""Test run_agent with tool calls disabled."""
@@ -898,7 +863,7 @@ class TestRunRequestSupport:
result = await entity.run_agent(mock_context, request)
assert result["status"] == "success"
assert isinstance(result, AgentRunResponse)
# Agent should have been called (tool disabling is framework-dependent)
mock_agent.run.assert_called_once()
@@ -925,8 +890,24 @@ class TestRunRequestSupport:
# Verify result was set
assert mock_context.set_result.called
result = mock_context.set_result.call_args[0][0]
assert result["status"] == "success"
assert result["message"] == "Test message"
assert isinstance(result, dict)
# Check if messages are present
assert "messages" in result
assert len(result["messages"]) > 0
message = result["messages"][0]
# Check for text in various possible locations
text_found = False
if "text" in message and message["text"] == "Response":
text_found = True
elif "contents" in message:
for content in message["contents"]:
if isinstance(content, dict) and content.get("text") == "Response":
text_found = True
break
assert text_found, f"Response text not found in message: {message}"
if __name__ == "__main__":