mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
e032133748
* Add integration tests * Fix flaky test * Fix env viz * Fix tests and address feedback
206 lines
6.9 KiB
Python
206 lines
6.9 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Test utilities for durabletask integration tests."""
|
|
|
|
import json
|
|
import time
|
|
from typing import Any
|
|
|
|
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
|
|
from durabletask.client import OrchestrationStatus
|
|
|
|
from agent_framework_durabletask import DurableAIAgentClient
|
|
|
|
|
|
def create_dts_client(endpoint: str, taskhub: str) -> DurableTaskSchedulerClient:
|
|
"""
|
|
Create a DurableTaskSchedulerClient with common configuration.
|
|
|
|
Args:
|
|
endpoint: The DTS endpoint address
|
|
taskhub: The task hub name
|
|
|
|
Returns:
|
|
A configured DurableTaskSchedulerClient instance
|
|
"""
|
|
return DurableTaskSchedulerClient(
|
|
host_address=endpoint,
|
|
secure_channel=False,
|
|
taskhub=taskhub,
|
|
token_credential=None,
|
|
)
|
|
|
|
|
|
def create_agent_client(
|
|
endpoint: str,
|
|
taskhub: str,
|
|
max_poll_retries: int = 90,
|
|
) -> tuple[DurableTaskSchedulerClient, DurableAIAgentClient]:
|
|
"""
|
|
Create a DurableAIAgentClient with the underlying DTS client.
|
|
|
|
Args:
|
|
endpoint: The DTS endpoint address
|
|
taskhub: The task hub name
|
|
max_poll_retries: Max poll retries for the agent client
|
|
|
|
Returns:
|
|
A tuple of (DurableTaskSchedulerClient, DurableAIAgentClient)
|
|
"""
|
|
dts_client = create_dts_client(endpoint, taskhub)
|
|
agent_client = DurableAIAgentClient(dts_client, max_poll_retries=max_poll_retries)
|
|
return dts_client, agent_client
|
|
|
|
|
|
class OrchestrationHelper:
|
|
"""Helper class for orchestration-related test operations."""
|
|
|
|
def __init__(self, dts_client: DurableTaskSchedulerClient):
|
|
"""
|
|
Initialize the orchestration helper.
|
|
|
|
Args:
|
|
dts_client: The DurableTaskSchedulerClient instance to use
|
|
"""
|
|
self.client = dts_client
|
|
|
|
def wait_for_orchestration(
|
|
self,
|
|
instance_id: str,
|
|
timeout: float = 60.0,
|
|
) -> Any:
|
|
"""
|
|
Wait for an orchestration to complete.
|
|
|
|
Args:
|
|
instance_id: The orchestration instance ID
|
|
timeout: Maximum time to wait in seconds
|
|
|
|
Returns:
|
|
The final OrchestrationMetadata
|
|
|
|
Raises:
|
|
TimeoutError: If the orchestration doesn't complete within timeout
|
|
RuntimeError: If the orchestration fails
|
|
"""
|
|
# Use the built-in wait_for_orchestration_completion method
|
|
metadata = self.client.wait_for_orchestration_completion(
|
|
instance_id=instance_id,
|
|
timeout=int(timeout),
|
|
)
|
|
|
|
if metadata is None:
|
|
raise TimeoutError(f"Orchestration {instance_id} did not complete within {timeout} seconds")
|
|
|
|
# Check if failed or terminated
|
|
if metadata.runtime_status == OrchestrationStatus.FAILED:
|
|
raise RuntimeError(f"Orchestration {instance_id} failed: {metadata.serialized_custom_status}")
|
|
if metadata.runtime_status == OrchestrationStatus.TERMINATED:
|
|
raise RuntimeError(f"Orchestration {instance_id} was terminated")
|
|
|
|
return metadata
|
|
|
|
def wait_for_orchestration_with_output(
|
|
self,
|
|
instance_id: str,
|
|
timeout: float = 60.0,
|
|
) -> tuple[Any, Any]:
|
|
"""
|
|
Wait for an orchestration to complete and return its output.
|
|
|
|
Args:
|
|
instance_id: The orchestration instance ID
|
|
timeout: Maximum time to wait in seconds
|
|
|
|
Returns:
|
|
A tuple of (OrchestrationMetadata, output)
|
|
|
|
Raises:
|
|
TimeoutError: If the orchestration doesn't complete within timeout
|
|
RuntimeError: If the orchestration fails
|
|
"""
|
|
metadata = self.wait_for_orchestration(instance_id, timeout)
|
|
|
|
# The output should be available in the metadata
|
|
return metadata, metadata.serialized_output
|
|
|
|
def get_orchestration_status(self, instance_id: str) -> Any | None:
|
|
"""
|
|
Get the current status of an orchestration.
|
|
|
|
Args:
|
|
instance_id: The orchestration instance ID
|
|
|
|
Returns:
|
|
The OrchestrationMetadata or None if not found
|
|
"""
|
|
try:
|
|
# Try to wait with a short timeout to get current status
|
|
return self.client.wait_for_orchestration_completion(
|
|
instance_id=instance_id,
|
|
timeout=1, # Very short timeout, just checking status
|
|
)
|
|
except Exception:
|
|
return None
|
|
|
|
def raise_event(
|
|
self,
|
|
instance_id: str,
|
|
event_name: str,
|
|
event_data: Any = None,
|
|
) -> None:
|
|
"""
|
|
Raise an external event to an orchestration.
|
|
|
|
Args:
|
|
instance_id: The orchestration instance ID
|
|
event_name: The name of the event
|
|
event_data: The event data payload
|
|
"""
|
|
self.client.raise_orchestration_event(instance_id, event_name, data=event_data)
|
|
|
|
def wait_for_notification(self, instance_id: str, timeout_seconds: int = 30) -> bool:
|
|
"""Wait for the orchestration to reach a notification point.
|
|
|
|
Polls the orchestration status until it appears to be waiting for approval.
|
|
|
|
Args:
|
|
instance_id: The orchestration instance ID
|
|
timeout_seconds: Maximum time to wait
|
|
|
|
Returns:
|
|
True if notification detected, False if timeout
|
|
"""
|
|
start_time = time.time()
|
|
while time.time() - start_time < timeout_seconds:
|
|
try:
|
|
metadata = self.client.get_orchestration_state(
|
|
instance_id=instance_id,
|
|
)
|
|
|
|
if metadata:
|
|
# Check if we're waiting for approval by examining custom status
|
|
if metadata.serialized_custom_status:
|
|
try:
|
|
custom_status = json.loads(metadata.serialized_custom_status)
|
|
# Handle both string and dict custom status
|
|
status_str = custom_status if isinstance(custom_status, str) else str(custom_status)
|
|
if status_str.lower().startswith("requesting human feedback"):
|
|
return True
|
|
except (json.JSONDecodeError, AttributeError):
|
|
# If it's not JSON, treat as plain string
|
|
if metadata.serialized_custom_status.lower().startswith("requesting human feedback"):
|
|
return True
|
|
|
|
# Check for terminal states
|
|
if metadata.runtime_status.name == "COMPLETED" or metadata.runtime_status.name == "FAILED":
|
|
return False
|
|
except Exception:
|
|
# Silently ignore transient errors during polling (e.g., network issues, service unavailable).
|
|
# The loop will retry until timeout, allowing the service to recover.
|
|
pass
|
|
|
|
time.sleep(1)
|
|
|
|
return False
|