mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Types API Review improvements (#3647)
* Replace Role and FinishReason classes with NewType + Literal
- Remove EnumLike metaclass from _types.py
- Replace Role class with NewType('Role', str) + RoleLiteral
- Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral
- Update all usages across codebase to use string literals
- Remove .value access patterns (direct string comparison now works)
- Add backward compatibility for legacy dict serialization format
- Update tests to reflect new string-based types
Addresses #3591, #3615
* Simplify ChatResponse and AgentResponse type hints (#3592)
- Remove overloads from ChatResponse.__init__
- Remove text parameter from ChatResponse.__init__
- Remove | dict[str, Any] from finish_reason and usage_details params
- Remove **kwargs from AgentResponse.__init__
- Both now accept ChatMessage | Sequence[ChatMessage] | None for messages
- Update docstrings and examples to reflect changes
- Fix tests that were using removed kwargs
- Fix Role type hint usage in ag-ui utils
* Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597)
- Remove text parameter from ChatResponseUpdate.__init__
- Remove text parameter from AgentResponseUpdate.__init__
- Remove **kwargs from both update classes
- Simplify contents parameter type to Sequence[Content] | None
- Update all usages to use contents=[Content.from_text(...)] pattern
- Fix imports in test files
- Update docstrings and examples
* Rename from_chat_response_updates to from_updates (#3593)
- ChatResponse.from_chat_response_updates → ChatResponse.from_updates
- ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator
- AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates
* Remove try_parse_value method from ChatResponse and AgentResponse (#3595)
- Remove try_parse_value method from ChatResponse
- Remove try_parse_value method from AgentResponse
- Remove try_parse_value calls from from_updates and from_update_generator methods
- Update samples to use try/except with response.value instead
- Update tests to use response.value pattern
- Users should now use response.value with try/except for safe parsing
* Add agent_id to AgentResponse and clarify author_name documentation (#3596)
- Add agent_id parameter to AgentResponse class
- Document that author_name is on ChatMessage objects, not responses
- Update ChatResponse docstring with author_name note
- Update AgentResponse docstring with author_name note
* Simplify ChatMessage.__init__ signature (#3618)
- Make contents a positional argument accepting Sequence[Content | str]
- Auto-convert strings in contents to TextContent
- Remove overloads, keep text kwarg for backward compatibility with serialization
- Update _parse_content_list to handle string items
- Update all usages across codebase to use new format: ChatMessage("role", ["text"])
* Allow Content as input on run and get_response
- Update prepare_messages and normalize_messages to accept Content
- Update type signatures in _agents.py and _clients.py
- Add tests for Content input handling
* Fix ChatMessage usage across packages and samples
Update all remaining ChatMessage(role=..., text=...) to use new
ChatMessage('role', ['text']) signature.
* Fix Role string usage and response format parsing
- Fix redis provider: remove .value access on string literals
- Fix durabletask ensure_response_format: set _response_format before accessing .value
* Fix ollama .value and ai_model_id issues, handle None in content list
- Fix ollama _chat_client: remove .value on string literals
- Fix ollama _chat_client: rename ai_model_id to model_id
- Fix _parse_content_list: skip None values gracefully
* Fix A2AAgent type signature to include Content
* Fix Role/FinishReason NewType dict annotations and improve test coverage to 95%
* Fix mypy errors for Role/FinishReason NewType usage
* Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py
* Fix Role NewType usage in durabletask _models.py
This commit is contained in:
committed by
GitHub
Unverified
parent
ef798629e5
commit
838a7fd61d
@@ -40,12 +40,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
dts_client = DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -53,7 +53,7 @@ def get_client(
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
)
|
||||
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
|
||||
|
||||
@@ -66,12 +66,12 @@ def run_client(agent_client: DurableAIAgentClient) -> None:
|
||||
# Get a reference to the Joker agent
|
||||
logger.debug("Getting reference to Joker agent...")
|
||||
joker = agent_client.get_agent("Joker")
|
||||
|
||||
|
||||
# Create a new thread for the conversation
|
||||
thread = joker.get_new_thread()
|
||||
logger.debug(f"Thread ID: {thread.session_id}")
|
||||
logger.info("Start chatting with the Joker agent! (Type 'exit' to quit)")
|
||||
|
||||
|
||||
# Interactive conversation loop
|
||||
while True:
|
||||
# Get user input
|
||||
@@ -80,33 +80,33 @@ def run_client(agent_client: DurableAIAgentClient) -> None:
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
logger.info("\nExiting...")
|
||||
break
|
||||
|
||||
|
||||
# Check for exit command
|
||||
if user_message.lower() == "exit":
|
||||
logger.info("Goodbye!")
|
||||
break
|
||||
|
||||
|
||||
# Skip empty messages
|
||||
if not user_message:
|
||||
continue
|
||||
|
||||
|
||||
# Send message to agent and get response
|
||||
try:
|
||||
response = joker.run(user_message, thread=thread)
|
||||
logger.info(f"Joker: {response.text} \n")
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting response: {e}")
|
||||
|
||||
|
||||
logger.info("Conversation completed.")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task Agent Client...")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
agent_client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
run_client(agent_client)
|
||||
except Exception as e:
|
||||
|
||||
@@ -15,40 +15,40 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
# Configure logging (must be after imports to override their basicConfig)
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task Agent Sample (Combined Worker + Client)...")
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
|
||||
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
agent_client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
try:
|
||||
# Run client interactions using helper function
|
||||
run_client(agent_client)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during agent interaction: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
@@ -52,12 +52,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -78,42 +78,42 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register the Joker agent
|
||||
logger.debug("Creating and registering Joker agent...")
|
||||
joker_agent = create_joker_agent()
|
||||
agent_worker.add_agent(joker_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agent: {joker_agent.name}")
|
||||
logger.debug(f" Entity name: dafx-{joker_agent.name}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Agent Worker...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.info("Worker is ready and listening for requests...")
|
||||
logger.info("Press Ctrl+C to stop.")
|
||||
logger.info("")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.debug("Worker stopped")
|
||||
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
dts_client = DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -54,7 +54,7 @@ def get_client(
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
)
|
||||
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
|
||||
|
||||
@@ -65,45 +65,45 @@ def run_client(agent_client: DurableAIAgentClient) -> None:
|
||||
agent_client: The DurableAIAgentClient instance
|
||||
"""
|
||||
logger.debug("Testing WeatherAgent")
|
||||
|
||||
|
||||
# Get reference to WeatherAgent
|
||||
weather_agent = agent_client.get_agent("WeatherAgent")
|
||||
weather_thread = weather_agent.get_new_thread()
|
||||
|
||||
|
||||
logger.debug(f"Created weather conversation thread: {weather_thread.session_id}")
|
||||
|
||||
|
||||
# Test WeatherAgent
|
||||
weather_message = "What is the weather in Seattle?"
|
||||
logger.info(f"User: {weather_message}")
|
||||
|
||||
|
||||
weather_response = weather_agent.run(weather_message, thread=weather_thread)
|
||||
logger.info(f"WeatherAgent: {weather_response.text} \n")
|
||||
|
||||
|
||||
logger.debug("Testing MathAgent")
|
||||
|
||||
|
||||
# Get reference to MathAgent
|
||||
math_agent = agent_client.get_agent("MathAgent")
|
||||
math_thread = math_agent.get_new_thread()
|
||||
|
||||
|
||||
logger.debug(f"Created math conversation thread: {math_thread.session_id}")
|
||||
|
||||
|
||||
# Test MathAgent
|
||||
math_message = "Calculate a 20% tip on a $50 bill"
|
||||
logger.info(f"User: {math_message}")
|
||||
|
||||
|
||||
math_response = math_agent.run(math_message, thread=math_thread)
|
||||
logger.info(f"MathAgent: {math_response.text} \n")
|
||||
|
||||
|
||||
logger.debug("Both agents completed successfully!")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Client...")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
agent_client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
run_client(agent_client)
|
||||
except Exception as e:
|
||||
|
||||
@@ -15,10 +15,9 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
# Configure logging
|
||||
@@ -29,26 +28,26 @@ logger = logging.getLogger(__name__)
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Sample (Combined Worker + Client)...")
|
||||
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
agent_client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
try:
|
||||
# Run client interactions using helper function
|
||||
run_client(agent_client)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during agent interaction: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
@@ -101,12 +101,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -127,43 +127,43 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register both agents
|
||||
logger.debug("Creating and registering agents...")
|
||||
weather_agent = create_weather_agent()
|
||||
math_agent = create_math_agent()
|
||||
|
||||
|
||||
agent_worker.add_agent(weather_agent)
|
||||
agent_worker.add_agent(math_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agents: {weather_agent.name}, {math_agent.name}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Worker...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.info("Worker is ready and listening for requests...")
|
||||
logger.info("Press Ctrl+C to stop. \n")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.info("Worker stopped")
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import redis.asyncio as aioredis
|
||||
from agent_framework.azure import DurableAIAgentClient
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
|
||||
|
||||
from redis_stream_response_handler import RedisStreamResponseHandler
|
||||
|
||||
# Configure logging
|
||||
@@ -71,12 +70,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
dts_client = DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -84,7 +83,7 @@ def get_client(
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
)
|
||||
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
|
||||
|
||||
@@ -100,33 +99,33 @@ async def stream_from_redis(thread_id: str, cursor: str | None = None) -> None:
|
||||
logger.debug(f"To manually check Redis, run: redis-cli XLEN {stream_key}")
|
||||
if cursor:
|
||||
logger.info(f"Resuming from cursor: {cursor}")
|
||||
|
||||
|
||||
async with await get_stream_handler() as stream_handler:
|
||||
logger.info(f"Stream handler created, starting to read...")
|
||||
logger.info("Stream handler created, starting to read...")
|
||||
try:
|
||||
chunk_count = 0
|
||||
async for chunk in stream_handler.read_stream(thread_id, cursor):
|
||||
chunk_count += 1
|
||||
logger.debug(f"Received chunk #{chunk_count}: error={chunk.error}, is_done={chunk.is_done}, text_len={len(chunk.text) if chunk.text else 0}")
|
||||
|
||||
|
||||
if chunk.error:
|
||||
logger.error(f"Stream error: {chunk.error}")
|
||||
break
|
||||
|
||||
|
||||
if chunk.is_done:
|
||||
print("\n✓ Response complete!", flush=True)
|
||||
logger.info(f"Stream completed after {chunk_count} chunks")
|
||||
break
|
||||
|
||||
|
||||
if chunk.text:
|
||||
# Print directly to console with flush for immediate display
|
||||
print(chunk.text, end='', flush=True)
|
||||
|
||||
print(chunk.text, end="", flush=True)
|
||||
|
||||
if chunk_count == 0:
|
||||
logger.warning("No chunks received from Redis stream!")
|
||||
logger.warning(f"Check Redis manually: redis-cli XLEN {stream_key}")
|
||||
logger.warning(f"View stream contents: redis-cli XREAD STREAMS {stream_key} 0")
|
||||
|
||||
|
||||
except Exception as ex:
|
||||
logger.error(f"Error reading from Redis: {ex}", exc_info=True)
|
||||
|
||||
@@ -140,47 +139,47 @@ def run_client(agent_client: DurableAIAgentClient) -> None:
|
||||
# Get a reference to the TravelPlanner agent
|
||||
logger.debug("Getting reference to TravelPlanner agent...")
|
||||
travel_planner = agent_client.get_agent("TravelPlanner")
|
||||
|
||||
|
||||
# Create a new thread for the conversation
|
||||
thread = travel_planner.get_new_thread()
|
||||
if not thread.session_id:
|
||||
logger.error("Failed to create a new thread with session ID!")
|
||||
return
|
||||
|
||||
|
||||
key = thread.session_id.key
|
||||
logger.info(f"Thread ID: {key}")
|
||||
|
||||
|
||||
# Get user input
|
||||
print("\nEnter your travel planning request:")
|
||||
user_message = input("> ").strip()
|
||||
|
||||
|
||||
if not user_message:
|
||||
logger.warning("No input provided. Using default message.")
|
||||
user_message = "Plan a 3-day trip to Tokyo with emphasis on culture and food"
|
||||
|
||||
|
||||
logger.info(f"\nYou: {user_message}\n")
|
||||
logger.info("TravelPlanner (streaming from Redis):")
|
||||
logger.info("-" * 80)
|
||||
|
||||
|
||||
# Start the agent run with wait_for_response=False for non-blocking execution
|
||||
# This signals the agent to start processing without waiting for completion
|
||||
# The agent will execute in the background and write chunks to Redis
|
||||
travel_planner.run(user_message, thread=thread, options={"wait_for_response": False})
|
||||
|
||||
|
||||
# Stream the response from Redis
|
||||
# This demonstrates that the client can stream from Redis while
|
||||
# the agent is still processing (or after it completes)
|
||||
asyncio.run(stream_from_redis(str(key)))
|
||||
|
||||
|
||||
logger.info("\nDemo completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Create the client
|
||||
client = get_client()
|
||||
|
||||
|
||||
# Run the demo
|
||||
run_client(client)
|
||||
|
||||
+1
-1
@@ -8,9 +8,9 @@ as a message broker. It enables clients to disconnect and reconnect without losi
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from collections.abc import AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
|
||||
|
||||
@@ -20,40 +20,40 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
# Configure logging (must be after imports to override their basicConfig)
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task Agent Sample with Redis Streaming...")
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
|
||||
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents and callbacks using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
agent_client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
try:
|
||||
# Run client interactions using helper function
|
||||
run_client(agent_client)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during agent interaction: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
@@ -153,13 +153,12 @@ def _get_weather_recommendation(condition: str) -> str:
|
||||
|
||||
if "rain" in condition_lower or "drizzle" in condition_lower:
|
||||
return "Bring an umbrella and waterproof jacket. Consider indoor activities for backup."
|
||||
elif "fog" in condition_lower:
|
||||
if "fog" in condition_lower:
|
||||
return "Morning visibility may be limited. Plan outdoor sightseeing for afternoon."
|
||||
elif "cold" in condition_lower:
|
||||
if "cold" in condition_lower:
|
||||
return "Layer up with warm clothing. Hot drinks and cozy cafés recommended."
|
||||
elif "hot" in condition_lower or "warm" in condition_lower:
|
||||
if "hot" in condition_lower or "warm" in condition_lower:
|
||||
return "Stay hydrated and use sunscreen. Plan strenuous activities for cooler morning hours."
|
||||
elif "thunder" in condition_lower or "storm" in condition_lower:
|
||||
if "thunder" in condition_lower or "storm" in condition_lower:
|
||||
return "Keep an eye on weather updates. Have indoor alternatives ready."
|
||||
else:
|
||||
return "Pleasant conditions expected. Great day for outdoor exploration!"
|
||||
return "Pleasant conditions expected. Great day for outdoor exploration!"
|
||||
|
||||
@@ -27,7 +27,6 @@ from agent_framework.azure import (
|
||||
)
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
|
||||
from redis_stream_response_handler import RedisStreamResponseHandler
|
||||
from tools import get_local_events, get_weather_forecast
|
||||
|
||||
@@ -186,12 +185,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -212,34 +211,34 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Create the Redis streaming callback
|
||||
redis_callback = RedisStreamCallback()
|
||||
|
||||
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker, callback=redis_callback)
|
||||
|
||||
|
||||
# Create and register the TravelPlanner agent
|
||||
logger.debug("Creating and registering TravelPlanner agent...")
|
||||
travel_agent = create_travel_agent()
|
||||
agent_worker.add_agent(travel_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agent: {travel_agent.name}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Agent Worker with Redis Streaming...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agent and callback
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
worker.start()
|
||||
|
||||
|
||||
try:
|
||||
# Keep the worker running
|
||||
while True:
|
||||
|
||||
+12
-12
@@ -41,12 +41,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -63,32 +63,32 @@ def run_client(client: DurableTaskSchedulerClient) -> None:
|
||||
client: The DurableTaskSchedulerClient instance
|
||||
"""
|
||||
logger.debug("Starting single agent chaining orchestration...")
|
||||
|
||||
|
||||
# Start the orchestration
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="single_agent_chaining_orchestration",
|
||||
input="",
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Orchestration started with instance ID: {instance_id}")
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
|
||||
|
||||
# Retrieve the final state
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
|
||||
|
||||
logger.debug("Orchestration completed successfully!")
|
||||
|
||||
|
||||
# Parse and display the result
|
||||
if result:
|
||||
final_text = json.loads(result)
|
||||
logger.info("Final refined sentence: %s \n", final_text)
|
||||
|
||||
|
||||
elif metadata:
|
||||
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
|
||||
if metadata.serialized_output:
|
||||
@@ -100,10 +100,10 @@ def run_client(client: DurableTaskSchedulerClient) -> None:
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task Single Agent Chaining Orchestration Client...")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
run_client(client)
|
||||
except Exception as e:
|
||||
|
||||
+7
-8
@@ -21,10 +21,9 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
# Configure logging
|
||||
@@ -35,22 +34,22 @@ logger = logging.getLogger(__name__)
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Single Agent Orchestration Chaining Sample...")
|
||||
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents and orchestrations using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
logger.debug("CLIENT: Starting orchestration...")
|
||||
|
||||
|
||||
# Run the client in the same process
|
||||
try:
|
||||
run_client(client)
|
||||
@@ -60,7 +59,7 @@ def main():
|
||||
logger.exception(f"Error during orchestration: {e}")
|
||||
finally:
|
||||
logger.debug("Worker stopping...")
|
||||
|
||||
|
||||
logger.debug("")
|
||||
logger.debug("Sample completed")
|
||||
|
||||
|
||||
+25
-25
@@ -11,15 +11,15 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
|
||||
from agent_framework import AgentResponse, ChatAgent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from durabletask.task import OrchestrationContext, Task
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import OrchestrationContext, Task
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -42,7 +42,7 @@ def create_writer_agent() -> "ChatAgent":
|
||||
"You refine short pieces of text. When given an initial sentence you enhance it;\n"
|
||||
"when given an improved sentence you polish it further."
|
||||
)
|
||||
|
||||
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name=WRITER_AGENT_NAME,
|
||||
instructions=instructions,
|
||||
@@ -78,18 +78,18 @@ def single_agent_chaining_orchestration(
|
||||
str: The final refined text from the second agent run
|
||||
"""
|
||||
logger.debug("[Orchestration] Starting single agent chaining...")
|
||||
|
||||
|
||||
# Wrap the orchestration context to access agents
|
||||
agent_context = DurableAIAgentOrchestrationContext(context)
|
||||
|
||||
|
||||
# Get the writer agent using the agent context
|
||||
writer = agent_context.get_agent(WRITER_AGENT_NAME)
|
||||
|
||||
|
||||
# Create a new thread for the conversation - this will be shared across both runs
|
||||
writer_thread = writer.get_new_thread()
|
||||
|
||||
|
||||
logger.debug(f"[Orchestration] Created thread: {writer_thread.session_id}")
|
||||
|
||||
|
||||
prompt = "Write a concise inspirational sentence about learning."
|
||||
# First run: Generate an initial inspirational sentence
|
||||
logger.info("[Orchestration] First agent run: Generating initial sentence about: %s", prompt)
|
||||
@@ -98,21 +98,21 @@ def single_agent_chaining_orchestration(
|
||||
thread=writer_thread,
|
||||
)
|
||||
logger.info(f"[Orchestration] Initial response: {initial_response.text}")
|
||||
|
||||
|
||||
# Second run: Refine the initial response on the same thread
|
||||
improved_prompt = (
|
||||
f"Improve this further while keeping it under 25 words: "
|
||||
f"{initial_response.text}"
|
||||
)
|
||||
|
||||
|
||||
logger.info("[Orchestration] Second agent run: Refining the sentence: %s", improved_prompt)
|
||||
refined_response = yield writer.run(
|
||||
messages=improved_prompt,
|
||||
thread=writer_thread,
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"[Orchestration] Refined response: {refined_response.text}")
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Chaining complete")
|
||||
return refined_response.text
|
||||
|
||||
@@ -134,12 +134,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -160,45 +160,45 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register the Writer agent
|
||||
logger.debug("Creating and registering Writer agent...")
|
||||
writer_agent = create_writer_agent()
|
||||
agent_worker.add_agent(writer_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agent: {writer_agent.name}")
|
||||
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(single_agent_chaining_orchestration) # type: ignore
|
||||
logger.debug(f"✓ Registered orchestration: {single_agent_chaining_orchestration.__name__}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Single Agent Chaining Worker with Orchestration...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents and orchestrations
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.debug("Worker is ready and listening for requests...")
|
||||
logger.debug("Press Ctrl+C to stop.")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.debug("Worker stopped")
|
||||
|
||||
|
||||
|
||||
+11
-11
@@ -41,12 +41,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -68,25 +68,25 @@ def run_client(client: DurableTaskSchedulerClient, prompt: str = "What is temper
|
||||
orchestrator="multi_agent_concurrent_orchestration",
|
||||
input=prompt,
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Orchestration started with instance ID: {instance_id}")
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
|
||||
|
||||
# Retrieve the final state
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
)
|
||||
|
||||
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
|
||||
|
||||
logger.debug("Orchestration completed successfully!")
|
||||
|
||||
|
||||
# Parse and display the result
|
||||
if result:
|
||||
result_json = json.loads(result) if isinstance(result, str) else result
|
||||
logger.info("Orchestration Results:\n%s", json.dumps(result_json, indent=2))
|
||||
|
||||
|
||||
elif metadata:
|
||||
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
|
||||
if metadata.serialized_output:
|
||||
@@ -98,10 +98,10 @@ def run_client(client: DurableTaskSchedulerClient, prompt: str = "What is temper
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Orchestration Client...")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
run_client(client)
|
||||
except Exception as e:
|
||||
|
||||
+6
-7
@@ -18,10 +18,9 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
# Configure logging
|
||||
@@ -32,20 +31,20 @@ logger = logging.getLogger(__name__)
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Orchestration Sample (Combined Worker + Client)...")
|
||||
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents and orchestrations using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
# Define the prompt
|
||||
prompt = "What is temperature?"
|
||||
logger.debug("CLIENT: Starting orchestration...")
|
||||
@@ -55,7 +54,7 @@ def main():
|
||||
run_client(client, prompt)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during sample execution: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
+29
-29
@@ -11,16 +11,16 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentResponse, ChatAgent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from durabletask.task import OrchestrationContext, when_all, Task
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import OrchestrationContext, Task, when_all
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -68,44 +68,44 @@ def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt:
|
||||
Returns:
|
||||
dict: Dictionary with 'physicist' and 'chemist' response texts
|
||||
"""
|
||||
|
||||
|
||||
logger.info(f"[Orchestration] Starting concurrent execution for prompt: {prompt}")
|
||||
|
||||
|
||||
# Wrap the orchestration context to access agents
|
||||
agent_context = DurableAIAgentOrchestrationContext(context)
|
||||
|
||||
|
||||
# Get agents using the agent context (returns DurableAIAgent proxies)
|
||||
physicist = agent_context.get_agent(PHYSICIST_AGENT_NAME)
|
||||
chemist = agent_context.get_agent(CHEMIST_AGENT_NAME)
|
||||
|
||||
|
||||
# Create separate threads for each agent
|
||||
physicist_thread = physicist.get_new_thread()
|
||||
chemist_thread = chemist.get_new_thread()
|
||||
|
||||
|
||||
logger.debug(f"[Orchestration] Created threads - Physicist: {physicist_thread.session_id}, Chemist: {chemist_thread.session_id}")
|
||||
|
||||
|
||||
# Create tasks from agent.run() calls - these return DurableAgentTask instances
|
||||
physicist_task = physicist.run(messages=str(prompt), thread=physicist_thread)
|
||||
chemist_task = chemist.run(messages=str(prompt), thread=chemist_thread)
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Created agent tasks, executing concurrently...")
|
||||
|
||||
|
||||
# Execute both tasks concurrently using when_all
|
||||
# The DurableAgentTask instances wrap the underlying entity calls
|
||||
task_results = yield when_all([physicist_task, chemist_task])
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Both agents completed")
|
||||
|
||||
|
||||
# Extract results from the tasks - DurableAgentTask yields AgentResponse
|
||||
physicist_result: AgentResponse = task_results[0]
|
||||
chemist_result: AgentResponse = task_results[1]
|
||||
|
||||
|
||||
result = {
|
||||
"physicist": physicist_result.text,
|
||||
"chemist": chemist_result.text,
|
||||
}
|
||||
|
||||
logger.debug(f"[Orchestration] Aggregated results ready")
|
||||
|
||||
logger.debug("[Orchestration] Aggregated results ready")
|
||||
return result
|
||||
|
||||
|
||||
@@ -126,12 +126,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -152,48 +152,48 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register both agents
|
||||
logger.debug("Creating and registering agents...")
|
||||
physicist_agent = create_physicist_agent()
|
||||
chemist_agent = create_chemist_agent()
|
||||
|
||||
|
||||
agent_worker.add_agent(physicist_agent)
|
||||
agent_worker.add_agent(chemist_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agents: {physicist_agent.name}, {chemist_agent.name}")
|
||||
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(multi_agent_concurrent_orchestration) # type: ignore
|
||||
logger.debug(f"✓ Registered orchestration: {multi_agent_concurrent_orchestration.__name__}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Multi-Agent Worker with Orchestration...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents and orchestrations
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.debug("Worker is ready and listening for requests...")
|
||||
logger.debug("Press Ctrl+C to stop.")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.debug("Worker stopped")
|
||||
|
||||
|
||||
|
||||
+17
-17
@@ -39,12 +39,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -70,36 +70,36 @@ def run_client(
|
||||
"email_id": email_id,
|
||||
"email_content": email_content,
|
||||
}
|
||||
|
||||
|
||||
logger.debug("Starting spam detection orchestration...")
|
||||
|
||||
|
||||
# Start the orchestration with the email payload
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="spam_detection_orchestration",
|
||||
input=payload,
|
||||
)
|
||||
|
||||
|
||||
logger.debug(f"Orchestration started with instance ID: {instance_id}")
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
|
||||
|
||||
# Retrieve the final state
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
|
||||
|
||||
logger.debug("Orchestration completed successfully!")
|
||||
|
||||
|
||||
# Parse and display the result
|
||||
if result:
|
||||
# Remove quotes if present
|
||||
if result.startswith('"') and result.endswith('"'):
|
||||
result = result[1:-1]
|
||||
logger.info(f"Result: {result}")
|
||||
|
||||
|
||||
elif metadata:
|
||||
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
|
||||
if metadata.serialized_output:
|
||||
@@ -111,29 +111,29 @@ def run_client(
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task Spam Detection Orchestration Client...")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
# Test with a legitimate email
|
||||
logger.info("TEST 1: Legitimate Email")
|
||||
|
||||
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-001",
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week."
|
||||
)
|
||||
|
||||
|
||||
# Test with a spam email
|
||||
logger.info("TEST 2: Spam Email")
|
||||
|
||||
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-002",
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during orchestration: {e}")
|
||||
finally:
|
||||
|
||||
+10
-11
@@ -18,10 +18,9 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -34,43 +33,43 @@ logger = logging.getLogger()
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task Spam Detection Orchestration Sample (Combined Worker + Client)...")
|
||||
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agents, orchestrations, and activities using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
client = get_client(log_handler=silent_handler)
|
||||
logger.debug("CLIENT: Starting orchestration tests...")
|
||||
|
||||
|
||||
try:
|
||||
# Test 1: Legitimate email
|
||||
# logger.info("TEST 1: Legitimate Email")
|
||||
|
||||
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-001",
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week."
|
||||
)
|
||||
|
||||
|
||||
# Test 2: Spam email
|
||||
logger.info("TEST 2: Spam Email")
|
||||
|
||||
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-002",
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during sample execution: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
+36
-36
@@ -11,16 +11,16 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import AgentResponse, ChatAgent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
# Configure logging
|
||||
@@ -118,24 +118,24 @@ def spam_detection_orchestration(context: OrchestrationContext, payload_raw: Any
|
||||
str: Result message from activity functions
|
||||
"""
|
||||
logger.debug("[Orchestration] Starting spam detection orchestration")
|
||||
|
||||
|
||||
# Validate input
|
||||
if not isinstance(payload_raw, dict):
|
||||
raise ValueError("Email data is required")
|
||||
|
||||
|
||||
try:
|
||||
payload = EmailPayload.model_validate(payload_raw)
|
||||
except ValidationError as exc:
|
||||
raise ValueError(f"Invalid email payload: {exc}") from exc
|
||||
|
||||
|
||||
logger.debug(f"[Orchestration] Processing email ID: {payload.email_id}")
|
||||
|
||||
|
||||
# Wrap the orchestration context to access agents
|
||||
agent_context = DurableAIAgentOrchestrationContext(context)
|
||||
|
||||
|
||||
# Get spam detection agent
|
||||
spam_agent = agent_context.get_agent(SPAM_AGENT_NAME)
|
||||
|
||||
|
||||
# Run spam detection
|
||||
spam_prompt = (
|
||||
"Analyze this email for spam content and return a JSON response with 'is_spam' (boolean) "
|
||||
@@ -143,52 +143,52 @@ def spam_detection_orchestration(context: OrchestrationContext, payload_raw: Any
|
||||
f"Email ID: {payload.email_id}\n"
|
||||
f"Content: {payload.email_content}"
|
||||
)
|
||||
|
||||
|
||||
logger.info("[Orchestration] Running spam detection agent: %s", spam_prompt)
|
||||
spam_result_task = spam_agent.run(
|
||||
messages=spam_prompt,
|
||||
options={"response_format": SpamDetectionResult},
|
||||
)
|
||||
|
||||
|
||||
spam_result_raw: AgentResponse = yield spam_result_task
|
||||
spam_result = cast(SpamDetectionResult, spam_result_raw.value)
|
||||
|
||||
|
||||
logger.info("[Orchestration] Spam detection result: is_spam=%s", spam_result.is_spam)
|
||||
|
||||
|
||||
# Branch based on spam detection result
|
||||
if spam_result.is_spam:
|
||||
logger.debug("[Orchestration] Email is spam, handling...")
|
||||
result_task: Task[str] = context.call_activity("handle_spam_email", input=spam_result.reason)
|
||||
result: str = yield result_task
|
||||
return result
|
||||
|
||||
|
||||
# Email is legitimate - draft a response
|
||||
logger.debug("[Orchestration] Email is legitimate, drafting response...")
|
||||
|
||||
|
||||
email_agent = agent_context.get_agent(EMAIL_AGENT_NAME)
|
||||
|
||||
|
||||
email_prompt = (
|
||||
"Draft a professional response to this email. Return a JSON response with a 'response' field "
|
||||
"containing the reply:\n\n"
|
||||
f"Email ID: {payload.email_id}\n"
|
||||
f"Content: {payload.email_content}"
|
||||
)
|
||||
|
||||
|
||||
logger.info("[Orchestration] Running email assistant agent: %s", email_prompt)
|
||||
email_result_task = email_agent.run(
|
||||
messages=email_prompt,
|
||||
options={"response_format": EmailResponse},
|
||||
)
|
||||
|
||||
|
||||
email_result_raw: AgentResponse = yield email_result_task
|
||||
email_result = cast(EmailResponse, email_result_raw.value)
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Email response drafted, sending...")
|
||||
result_task: Task[str] = context.call_activity("send_email", input=email_result.response)
|
||||
result: str = yield result_task
|
||||
|
||||
logger.info("Sent Email: %s", result)
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -209,12 +209,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -235,55 +235,55 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register both agents
|
||||
logger.debug("Creating and registering agents...")
|
||||
spam_agent = create_spam_agent()
|
||||
email_agent = create_email_agent()
|
||||
|
||||
|
||||
agent_worker.add_agent(spam_agent)
|
||||
agent_worker.add_agent(email_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agents: {spam_agent.name}, {email_agent.name}")
|
||||
|
||||
|
||||
# Register activity functions
|
||||
logger.debug("Registering activity functions...")
|
||||
worker.add_activity(handle_spam_email) # type: ignore[arg-type]
|
||||
worker.add_activity(send_email) # type: ignore[arg-type]
|
||||
logger.debug(f"✓ Registered activity: handle_spam_email")
|
||||
logger.debug(f"✓ Registered activity: send_email")
|
||||
|
||||
logger.debug("✓ Registered activity: handle_spam_email")
|
||||
logger.debug("✓ Registered activity: send_email")
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(spam_detection_orchestration) # type: ignore[arg-type]
|
||||
logger.debug(f"✓ Registered orchestration: {spam_detection_orchestration.__name__}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task Spam Detection Worker with Orchestration...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents, orchestrations, and activities
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.debug("Worker is ready and listening for requests...")
|
||||
logger.debug("Press Ctrl+C to stop.")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.debug("Worker stopped")
|
||||
|
||||
|
||||
|
||||
+52
-53
@@ -45,12 +45,12 @@ def get_client(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerClient(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -70,16 +70,16 @@ def _log_completion_result(
|
||||
"""
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
|
||||
logger.debug(f"Orchestration completed successfully!")
|
||||
|
||||
|
||||
logger.debug("Orchestration completed successfully!")
|
||||
|
||||
if result:
|
||||
try:
|
||||
result_dict = json.loads(result)
|
||||
logger.info("Final Result: %s", json.dumps(result_dict, indent=2))
|
||||
except json.JSONDecodeError:
|
||||
logger.debug(f"Result: {result}")
|
||||
|
||||
|
||||
elif metadata:
|
||||
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
|
||||
if metadata.serialized_output:
|
||||
@@ -105,7 +105,7 @@ def _wait_and_log_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
|
||||
_log_completion_result(metadata)
|
||||
|
||||
|
||||
@@ -127,18 +127,18 @@ def send_approval(
|
||||
"approved": approved,
|
||||
"feedback": feedback
|
||||
}
|
||||
|
||||
|
||||
logger.debug(f"Sending {'APPROVAL' if approved else 'REJECTION'} to instance {instance_id}")
|
||||
if feedback:
|
||||
logger.debug(f"Feedback: {feedback}")
|
||||
|
||||
|
||||
# Raise the external event
|
||||
client.raise_orchestration_event(
|
||||
instance_id=instance_id,
|
||||
event_name=HUMAN_APPROVAL_EVENT,
|
||||
data=approval_data
|
||||
)
|
||||
|
||||
|
||||
logger.debug("Event sent successfully")
|
||||
|
||||
|
||||
@@ -160,14 +160,14 @@ def wait_for_notification(
|
||||
True if notification detected, False if timeout
|
||||
"""
|
||||
logger.debug("Waiting for orchestration to reach notification point...")
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout_seconds:
|
||||
try:
|
||||
metadata = 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:
|
||||
@@ -183,19 +183,19 @@ def wait_for_notification(
|
||||
if metadata.serialized_custom_status.lower().startswith("requesting human feedback"):
|
||||
logger.debug("Orchestration is requesting human feedback")
|
||||
return True
|
||||
|
||||
|
||||
# Check for terminal states
|
||||
if metadata.runtime_status.name == "COMPLETED":
|
||||
logger.debug("Orchestration already completed")
|
||||
return False
|
||||
elif metadata.runtime_status.name == "FAILED":
|
||||
if metadata.runtime_status.name == "FAILED":
|
||||
logger.error("Orchestration failed")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Status check: {e}")
|
||||
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
logger.warning("Timeout waiting for notification")
|
||||
return False
|
||||
|
||||
@@ -208,94 +208,93 @@ def run_interactive_client(client: DurableTaskSchedulerClient) -> None:
|
||||
"""
|
||||
# Get user inputs
|
||||
logger.debug("Content Generation - Human-in-the-Loop")
|
||||
|
||||
|
||||
topic = input("Enter the topic for content generation: ").strip()
|
||||
if not topic:
|
||||
topic = "The benefits of cloud computing"
|
||||
logger.info(f"Using default topic: {topic}")
|
||||
|
||||
|
||||
max_attempts_str = input("Enter max review attempts (default: 3): ").strip()
|
||||
max_review_attempts = int(max_attempts_str) if max_attempts_str else 3
|
||||
|
||||
|
||||
timeout_hours_str = input("Enter approval timeout in hours (default: 5): ").strip()
|
||||
timeout_hours = float(timeout_hours_str) if timeout_hours_str else 5.0
|
||||
approval_timeout_seconds = int(timeout_hours * 3600)
|
||||
|
||||
|
||||
payload = {
|
||||
"topic": topic,
|
||||
"max_review_attempts": max_review_attempts,
|
||||
"approval_timeout_seconds": approval_timeout_seconds
|
||||
}
|
||||
|
||||
|
||||
logger.debug(f"Configuration: Topic={topic}, Max attempts={max_review_attempts}, Timeout={timeout_hours}h")
|
||||
|
||||
|
||||
# Start the orchestration
|
||||
logger.debug("Starting content generation orchestration...")
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="content_generation_hitl_orchestration",
|
||||
input=payload,
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Orchestration started with instance ID: {instance_id}")
|
||||
|
||||
|
||||
# Review loop
|
||||
attempt = 1
|
||||
while attempt <= max_review_attempts:
|
||||
logger.info(f"Review Attempt {attempt}/{max_review_attempts}")
|
||||
|
||||
|
||||
# Wait for orchestration to reach notification point
|
||||
logger.debug("Waiting for content generation...")
|
||||
if not wait_for_notification(client, instance_id, timeout_seconds=120):
|
||||
logger.error("Failed to receive notification. Orchestration may have completed or failed.")
|
||||
break
|
||||
|
||||
|
||||
logger.info("Content is ready for review! Please review the content in the worker logs.")
|
||||
|
||||
|
||||
# Get user decision
|
||||
while True:
|
||||
decision = input("Do you approve this content? (yes/no): ").strip().lower()
|
||||
if decision in ['yes', 'y', 'no', 'n']:
|
||||
if decision in ["yes", "y", "no", "n"]:
|
||||
break
|
||||
logger.info("Please enter 'yes' or 'no'")
|
||||
|
||||
approved = decision in ['yes', 'y']
|
||||
|
||||
|
||||
approved = decision in ["yes", "y"]
|
||||
|
||||
if approved:
|
||||
logger.debug("Sending approval...")
|
||||
send_approval(client, instance_id, approved=True)
|
||||
logger.info("Approval sent. Waiting for orchestration to complete...")
|
||||
_wait_and_log_completion(client, instance_id, timeout=60)
|
||||
break
|
||||
else:
|
||||
feedback = input("Enter feedback for improvement: ").strip()
|
||||
if not feedback:
|
||||
feedback = "Please revise the content."
|
||||
|
||||
logger.debug("Sending rejection with feedback...")
|
||||
send_approval(client, instance_id, approved=False, feedback=feedback)
|
||||
logger.info("Rejection sent. Content will be regenerated...")
|
||||
|
||||
attempt += 1
|
||||
|
||||
if attempt > max_review_attempts:
|
||||
logger.info(f"Maximum review attempts ({max_review_attempts}) reached.")
|
||||
_wait_and_log_completion(client, instance_id, timeout=30)
|
||||
break
|
||||
|
||||
# Small pause before next iteration
|
||||
time.sleep(2)
|
||||
feedback = input("Enter feedback for improvement: ").strip()
|
||||
if not feedback:
|
||||
feedback = "Please revise the content."
|
||||
|
||||
logger.debug("Sending rejection with feedback...")
|
||||
send_approval(client, instance_id, approved=False, feedback=feedback)
|
||||
logger.info("Rejection sent. Content will be regenerated...")
|
||||
|
||||
attempt += 1
|
||||
|
||||
if attempt > max_review_attempts:
|
||||
logger.info(f"Maximum review attempts ({max_review_attempts}) reached.")
|
||||
_wait_and_log_completion(client, instance_id, timeout=30)
|
||||
break
|
||||
|
||||
# Small pause before next iteration
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point for the client application."""
|
||||
logger.debug("Starting Durable Task HITL Content Generation Client")
|
||||
|
||||
|
||||
# Create client using helper function
|
||||
client = get_client()
|
||||
|
||||
|
||||
try:
|
||||
run_interactive_client(client)
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Interrupted by user")
|
||||
except Exception as e:
|
||||
|
||||
+8
-9
@@ -18,10 +18,9 @@ To run this sample:
|
||||
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import helper functions from worker and client modules
|
||||
from client import get_client, run_interactive_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -34,28 +33,28 @@ logger = logging.getLogger()
|
||||
def main():
|
||||
"""Main entry point - runs both worker and client in single process."""
|
||||
logger.debug("Starting Durable Task HITL Content Generation Sample (Combined Worker + Client)...")
|
||||
|
||||
|
||||
silent_handler = logging.NullHandler()
|
||||
# Create and start the worker using helper function and context manager
|
||||
with get_worker(log_handler=silent_handler) as dts_worker:
|
||||
# Register agent, orchestration, and activities using helper function
|
||||
setup_worker(dts_worker)
|
||||
|
||||
|
||||
# Start the worker
|
||||
dts_worker.start()
|
||||
logger.debug("Worker started and listening for requests...")
|
||||
|
||||
|
||||
# Create the client using helper function
|
||||
client = get_client(log_handler=silent_handler)
|
||||
|
||||
|
||||
try:
|
||||
logger.debug("CLIENT: Starting orchestration tests...")
|
||||
|
||||
|
||||
run_interactive_client(client)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Error during sample execution: {e}")
|
||||
|
||||
|
||||
logger.debug("Sample completed. Worker shutting down...")
|
||||
|
||||
|
||||
|
||||
+55
-54
@@ -11,17 +11,17 @@ Prerequisites:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import AgentResponse, ChatAgent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
# Configure logging
|
||||
@@ -64,7 +64,7 @@ def create_writer_agent() -> "ChatAgent":
|
||||
"Return your response as JSON with 'title' and 'content' fields."
|
||||
"Limit response to 300 words or less."
|
||||
)
|
||||
|
||||
|
||||
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name=WRITER_AGENT_NAME,
|
||||
instructions=instructions,
|
||||
@@ -85,6 +85,7 @@ def notify_user_for_approval(context: ActivityContext, content: dict[str, str])
|
||||
logger.info("Use the client to send approval or rejection.")
|
||||
return "Notification sent to user for approval."
|
||||
|
||||
|
||||
def publish_content(context: ActivityContext, content: dict[str, str]) -> str:
|
||||
"""Activity function to publish approved content.
|
||||
|
||||
@@ -100,7 +101,7 @@ def publish_content(context: ActivityContext, content: dict[str, str]) -> str:
|
||||
|
||||
|
||||
def content_generation_hitl_orchestration(
|
||||
context: OrchestrationContext,
|
||||
context: OrchestrationContext,
|
||||
payload_raw: Any
|
||||
) -> Generator[Task[Any], Any, dict[str, str]]:
|
||||
"""Human-in-the-loop orchestration for content generation with approval workflow.
|
||||
@@ -128,44 +129,44 @@ def content_generation_hitl_orchestration(
|
||||
RuntimeError: If max review attempts exhausted
|
||||
"""
|
||||
logger.debug("[Orchestration] Starting HITL content generation orchestration")
|
||||
|
||||
|
||||
# Validate input
|
||||
if not isinstance(payload_raw, dict):
|
||||
raise ValueError("Content generation input is required")
|
||||
|
||||
|
||||
try:
|
||||
payload = ContentGenerationInput.model_validate(payload_raw)
|
||||
except ValidationError as exc:
|
||||
raise ValueError(f"Invalid content generation input: {exc}") from exc
|
||||
|
||||
|
||||
logger.debug(f"[Orchestration] Topic: {payload.topic}")
|
||||
logger.debug(f"[Orchestration] Max attempts: {payload.max_review_attempts}")
|
||||
logger.debug(f"[Orchestration] Approval timeout: {payload.approval_timeout_seconds}s")
|
||||
|
||||
|
||||
# Wrap the orchestration context to access agents
|
||||
agent_context = DurableAIAgentOrchestrationContext(context)
|
||||
|
||||
|
||||
# Get the writer agent
|
||||
writer = agent_context.get_agent(WRITER_AGENT_NAME)
|
||||
writer_thread = writer.get_new_thread()
|
||||
|
||||
logger.info(f"ThreadID: {writer_thread.session_id}")
|
||||
|
||||
|
||||
# Generate initial content
|
||||
logger.info("[Orchestration] Generating initial content...")
|
||||
|
||||
|
||||
initial_response: AgentResponse = yield writer.run(
|
||||
messages=f"Write a short article about '{payload.topic}'.",
|
||||
thread=writer_thread,
|
||||
options={"response_format": GeneratedContent},
|
||||
)
|
||||
content = cast(GeneratedContent, initial_response.value)
|
||||
|
||||
|
||||
if not isinstance(content, GeneratedContent):
|
||||
raise ValueError("Agent returned no content after extraction.")
|
||||
|
||||
|
||||
logger.debug(f"[Orchestration] Initial content generated: {content.title}")
|
||||
|
||||
|
||||
# Review loop
|
||||
attempt = 0
|
||||
while attempt < payload.max_review_attempts:
|
||||
@@ -173,34 +174,34 @@ def content_generation_hitl_orchestration(
|
||||
logger.debug(f"[Orchestration] Review iteration #{attempt}/{payload.max_review_attempts}")
|
||||
|
||||
context.set_custom_status(f"Requesting human feedback (Attempt {attempt}, timeout {payload.approval_timeout_seconds}s)")
|
||||
|
||||
|
||||
# Notify user for approval
|
||||
yield context.call_activity(
|
||||
"notify_user_for_approval",
|
||||
"notify_user_for_approval",
|
||||
input=content.model_dump()
|
||||
)
|
||||
|
||||
logger.debug("[Orchestration] Waiting for human approval or timeout...")
|
||||
|
||||
|
||||
# Wait for approval event or timeout
|
||||
approval_task: Task[Any] = context.wait_for_external_event(HUMAN_APPROVAL_EVENT) # type: ignore
|
||||
timeout_task: Task[Any] = context.create_timer( # type: ignore
|
||||
context.current_utc_datetime + timedelta(seconds=payload.approval_timeout_seconds)
|
||||
)
|
||||
|
||||
|
||||
# Race between approval and timeout
|
||||
winner_task = yield when_any([approval_task, timeout_task]) # type: ignore
|
||||
|
||||
|
||||
if winner_task == approval_task:
|
||||
# Approval received before timeout
|
||||
logger.debug("[Orchestration] Received human approval event")
|
||||
|
||||
context.set_custom_status("Content reviewed by human reviewer.")
|
||||
|
||||
|
||||
# Parse approval
|
||||
approval_data: Any = approval_task.get_result() # type: ignore
|
||||
approval_data: Any = approval_task.get_result() # type: ignore
|
||||
logger.debug(f"[Orchestration] Approval data: {approval_data}")
|
||||
|
||||
|
||||
# Handle different formats of approval_data
|
||||
if isinstance(approval_data, dict):
|
||||
approval = HumanApproval.model_validate(approval_data)
|
||||
@@ -215,7 +216,7 @@ def content_generation_hitl_orchestration(
|
||||
approval = HumanApproval(approved=False, feedback=approval_data)
|
||||
else:
|
||||
approval = HumanApproval(approved=False, feedback=str(approval_data)) # type: ignore
|
||||
|
||||
|
||||
if approval.approved:
|
||||
# Content approved - publish and return
|
||||
logger.debug("[Orchestration] Content approved! Publishing...")
|
||||
@@ -225,52 +226,52 @@ def content_generation_hitl_orchestration(
|
||||
input=content.model_dump()
|
||||
)
|
||||
yield publish_task
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Content published successfully")
|
||||
return {"content": content.content, "title": content.title}
|
||||
|
||||
|
||||
# Content rejected - incorporate feedback and regenerate
|
||||
logger.debug(f"[Orchestration] Content rejected. Feedback: {approval.feedback}")
|
||||
|
||||
|
||||
# Check if we've exhausted attempts
|
||||
if attempt >= payload.max_review_attempts:
|
||||
context.set_custom_status("Max review attempts exhausted.")
|
||||
# Max attempts exhausted
|
||||
logger.error(f"[Orchestration] Max attempts ({payload.max_review_attempts}) exhausted")
|
||||
break
|
||||
|
||||
context.set_custom_status(f"Content rejected by human reviewer. Regenerating...")
|
||||
|
||||
|
||||
context.set_custom_status("Content rejected by human reviewer. Regenerating...")
|
||||
|
||||
rewrite_prompt = (
|
||||
"The content was rejected by a human reviewer. Please rewrite the article incorporating their feedback.\n\n"
|
||||
f"Human Feedback: {approval.feedback or 'No specific feedback provided.'}"
|
||||
)
|
||||
|
||||
|
||||
logger.debug("[Orchestration] Regenerating content with feedback...")
|
||||
|
||||
logger.warning(f"Regenerating with ThreadID: {writer_thread.session_id}")
|
||||
|
||||
|
||||
rewrite_response: AgentResponse = yield writer.run(
|
||||
messages=rewrite_prompt,
|
||||
thread=writer_thread,
|
||||
options={"response_format": GeneratedContent},
|
||||
)
|
||||
rewritten_content = cast(GeneratedContent, rewrite_response.value)
|
||||
|
||||
|
||||
if not isinstance(rewritten_content, GeneratedContent):
|
||||
raise ValueError("Agent returned no content after rewrite.")
|
||||
|
||||
|
||||
content = rewritten_content
|
||||
logger.debug(f"[Orchestration] Content regenerated: {content.title}")
|
||||
|
||||
|
||||
else:
|
||||
# Timeout occurred
|
||||
logger.error(f"[Orchestration] Approval timeout after {payload.approval_timeout_seconds}s")
|
||||
|
||||
|
||||
raise TimeoutError(
|
||||
f"Human approval timed out after {payload.approval_timeout_seconds} second(s)."
|
||||
)
|
||||
|
||||
|
||||
# If we exit the loop without returning, max attempts were exhausted
|
||||
context.set_custom_status("Max review attempts exhausted.")
|
||||
raise RuntimeError(
|
||||
@@ -295,12 +296,12 @@ def get_worker(
|
||||
"""
|
||||
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
|
||||
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
|
||||
|
||||
|
||||
logger.debug(f"Using taskhub: {taskhub_name}")
|
||||
logger.debug(f"Using endpoint: {endpoint_url}")
|
||||
|
||||
|
||||
credential = None if endpoint_url == "http://localhost:8080" else DefaultAzureCredential()
|
||||
|
||||
|
||||
return DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
@@ -321,52 +322,52 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""
|
||||
# Wrap it with the agent worker
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
|
||||
# Create and register the writer agent
|
||||
logger.debug("Creating and registering Writer agent...")
|
||||
writer_agent = create_writer_agent()
|
||||
agent_worker.add_agent(writer_agent)
|
||||
|
||||
|
||||
logger.debug(f"✓ Registered agent: {writer_agent.name}")
|
||||
|
||||
|
||||
# Register activity functions
|
||||
logger.debug("Registering activity functions...")
|
||||
worker.add_activity(notify_user_for_approval) # type: ignore
|
||||
worker.add_activity(publish_content) # type: ignore
|
||||
logger.debug(f"✓ Registered activity: notify_user_for_approval")
|
||||
logger.debug(f"✓ Registered activity: publish_content")
|
||||
|
||||
logger.debug("✓ Registered activity: notify_user_for_approval")
|
||||
logger.debug("✓ Registered activity: publish_content")
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(content_generation_hitl_orchestration) # type: ignore
|
||||
worker.add_orchestrator(content_generation_hitl_orchestration) # type: ignore
|
||||
logger.debug(f"✓ Registered orchestration: {content_generation_hitl_orchestration.__name__}")
|
||||
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the worker process."""
|
||||
logger.debug("Starting Durable Task HITL Content Generation Worker...")
|
||||
|
||||
|
||||
# Create a worker using the helper function
|
||||
worker = get_worker()
|
||||
|
||||
|
||||
# Setup worker with agents, orchestrations, and activities
|
||||
setup_worker(worker)
|
||||
|
||||
|
||||
logger.debug("Worker is ready and listening for requests...")
|
||||
logger.debug("Press Ctrl+C to stop.")
|
||||
|
||||
|
||||
try:
|
||||
# Start the worker (this blocks until stopped)
|
||||
worker.start()
|
||||
|
||||
|
||||
# Keep the worker running
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.debug("Worker shutdown initiated")
|
||||
|
||||
|
||||
logger.debug("Worker stopped")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user