diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 23902b77c9..7b012ccf23 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **agent-framework-azurefunctions**: Durable Agents: platforms should use consistent entity method names (#2234) + ## [1.0.0b251216] - 2025-12-16 ### Added diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py index 7d8ebe0264..74462c8441 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_app.py @@ -414,7 +414,7 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien request_response_format, ) logger.debug("Signalling entity %s with request: %s", entity_instance_id, run_request) - await client.signal_entity(entity_instance_id, "run_agent", run_request) + await client.signal_entity(entity_instance_id, "run", run_request) logger.debug(f"[HTTP Trigger] Signal sent to entity {session_id}") @@ -495,7 +495,8 @@ def entity_function(context: df.DurableEntityContext) -> None: """Durable entity that manages agent execution and conversation state. Operations: - - run_agent: Execute the agent with a message + - run: Execute the agent with a message + - run_agent: (Deprecated) Execute the agent with a message - reset: Clear conversation history """ entity_handler = create_agent_entity(agent, callback) @@ -637,7 +638,7 @@ async def _handle_mcp_tool_invocation( logger.info("[MCP Tool] Invoking agent '%s' with query: %s", agent_name, query_preview) # Signal entity to run agent - await client.signal_entity(entity_instance_id, "run_agent", run_request) + await client.signal_entity(entity_instance_id, "run", run_request) # Poll for response (similar to HTTP handler) try: diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_entities.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_entities.py index 45872ce1a1..2cc86c1b65 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_entities.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_entities.py @@ -46,7 +46,8 @@ class AgentEntity: - Handles tool execution Operations: - - run_agent: Execute the agent with a message + - run: Execute the agent with a message + - run_agent: (Deprecated) Execute the agent with a message - reset: Clear conversation history Attributes: @@ -94,6 +95,22 @@ async def run_agent( self, context: df.DurableEntityContext, request: RunRequest | dict[str, Any] | str, + ) -> AgentRunResponse: + """(Deprecated) Execute the agent with a message directly in the entity. + + Args: + context: Entity context + request: RunRequest object, dict, or string message (for backward compatibility) + + Returns: + AgentRunResponse enriched with execution metadata. + """ + return await self.run(context, request) + + async def run( + self, + context: df.DurableEntityContext, + request: RunRequest | dict[str, Any] | str, ) -> AgentRunResponse: """Execute the agent with a message directly in the entity. @@ -124,7 +141,7 @@ async def run_agent( state_request = DurableAgentStateRequest.from_run_request(run_request) self.state.data.conversation_history.append(state_request) - logger.debug(f"[AgentEntity.run_agent] Received Message: {state_request}") + logger.debug(f"[AgentEntity.run] Received Message: {state_request}") try: # Build messages from conversation history, excluding error responses @@ -150,7 +167,7 @@ async def run_agent( ) logger.debug( - "[AgentEntity.run_agent] Agent invocation completed - response type: %s", + "[AgentEntity.run] Agent invocation completed - response type: %s", type(agent_run_response).__name__, ) @@ -167,12 +184,12 @@ async def run_agent( state_response = DurableAgentStateResponse.from_run_response(correlation_id, agent_run_response) self.state.data.conversation_history.append(state_response) - logger.debug("[AgentEntity.run_agent] AgentRunResponse stored in conversation history") + logger.debug("[AgentEntity.run] AgentRunResponse stored in conversation history") return agent_run_response except Exception as exc: - logger.exception("[AgentEntity.run_agent] Agent execution failed.") + logger.exception("[AgentEntity.run] Agent execution failed.") # Create error message error_message = ChatMessage( @@ -367,7 +384,7 @@ async def _entity_coroutine(context: df.DurableEntityContext) -> None: operation = context.operation_name - if operation == "run_agent": + if operation == "run" or operation == "run_agent": input_data: Any = context.get_input() request: str | dict[str, Any] @@ -377,7 +394,7 @@ async def _entity_coroutine(context: df.DurableEntityContext) -> None: # Fall back to treating input as message string request = "" if input_data is None else str(cast(object, input_data)) - result = await entity.run_agent(context, request) + result = await entity.run(context, request) context.set_result(result.to_dict()) elif operation == "reset": diff --git a/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py b/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py index d5adc68d74..4cef22e023 100644 --- a/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py +++ b/python/packages/azurefunctions/agent_framework_azurefunctions/_orchestration.py @@ -285,7 +285,7 @@ def my_orchestration(context): logger.debug("[DurableAIAgent] Calling entity %s with message: %s", entity_id, message_str[:100]) # Call the entity to get the underlying task - entity_task = self.context.call_entity(entity_id, "run_agent", run_request.to_dict()) + entity_task = self.context.call_entity(entity_id, "run", run_request.to_dict()) # Wrap it in an AgentTask that will convert the result to AgentRunResponse agent_task = AgentTask( diff --git a/python/packages/azurefunctions/tests/integration_tests/README.md b/python/packages/azurefunctions/tests/integration_tests/README.md index d9ecb86234..a7f9fadc44 100644 --- a/python/packages/azurefunctions/tests/integration_tests/README.md +++ b/python/packages/azurefunctions/tests/integration_tests/README.md @@ -29,7 +29,7 @@ docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azu **Durable Task Scheduler:** ```bash -docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest +docker run -d -p 8080:8080 -p 8082:8082 -e DTS_USE_DYNAMIC_TASK_HUBS=true mcr.microsoft.com/dts/dts-emulator:latest ``` ## Running Tests diff --git a/python/packages/azurefunctions/tests/test_app.py b/python/packages/azurefunctions/tests/test_app.py index 817a81e856..6937b3e0f5 100644 --- a/python/packages/azurefunctions/tests/test_app.py +++ b/python/packages/azurefunctions/tests/test_app.py @@ -338,7 +338,7 @@ async def test_entity_run_agent_operation(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Test message", "thread_id": "test-conv-123", "correlationId": "corr-app-entity-1"}, ) @@ -358,7 +358,7 @@ async def test_entity_stores_conversation_history(self) -> None: mock_context = Mock() # Send first message - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-app-entity-2"} ) @@ -367,7 +367,7 @@ async def test_entity_stores_conversation_history(self) -> None: assert len(history) == 1 # Just the user message # Send second message - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-2", "correlationId": "corr-app-entity-2b"} ) @@ -398,12 +398,12 @@ async def test_entity_increments_message_count(self) -> None: assert len(entity.state.data.conversation_history) == 0 - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-app-entity-3a"} ) assert len(entity.state.data.conversation_history) == 2 - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-app-entity-3b"} ) assert len(entity.state.data.conversation_history) == 4 @@ -433,8 +433,36 @@ def test_create_agent_entity_returns_function(self) -> None: assert callable(entity_function) + def test_entity_function_handles_run_operation(self) -> None: + """Test that the entity function handles the run operation.""" + mock_agent = Mock() + mock_agent.run = AsyncMock( + return_value=AgentRunResponse(messages=[ChatMessage(role="assistant", text="Response")]) + ) + + entity_function = create_agent_entity(mock_agent) + + # Mock context + mock_context = Mock() + mock_context.operation_name = "run" + mock_context.get_input.return_value = { + "message": "Test message", + "thread_id": "conv-123", + "correlationId": "corr-app-factory-1", + } + mock_context.get_state.return_value = None + + # Execute entity function + entity_function(mock_context) + + # Verify result was set + assert mock_context.set_result.called + assert mock_context.set_state.called + result_call = mock_context.set_result.call_args[0][0] + assert "error" not in result_call + def test_entity_function_handles_run_agent_operation(self) -> None: - """Test that the entity function handles the run_agent operation.""" + """Test that the entity function handles the deprecated run_agent operation for backward compatibility.""" mock_agent = Mock() mock_agent.run = AsyncMock( return_value=AgentRunResponse(messages=[ChatMessage(role="assistant", text="Response")]) @@ -458,6 +486,8 @@ def test_entity_function_handles_run_agent_operation(self) -> None: # Verify result was set assert mock_context.set_result.called assert mock_context.set_state.called + result_call = mock_context.set_result.call_args[0][0] + assert "error" not in result_call def test_entity_function_handles_reset_operation(self) -> None: """Test that the entity function handles the reset operation.""" @@ -585,7 +615,7 @@ async def test_entity_handles_agent_error(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Test message", "thread_id": "conv-1", "correlationId": "corr-app-error-1"} ) @@ -605,7 +635,7 @@ def test_entity_function_handles_exception(self) -> None: entity_function = create_agent_entity(mock_agent) mock_context = Mock() - mock_context.operation_name = "run_agent" + mock_context.operation_name = "run" mock_context.get_input.side_effect = Exception("Input error") mock_context.get_state.return_value = None diff --git a/python/packages/azurefunctions/tests/test_entities.py b/python/packages/azurefunctions/tests/test_entities.py index 66f39861a1..dcea75aa1c 100644 --- a/python/packages/azurefunctions/tests/test_entities.py +++ b/python/packages/azurefunctions/tests/test_entities.py @@ -108,6 +108,33 @@ def test_init_with_different_agent_types(self) -> None: class TestAgentEntityRunAgent: """Test suite for the run_agent operation.""" + async def test_run_executes_agent(self) -> None: + """Test that run executes the agent.""" + mock_agent = Mock() + mock_response = _agent_response("Test response") + mock_agent.run = AsyncMock(return_value=mock_response) + + entity = AgentEntity(mock_agent) + mock_context = Mock() + + result = await entity.run( + mock_context, {"message": "Test message", "thread_id": "conv-123", "correlationId": "corr-entity-1"} + ) + + # Verify agent.run was called + mock_agent.run.assert_called_once() + _, kwargs = mock_agent.run.call_args + sent_messages: list[Any] = kwargs.get("messages") + assert len(sent_messages) == 1 + sent_message = sent_messages[0] + assert isinstance(sent_message, ChatMessage) + assert getattr(sent_message, "text", None) == "Test message" + assert getattr(sent_message.role, "value", sent_message.role) == "user" + + # Verify result + assert isinstance(result, AgentRunResponse) + assert result.text == "Test response" + async def test_run_agent_executes_agent(self) -> None: """Test that run_agent executes the agent.""" mock_agent = Mock() @@ -156,7 +183,7 @@ async def update_generator() -> AsyncIterator[AgentRunResponseUpdate]: entity = AgentEntity(mock_agent, callback=callback) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, { "message": "Tell me something", @@ -203,7 +230,7 @@ async def test_run_agent_final_callback_without_streaming(self) -> None: entity = AgentEntity(mock_agent, callback=callback) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, { "message": "Hi", @@ -235,7 +262,7 @@ async def test_run_agent_updates_conversation_history(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - await entity.run_agent( + await entity.run( mock_context, {"message": "User message", "thread_id": "conv-1", "correlationId": "corr-entity-2"} ) @@ -263,17 +290,17 @@ async def test_run_agent_increments_message_count(self) -> None: assert len(entity.state.data.conversation_history) == 0 - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-entity-3a"} ) assert len(entity.state.data.conversation_history) == 2 - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-entity-3b"} ) assert len(entity.state.data.conversation_history) == 4 - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlationId": "corr-entity-3c"} ) assert len(entity.state.data.conversation_history) == 6 @@ -287,9 +314,7 @@ async def test_run_agent_with_none_thread_id(self) -> None: mock_context = Mock() with pytest.raises(ValueError, match="thread_id"): - await entity.run_agent( - mock_context, {"message": "Message", "thread_id": None, "correlationId": "corr-entity-5"} - ) + await entity.run(mock_context, {"message": "Message", "thread_id": None, "correlationId": "corr-entity-5"}) async def test_run_agent_multiple_conversations(self) -> None: """Test that run_agent maintains history across multiple messages.""" @@ -300,13 +325,13 @@ async def test_run_agent_multiple_conversations(self) -> None: mock_context = Mock() # Send multiple messages - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-entity-8a"} ) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-entity-8b"} ) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlationId": "corr-entity-8c"} ) @@ -374,10 +399,10 @@ async def test_reset_after_conversation(self) -> None: mock_context = Mock() # Have a conversation - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-entity-10a"} ) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-entity-10b"} ) @@ -413,7 +438,7 @@ def test_entity_function_handles_run_agent(self) -> None: # Mock context mock_context = Mock() - mock_context.operation_name = "run_agent" + mock_context.operation_name = "run" mock_context.get_input.return_value = { "message": "Test message", "thread_id": "conv-123", @@ -576,7 +601,7 @@ async def test_run_agent_handles_agent_exception(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-1"} ) @@ -595,7 +620,7 @@ async def test_run_agent_handles_value_error(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-2"} ) @@ -614,7 +639,7 @@ async def test_run_agent_handles_timeout_error(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-error-3"} ) @@ -631,7 +656,7 @@ def test_entity_function_handles_exception_in_operation(self) -> None: entity_function = create_agent_entity(mock_agent) mock_context = Mock() - mock_context.operation_name = "run_agent" + mock_context.operation_name = "run" mock_context.get_input.side_effect = Exception("Input error") mock_context.get_state.return_value = None @@ -651,7 +676,7 @@ async def test_run_agent_preserves_message_on_error(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - result = await entity.run_agent( + result = await entity.run( mock_context, {"message": "Test message", "thread_id": "conv-123", "correlationId": "corr-entity-error-4"}, ) @@ -674,7 +699,7 @@ async def test_conversation_history_has_timestamps(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - await entity.run_agent( + await entity.run( mock_context, {"message": "Message", "thread_id": "conv-1", "correlationId": "corr-entity-history-1"} ) @@ -694,19 +719,19 @@ async def test_conversation_history_ordering(self) -> None: # Send multiple messages with different responses mock_agent.run = AsyncMock(return_value=_agent_response("Response 1")) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-entity-history-2a"}, ) mock_agent.run = AsyncMock(return_value=_agent_response("Response 2")) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-entity-history-2b"}, ) mock_agent.run = AsyncMock(return_value=_agent_response("Response 3")) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 3", "thread_id": "conv-1", "correlationId": "corr-entity-history-2c"}, ) @@ -729,11 +754,11 @@ async def test_conversation_history_role_alternation(self) -> None: entity = AgentEntity(mock_agent) mock_context = Mock() - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 1", "thread_id": "conv-1", "correlationId": "corr-entity-history-3a"}, ) - await entity.run_agent( + await entity.run( mock_context, {"message": "Message 2", "thread_id": "conv-1", "correlationId": "corr-entity-history-3b"}, ) @@ -766,7 +791,7 @@ async def test_run_agent_with_run_request_object(self) -> None: correlation_id="corr-runreq-1", ) - result = await entity.run_agent(mock_context, request) + result = await entity.run(mock_context, request) assert isinstance(result, AgentRunResponse) assert result.text == "Response" @@ -787,7 +812,7 @@ async def test_run_agent_with_dict_request(self) -> None: "correlationId": "corr-runreq-2", } - result = await entity.run_agent(mock_context, request_dict) + result = await entity.run(mock_context, request_dict) assert isinstance(result, AgentRunResponse) assert result.text == "Response" @@ -801,7 +826,7 @@ async def test_run_agent_with_string_raises_without_correlation(self) -> None: mock_context = Mock() with pytest.raises(ValueError): - await entity.run_agent(mock_context, "Simple message") + await entity.run(mock_context, "Simple message") async def test_run_agent_stores_role_in_history(self) -> None: """Test that run_agent stores the role in conversation history.""" @@ -819,7 +844,7 @@ async def test_run_agent_stores_role_in_history(self) -> None: correlation_id="corr-runreq-3", ) - await entity.run_agent(mock_context, request) + await entity.run(mock_context, request) # Check that system role was stored history = entity.state.data.conversation_history @@ -842,7 +867,7 @@ async def test_run_agent_with_response_format(self) -> None: correlation_id="corr-runreq-4", ) - result = await entity.run_agent(mock_context, request) + result = await entity.run(mock_context, request) assert isinstance(result, AgentRunResponse) assert result.text == '{"answer": 42}' @@ -860,7 +885,7 @@ async def test_run_agent_disable_tool_calls(self) -> None: message="Test", thread_id="conv-runreq-5", enable_tool_calls=False, correlation_id="corr-runreq-5" ) - result = await entity.run_agent(mock_context, request) + result = await entity.run(mock_context, request) assert isinstance(result, AgentRunResponse) # Agent should have been called (tool disabling is framework-dependent) @@ -874,7 +899,7 @@ async def test_entity_function_with_run_request_dict(self) -> None: entity_function = create_agent_entity(mock_agent) mock_context = Mock() - mock_context.operation_name = "run_agent" + mock_context.operation_name = "run" mock_context.get_input.return_value = { "message": "Test message", "thread_id": "conv-789", diff --git a/python/packages/azurefunctions/tests/test_orchestration.py b/python/packages/azurefunctions/tests/test_orchestration.py index 0f845d4105..b0dd313b0b 100644 --- a/python/packages/azurefunctions/tests/test_orchestration.py +++ b/python/packages/azurefunctions/tests/test_orchestration.py @@ -295,7 +295,7 @@ def test_run_creates_entity_call(self) -> None: call_args = mock_context.call_entity.call_args entity_id, operation, request = call_args[0] - assert operation == "run_agent" + assert operation == "run" assert request["message"] == "Test message" assert request["enable_tool_calls"] is True assert "correlationId" in request