Skip to content

Conversation

@larohra
Copy link
Contributor

@larohra larohra commented Nov 17, 2025

Motivation and Context

Description

The changes bring parity with Dotnet experience and also with agent-framework in general.

This introduces a breaking change where now we need to use yield from when calling the DurableAIAgent (vs just yield before) -

initial_raw = yield from writer.run(
        messages=f"Write a short article about '{payload.topic}'.",
        thread=writer_thread,
        response_format=GeneratedContent,
    )

Additionally, the responses are now of type AgentRunResponse and the structured response is stored in AgentRunResponse.value (as opposed to response["structured_response"] before).

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

@larohra larohra requested a review from a team as a code owner November 17, 2025 22:21
Copilot AI review requested due to automatic review settings November 17, 2025 22:21
@larohra larohra added the azure-functions Issues and PRs related to Azure Functions label Nov 17, 2025
@github-actions github-actions bot changed the title [Breaking] Python: Respond with AgentRunResponse with serialized structured output Python: [Breaking] Python: Respond with AgentRunResponse with serialized structured output Nov 17, 2025
@markwallace-microsoft
Copy link
Member

markwallace-microsoft commented Nov 17, 2025

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/azurefunctions/agent_framework_azurefunctions
   _entities.py1792188%120, 160–161, 212, 220, 225–226, 253–254, 263, 270, 281, 287–288, 307–308, 378, 410–412, 414
   _models.py1501292%204, 207, 210, 222, 225, 244, 247, 252, 257, 274, 329, 348
   _orchestration.py871879%168, 173, 175–176, 178, 193, 202–203, 205, 261, 266–273
TOTAL15449236084% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
2220 130 💤 0 ❌ 0 🔥 53.413s ⏱️

Copilot finished reviewing on behalf of larohra November 17, 2025 22:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a breaking change to make DurableAIAgent.run() return AgentRunResponse instead of raw dictionaries, aligning with the agent-framework protocols and bringing parity with the .NET experience. The key changes include:

  • Modified DurableAIAgent.run() to use generator protocol (yield from) and return typed AgentRunResponse
  • Removed the AgentResponse wrapper class in favor of using AgentRunResponse directly with metadata in additional_properties
  • Deserialization of structured output (response.value) now happens in the orchestration layer via _ensure_response_format()
  • Updated all samples and tests to use yield from syntax and access structured responses via .value property

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py Added generator-based run() method returning AgentRunResponse, with helper methods _load_agent_response() and _ensure_response_format() for response handling and structured output parsing
python/packages/azurefunctions/agent_framework_azurefunctions/_entities.py Modified run_agent() to return AgentRunResponse directly, removed JSON parsing logic (now handled at orchestration layer), updated metadata handling via additional_properties
python/packages/azurefunctions/agent_framework_azurefunctions/_models.py Removed AgentResponse dataclass as it's replaced by AgentRunResponse
python/samples/getting_started/azure_functions/04_single_agent_orchestration_chaining/function_app.py Updated to use yield from and access response via .text property
python/samples/getting_started/azure_functions/05_multi_agent_orchestration_concurrency/function_app.py Updated to manually advance generators for concurrent execution, accessing results via .text property
python/samples/getting_started/azure_functions/06_multi_agent_orchestration_conditionals/function_app.py Updated to use yield from and access structured responses via .value property, removed _coerce_structured() helper
python/samples/getting_started/azure_functions/07_single_agent_orchestration_hitl/function_app.py Updated to use yield from and access structured responses via .value property with validation, removed _coerce_generated_content() helper
python/packages/azurefunctions/tests/test_orchestration.py Added tests for _load_agent_response() helper method covering instance, serialized, and None cases
python/packages/azurefunctions/tests/test_models.py Removed all tests for the deleted AgentResponse class
python/packages/azurefunctions/tests/test_entities.py Updated assertions to check AgentRunResponse type and access metadata via additional_properties
python/packages/azurefunctions/tests/test_app.py Updated assertions to check AgentRunResponse type and access metadata via additional_properties

@larohra larohra requested a review from Copilot November 20, 2025 18:27
Copilot finished reviewing on behalf of larohra November 20, 2025 18:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Copy link
Member

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the feedback below, can we open an issue to track updating the snippets lab (the one we used for Ignite) to account for these breaking changes?

role=Role.ASSISTANT, contents=[ErrorContent(message=str(exc), error_code=type(exc).__name__)]
)
error_message = ChatMessage(
role=Role.ASSISTANT, contents=[ErrorContent(message=str(exc), error_code=type(exc).__name__)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you're creating a custom error response from the entity when something unexpected happens, but I don't believe we're doing this in .NET. I'm also not sure if this is the right thing for us to do since it hides the fact that an error occurred from Functions telemetry. Regardless of what design we think is best, we should do the same thing in both languages. Can you open an issue to review the implementation and track consistent error handling for agent invocations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the experiences should be consistent.
Created an issue to track this separately - #2434

chemist_result = None

try:
physicist_gen.send(task_results[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen this direct access of generators and send in durable Python apps before, and I don't think it's intended that we make this a normal part of the programming model. Why is this needed, and can it be replaced with something more conventional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a comment at the top of this file, but this is basically only for the advance cases where the customers want true parallelism with invoking multi-agents with generators.

 """Fan out to two domain-specific agents and aggregate their responses.

    Note: The generator protocol lets us extract the Durable Task objects so we
    can pass them to task_all for true parallelism. If you only use `yield
    from` on each run call, the agents execute sequentially.
    For sequential execution, you can simply use:
        physicist_result = yield from physicist.run(messages=prompt, thread=physicist_thread)
        chemist_result = yield from chemist.run(messages=prompt, thread=chemist_thread)
    """

The conventional method is defined throughout the rest of the samples with using the yield from model. The only catch here is that that convention is more synchronous -

email_result_raw = yield from email_agent.run(
        messages=email_prompt,
        thread=email_thread,
        response_format=EmailResponse,
    )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand what the sample is doing, but I don't understand why we can't use normal generator syntax. This is not something durable customers need to do in Python today: https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-overview?tabs=in-process%2Cnodejs-v3%2Cv1-model&pivots=python#fan-in-out

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is in the response type of the types. The context.task_all expects a list[TaskBase] but the agent.run responds with a Generator[.., .., AgentRunResponse] due to which it can't resolve properly. The main reason why this is happening is because we do some post-processing after we get the result back from the entity after we signal it using context.signal_entity() and return an object of AgentRunResponse. Earlier, we were just returning the same task that we got when executing teh signal_entity command without any post-processing.

result = yield self.context.call_entity(entity_id, "run_agent", run_request.to_dict())

logger.debug(
    "[DurableAIAgent] Entity call completed for correlation_id %s",
    correlation_id,
)

response = self._load_agent_response(result)

if response_format is not None:
    self._ensure_response_format(response_format, correlation_id, response)

return response

@larohra
Copy link
Contributor Author

larohra commented Nov 24, 2025

In addition to the feedback below, can we open an issue to track updating the snippets lab (the one we used for Ignite) to account for these breaking changes?

Opened an issue #2435

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

azure-functions Issues and PRs related to Azure Functions python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants