mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add Durabletask samples and minor fixes (#3157)
* Add samples and minor fixes * Add redis sample and wait-for-completion * Add wait-for-completion support * ADd missing docs
This commit is contained in:
committed by
GitHub
Unverified
parent
1e36ba33c4
commit
3df916064c
@@ -17,6 +17,7 @@ from agent_framework_durabletask import (
|
||||
load_agent_response,
|
||||
)
|
||||
from azure.durable_functions.models import TaskBase
|
||||
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
||||
from azure.durable_functions.models.Task import CompoundTask, TaskState
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -42,6 +43,25 @@ else:
|
||||
_TypedCompoundTask = CompoundTask
|
||||
|
||||
|
||||
class PreCompletedTask(TaskBase):
|
||||
"""A simple task that is already completed with a result.
|
||||
|
||||
Used for fire-and-forget mode where we want to return immediately
|
||||
with an acceptance response without waiting for entity processing.
|
||||
"""
|
||||
|
||||
def __init__(self, result: Any):
|
||||
"""Initialize with a completed result.
|
||||
|
||||
Args:
|
||||
result: The result value for this completed task
|
||||
"""
|
||||
# Initialize with a NoOp action since we don't need actual orchestration actions
|
||||
super().__init__(-1, NoOpAction())
|
||||
# Immediately mark as completed with the result
|
||||
self.set_value(is_error=False, value=result)
|
||||
|
||||
|
||||
class AgentTask(_TypedCompoundTask):
|
||||
"""A custom Task that wraps entity calls and provides typed AgentRunResponse results.
|
||||
|
||||
@@ -62,10 +82,13 @@ class AgentTask(_TypedCompoundTask):
|
||||
response_format: Optional Pydantic model for response parsing
|
||||
correlation_id: Correlation ID for logging
|
||||
"""
|
||||
super().__init__([entity_task])
|
||||
# Set instance variables BEFORE calling super().__init__
|
||||
# because super().__init__ may trigger try_set_value for pre-completed tasks
|
||||
self._response_format = response_format
|
||||
self._correlation_id = correlation_id
|
||||
|
||||
super().__init__([entity_task])
|
||||
|
||||
# Override action_repr to expose the inner task's action directly
|
||||
# This ensures compatibility with ReplaySchema V3 which expects Action objects.
|
||||
self.action_repr = entity_task.action_repr
|
||||
@@ -130,16 +153,27 @@ class AzureFunctionsAgentExecutor(DurableAgentExecutor[AgentTask]):
|
||||
message: str,
|
||||
response_format: type[BaseModel] | None,
|
||||
enable_tool_calls: bool,
|
||||
wait_for_response: bool = True,
|
||||
) -> RunRequest:
|
||||
"""Get the current run request from the orchestration context.
|
||||
|
||||
Args:
|
||||
message: The message to send to the agent
|
||||
response_format: Optional Pydantic model for response parsing
|
||||
enable_tool_calls: Whether to enable tool calls
|
||||
wait_for_response: Must be True for orchestration contexts
|
||||
|
||||
Returns:
|
||||
RunRequest: The current run request
|
||||
|
||||
Raises:
|
||||
ValueError: If wait_for_response=False (not supported in orchestrations)
|
||||
"""
|
||||
request = super().get_run_request(
|
||||
message,
|
||||
response_format,
|
||||
enable_tool_calls,
|
||||
wait_for_response,
|
||||
)
|
||||
request.orchestration_id = self.context.instance_id
|
||||
return request
|
||||
@@ -166,7 +200,24 @@ class AzureFunctionsAgentExecutor(DurableAgentExecutor[AgentTask]):
|
||||
session_id,
|
||||
)
|
||||
|
||||
entity_task = self.context.call_entity(entity_id, "run", run_request.to_dict())
|
||||
# Branch based on wait_for_response
|
||||
if not run_request.wait_for_response:
|
||||
# Fire-and-forget mode: signal entity and return pre-completed task
|
||||
logger.debug(
|
||||
"[AzureFunctionsAgentExecutor] Fire-and-forget mode: signaling entity (correlation: %s)",
|
||||
run_request.correlation_id,
|
||||
)
|
||||
self.context.signal_entity(entity_id, "run", run_request.to_dict())
|
||||
|
||||
# Create acceptance response using base class helper
|
||||
acceptance_response = self._create_acceptance_response(run_request.correlation_id)
|
||||
|
||||
# Create a pre-completed task with the acceptance response
|
||||
entity_task = PreCompletedTask(acceptance_response)
|
||||
else:
|
||||
# Blocking mode: call entity and wait for response
|
||||
entity_task = self.context.call_entity(entity_id, "run", run_request.to_dict())
|
||||
|
||||
return AgentTask(
|
||||
entity_task=entity_task,
|
||||
response_format=run_request.response_format,
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from agent_framework import AgentRunResponse, ChatMessage
|
||||
from agent_framework import AgentRunResponse, ChatMessage, Role
|
||||
from agent_framework_durabletask import DurableAIAgent
|
||||
from azure.durable_functions.models.Task import TaskBase, TaskState
|
||||
|
||||
@@ -206,6 +206,81 @@ class TestAgentFunctionAppGetAgent:
|
||||
app.get_agent(Mock(), "MissingAgent")
|
||||
|
||||
|
||||
class TestAzureFunctionsFireAndForget:
|
||||
"""Test fire-and-forget mode for AzureFunctionsAgentExecutor."""
|
||||
|
||||
def test_fire_and_forget_calls_signal_entity(self, executor_with_uuid: tuple[Any, Mock, str]) -> None:
|
||||
"""Verify wait_for_response=False calls signal_entity instead of call_entity."""
|
||||
executor, context, _ = executor_with_uuid
|
||||
context.signal_entity = Mock()
|
||||
context.call_entity = Mock(return_value=_create_entity_task())
|
||||
|
||||
agent = DurableAIAgent(executor, "TestAgent")
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# Run with wait_for_response=False
|
||||
result = agent.run("Test message", thread=thread, wait_for_response=False)
|
||||
|
||||
# Verify signal_entity was called and call_entity was not
|
||||
assert context.signal_entity.call_count == 1
|
||||
assert context.call_entity.call_count == 0
|
||||
|
||||
# Should still return an AgentTask
|
||||
assert isinstance(result, AgentTask)
|
||||
|
||||
def test_fire_and_forget_returns_completed_task(self, executor_with_uuid: tuple[Any, Mock, str]) -> None:
|
||||
"""Verify wait_for_response=False returns pre-completed AgentTask."""
|
||||
executor, context, _ = executor_with_uuid
|
||||
context.signal_entity = Mock()
|
||||
|
||||
agent = DurableAIAgent(executor, "TestAgent")
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
result = agent.run("Test message", thread=thread, wait_for_response=False)
|
||||
|
||||
# Task should be immediately complete
|
||||
assert isinstance(result, AgentTask)
|
||||
assert result.is_completed
|
||||
|
||||
def test_fire_and_forget_returns_acceptance_response(self, executor_with_uuid: tuple[Any, Mock, str]) -> None:
|
||||
"""Verify wait_for_response=False returns acceptance response."""
|
||||
executor, context, _ = executor_with_uuid
|
||||
context.signal_entity = Mock()
|
||||
|
||||
agent = DurableAIAgent(executor, "TestAgent")
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
result = agent.run("Test message", thread=thread, wait_for_response=False)
|
||||
|
||||
# Get the result
|
||||
response = result.result
|
||||
assert isinstance(response, AgentRunResponse)
|
||||
assert len(response.messages) == 1
|
||||
assert response.messages[0].role == Role.SYSTEM
|
||||
# Check message contains key information
|
||||
message_text = response.messages[0].text
|
||||
assert "accepted" in message_text.lower()
|
||||
assert "background" in message_text.lower()
|
||||
|
||||
def test_blocking_mode_still_works(self, executor_with_uuid: tuple[Any, Mock, str]) -> None:
|
||||
"""Verify wait_for_response=True uses call_entity as before."""
|
||||
executor, context, _ = executor_with_uuid
|
||||
context.signal_entity = Mock()
|
||||
context.call_entity = Mock(return_value=_create_entity_task())
|
||||
|
||||
agent = DurableAIAgent(executor, "TestAgent")
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
result = agent.run("Test message", thread=thread, wait_for_response=True)
|
||||
|
||||
# Verify call_entity was called and signal_entity was not
|
||||
assert context.call_entity.call_count == 1
|
||||
assert context.signal_entity.call_count == 0
|
||||
|
||||
# Should return an AgentTask
|
||||
assert isinstance(result, AgentTask)
|
||||
|
||||
|
||||
class TestOrchestrationIntegration:
|
||||
"""Integration tests for orchestration scenarios."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user