Python: Complete durableagent package (#3058)

* Add worker and clients

* Clean code and refactor common code

* Implement sample

* Add sample

* Update readmes

* Fix tests

* Fix tests

* Update requirements

* Fix typo

* Address comments

* use response.text
This commit is contained in:
Laveesh Rohra
2026-01-07 13:53:21 -08:00
committed by GitHub
Unverified
parent a5b36dc379
commit e3eff65a6b
46 changed files with 4477 additions and 1644 deletions
@@ -0,0 +1,66 @@
# Single Agent Sample
This sample demonstrates how to use the durable agents extension to create a worker-client setup that hosts a single AI agent and provides interactive conversation via the Durable Task Scheduler.
## Key Concepts Demonstrated
- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
- Registering durable agents with the worker and interacting with them via a client.
- Conversation management (via threads) for isolated interactions.
- Worker-client architecture for distributed agent execution.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup, you can run the sample using separate worker and client processes:
**Start the worker:**
```bash
cd samples/getting_started/durabletask/01_single_agent
python worker.py
```
The worker will register the Joker agent and listen for requests.
**In a new terminal, run the client:**
```bash
python client.py
```
The client will interact with the Joker agent:
```
Starting Durable Task Agent Client...
Using taskhub: default
Using endpoint: http://localhost:8080
Getting reference to Joker agent...
Created conversation thread: a1b2c3d4-e5f6-7890-abcd-ef1234567890
User: Tell me a short joke about cloud computing.
Joker: Why did the cloud break up with the server?
Because it found someone more "uplifting"!
User: Now tell me one about Python programming.
Joker: Why do Python programmers prefer dark mode?
Because light attracts bugs!
```
## Viewing Agent State
You can view the state of the agent in the Durable Task Scheduler dashboard:
1. Open your browser and navigate to `http://localhost:8082`
2. In the dashboard, you can view the state of the Joker agent, including its conversation history and current state
The agent maintains conversation state across multiple interactions, and you can inspect this state in the dashboard to understand how the durable agents extension manages conversation context.
@@ -0,0 +1,92 @@
"""Client application for interacting with a Durable Task hosted agent.
This client connects to the Durable Task Scheduler and sends requests to
registered agents, demonstrating how to interact with agents from external processes.
Prerequisites:
- The worker must be running with the agent registered
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running
"""
import asyncio
import logging
import os
from agent_framework_durabletask import DurableAIAgentClient
from azure.identity import DefaultAzureCredential
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def main() -> None:
"""Main entry point for the client application."""
logger.info("Starting Durable Task Agent Client...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a client using Azure Managed Durable Task
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
# Wrap it with the agent client
agent_client = DurableAIAgentClient(client)
# Get a reference to the Joker agent
logger.info("Getting reference to Joker agent...")
joker = agent_client.get_agent("Joker")
# Create a new thread for the conversation
thread = joker.get_new_thread()
logger.info(f"Created conversation thread: {thread.session_id}")
logger.info("")
try:
# First message
message1 = "Tell me a short joke about cloud computing."
logger.info(f"User: {message1}")
logger.info("")
# Run the agent - this blocks until the response is ready
response1 = joker.run(message1, thread=thread)
logger.info(f"Agent: {response1.text}")
logger.info("")
# Second message - continuing the conversation
message2 = "Now tell me one about Python programming."
logger.info(f"User: {message2}")
logger.info("")
response2 = joker.run(message2, thread=thread)
logger.info(f"Agent: {response2.text}")
logger.info("")
logger.info(f"Conversation completed successfully!")
logger.info(f"Thread ID: {thread.session_id}")
except Exception as e:
logger.exception(f"Error during agent interaction: {e}")
finally:
logger.info("Client shutting down")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,6 @@
# Agent Framework packages (installing from local package until a package is published)
-e ../../../../
-e ../../../../packages/durabletask
# Azure authentication
azure-identity
@@ -0,0 +1,137 @@
"""Single Agent Sample - Durable Task Integration (Combined Worker + Client)
This sample demonstrates running both the worker and client in a single process.
The worker is started first to register the agent, then client operations are
performed against the running worker.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running (e.g., using Docker)
To run this sample:
python sample.py
"""
import logging
import os
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentClient, DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from dotenv import load_dotenv
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_joker_agent():
"""Create the Joker agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Joker agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="Joker",
instructions="You are good at telling jokes.",
)
def main():
"""Main entry point - runs both worker and client in single process."""
logger.info("Starting Durable Task Agent Sample (Combined Worker + Client)...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
secure_channel = endpoint != "http://localhost:8080"
# Create and start the worker using a context manager
with DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=secure_channel,
taskhub=taskhub_name,
token_credential=credential
) as worker:
# Wrap with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register the Joker agent
logger.info("Creating and registering Joker agent...")
joker_agent = create_joker_agent()
agent_worker.add_agent(joker_agent)
logger.info(f"✓ Registered agent: {joker_agent.name}")
logger.info(f" Entity name: dafx-{joker_agent.name}")
logger.info("")
# Start the worker
worker.start()
logger.info("Worker started and listening for requests...")
logger.info("")
# Create the client
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=secure_channel,
taskhub=taskhub_name,
token_credential=credential
)
# Wrap it with the agent client
agent_client = DurableAIAgentClient(client)
# Get a reference to the Joker agent
logger.info("Getting reference to Joker agent...")
joker = agent_client.get_agent("Joker")
# Create a new thread for the conversation
thread = joker.get_new_thread()
logger.info(f"Created conversation thread: {thread.session_id}")
logger.info("")
try:
# First message
message1 = "Tell me a short joke about cloud computing."
logger.info(f"User: {message1}")
logger.info("")
# Run the agent - this blocks until the response is ready
response1 = joker.run(message1, thread=thread)
logger.info(f"Agent: {response1.text}; {response1}")
logger.info("")
# Second message - continuing the conversation
message2 = "Now tell me one about Python programming."
logger.info(f"User: {message2}")
logger.info("")
response2 = joker.run(message2, thread=thread)
logger.info(f"Agent: {response2.text}; {response2}")
logger.info("")
logger.info(f"Conversation completed successfully!")
logger.info(f"Thread ID: {thread.session_id}")
except Exception as e:
logger.exception(f"Error during agent interaction: {e}")
logger.info("")
logger.info("Sample completed. Worker shutting down...")
if __name__ == "__main__":
load_dotenv()
main()
@@ -0,0 +1,89 @@
"""Worker process for hosting a single Azure OpenAI-powered agent using Durable Task.
This worker registers agents as durable entities and continuously listens for requests.
The worker should run as a background service, processing incoming agent requests.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Start a Durable Task Scheduler (e.g., using Docker)
"""
import asyncio
import logging
import os
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_joker_agent():
"""Create the Joker agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Joker agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name="Joker",
instructions="You are good at telling jokes.",
)
async def main():
"""Main entry point for the worker process."""
logger.info("Starting Durable Task Agent Worker...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a worker using Azure Managed Durable Task
worker = DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
# Wrap it with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register the Joker agent
logger.info("Creating and registering Joker agent...")
joker_agent = create_joker_agent()
agent_worker.add_agent(joker_agent)
logger.info(f"✓ Registered agent: {joker_agent.name}")
logger.info(f" Entity name: dafx-{joker_agent.name}")
logger.info("")
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.info("Worker shutdown initiated")
logger.info("Worker stopped")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,72 @@
# Single Agent Orchestration Chaining Sample
This sample demonstrates how to chain multiple invocations of the same agent using a durable orchestration while preserving conversation state between runs.
## Key Concepts Demonstrated
- Using durable orchestrations to coordinate sequential agent invocations.
- Chaining agent calls where the output of one run becomes input to the next.
- Maintaining conversation context across sequential runs using a shared thread.
- Using `DurableAIAgentOrchestrationContext` to access agents within orchestrations.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup, you can run the sample using one of two approaches:
### Option 1: Combined Worker + Client (Quick Start)
```bash
cd samples/getting_started/durabletask/04_single_agent_orchestration_chaining
python sample.py
```
This runs both worker and client in a single process.
### Option 2: Separate Worker and Client
**Start the worker in one terminal:**
```bash
python worker.py
```
**In a new terminal, run the client:**
```bash
python client.py
```
The orchestration will execute the writer agent twice sequentially, and you'll see output like:
```
[Orchestration] Starting single agent chaining...
[Orchestration] Created thread: abc-123
[Orchestration] First agent run: Generating initial sentence...
[Orchestration] Initial response: Every small step forward is progress toward mastery.
[Orchestration] Second agent run: Refining the sentence...
[Orchestration] Refined response: Each small step forward brings you closer to mastery and growth.
[Orchestration] Chaining complete
================================================================================
Orchestration Result
================================================================================
Each small step forward brings you closer to mastery and growth.
```
## Viewing Orchestration State
You can view the state of the orchestration in the Durable Task Scheduler dashboard:
1. Open your browser and navigate to `http://localhost:8082`
2. In the dashboard, you can view the orchestration instance, including:
- The sequential execution of both agent runs
- The conversation thread shared between runs
- Input and output at each step
- Overall orchestration state and history
The orchestration maintains the conversation context across both agent invocations, demonstrating how durable orchestrations can coordinate multi-step agent workflows.
@@ -0,0 +1,104 @@
"""Client application for starting a single agent chaining orchestration.
This client connects to the Durable Task Scheduler and starts an orchestration
that runs a writer agent twice sequentially on the same thread, demonstrating
how conversation context is maintained across multiple agent invocations.
Prerequisites:
- The worker must be running with the writer agent and orchestration registered
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running
"""
import asyncio
import json
import logging
import os
from azure.identity import DefaultAzureCredential
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def main() -> None:
"""Main entry point for the client application."""
logger.info("Starting Durable Task Single Agent Chaining Orchestration Client...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a client using Azure Managed Durable Task
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
logger.info("Starting single agent chaining orchestration...")
logger.info("")
try:
# Start the orchestration
instance_id = client.schedule_new_orchestration(
orchestrator="single_agent_chaining_orchestration",
input="",
)
logger.info(f"Orchestration started with instance ID: {instance_id}")
logger.info("Waiting for orchestration to complete...")
logger.info("")
# 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.info("=" * 80)
logger.info("Orchestration completed successfully!")
logger.info("=" * 80)
logger.info("")
logger.info("Results:")
logger.info("")
# Parse and display the result
if result:
final_text = json.loads(result)
logger.info("Final refined sentence:")
logger.info(f" {final_text}")
logger.info("")
logger.info("=" * 80)
elif metadata:
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
if metadata.serialized_output:
logger.error(f"Output: {metadata.serialized_output}")
else:
logger.error("Orchestration did not complete within the timeout period")
except Exception as e:
logger.exception(f"Error during orchestration: {e}")
finally:
logger.info("")
logger.info("Client shutting down")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,6 @@
# Agent Framework packages (installing from local package until a package is published)
-e ../../../../
-e ../../../../packages/durabletask
# Azure authentication
azure-identity
@@ -0,0 +1,255 @@
"""Single Agent Orchestration Chaining Sample - Durable Task Integration
This sample demonstrates chaining two invocations of the same agent inside a Durable Task
orchestration while preserving the conversation state between runs. The orchestration
runs the writer agent sequentially on a shared thread to refine text iteratively.
Components used:
- AzureOpenAIChatClient to construct the writer agent
- DurableTaskSchedulerWorker and DurableAIAgentWorker for agent hosting
- DurableTaskSchedulerClient and orchestration for sequential agent invocations
- Thread management to maintain conversation context across invocations
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running (e.g., using Docker emulator)
To run this sample:
python sample.py
"""
import asyncio
import json
import logging
import os
from collections.abc import Generator
from typing import Any
from agent_framework import AgentRunResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentOrchestrationContext, DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from dotenv import load_dotenv
from durabletask.task import OrchestrationContext, Task
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Agent name
WRITER_AGENT_NAME = "WriterAgent"
def create_writer_agent():
"""Create the Writer agent using Azure OpenAI.
This agent refines short pieces of text, enhancing initial sentences
and polishing improved versions further.
Returns:
AgentProtocol: The configured Writer agent
"""
instructions = (
"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()).create_agent(
name=WRITER_AGENT_NAME,
instructions=instructions,
)
def single_agent_chaining_orchestration(
context: OrchestrationContext, _: str
) -> Generator[Task[Any], Any, str]:
"""Orchestration that runs the writer agent twice on the same thread.
This demonstrates chaining behavior where the output of the first agent run
becomes part of the input for the second run, all while maintaining the
conversation context through a shared thread.
Args:
context: The orchestration context
_: Input parameter (unused)
Returns:
str: The final refined text from the second agent run
"""
logger.info("[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.info(f"[Orchestration] Created thread: {writer_thread.session_id}")
# First run: Generate an initial inspirational sentence
logger.info("[Orchestration] First agent run: Generating initial sentence...")
initial_response: AgentRunResponse = yield writer.run(
messages="Write a concise inspirational sentence about learning.",
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...")
refined_response: AgentRunResponse = yield writer.run(
messages=improved_prompt,
thread=writer_thread,
)
logger.info(f"[Orchestration] Refined response: {refined_response.text}")
logger.info("[Orchestration] Chaining complete")
return refined_response.text
async def run_client(
endpoint: str, taskhub_name: str, credential: DefaultAzureCredential | None
):
"""Run the client to start and monitor the orchestration.
Args:
endpoint: The durable task scheduler endpoint
taskhub_name: The task hub name
credential: The credential for authentication
"""
logger.info("")
logger.info("=" * 80)
logger.info("CLIENT: Starting orchestration...")
logger.info("=" * 80)
logger.info("")
# Create a client
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
try:
# Start the orchestration
instance_id = client.schedule_new_orchestration(
single_agent_chaining_orchestration
)
logger.info(f"Orchestration started with instance ID: {instance_id}")
logger.info("Waiting for orchestration to complete...")
logger.info("")
# 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.info("")
logger.info("=" * 80)
logger.info("ORCHESTRATION COMPLETED SUCCESSFULLY!")
logger.info("=" * 80)
logger.info("")
# Parse and display the result
if result:
final_text = json.loads(result)
logger.info("Final refined sentence:")
logger.info(f" {final_text}")
else:
logger.warning("No output returned from orchestration")
elif metadata:
logger.error(f"Orchestration did not complete successfully: {metadata.runtime_status.name}")
if metadata.serialized_output:
logger.error(f"Output: {metadata.serialized_output}")
else:
logger.error("Could not retrieve orchestration metadata")
except Exception as e:
logger.exception(f"Error during orchestration: {e}")
logger.info("")
logger.info("Client shutting down")
def main():
"""Main entry point - runs both worker and client in single process."""
logger.info("Starting Single Agent Orchestration Chaining Sample...")
logger.info("")
# Load environment variables
load_dotenv()
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
secure_channel = endpoint != "http://localhost:8080"
# Create and start the worker using a context manager
with DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=secure_channel,
taskhub=taskhub_name,
token_credential=credential
) as worker:
# Wrap with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register the Writer agent
logger.info("Creating and registering Writer agent...")
writer_agent = create_writer_agent()
agent_worker.add_agent(writer_agent)
logger.info(f"✓ Registered agent: {writer_agent.name}")
logger.info(f" Entity name: dafx-{writer_agent.name}")
# Register the orchestration function
logger.info("Registering orchestration function...")
worker.add_orchestrator(single_agent_chaining_orchestration)
logger.info("✓ Registered orchestration: single_agent_chaining_orchestration")
logger.info("")
# Start the worker
worker.start()
logger.info("Worker started and listening for requests...")
logger.info("")
# Run the client in the same process
try:
asyncio.run(run_client(endpoint, taskhub_name, credential))
except KeyboardInterrupt:
logger.info("Sample interrupted by user")
finally:
logger.info("Worker stopping...")
logger.info("Sample completed")
if __name__ == "__main__":
load_dotenv()
main()
@@ -0,0 +1,167 @@
"""Worker process for hosting a single agent with chaining orchestration using Durable Task.
This worker registers a writer agent and an orchestration function that demonstrates
chaining behavior by running the agent twice sequentially on the same thread,
preserving conversation context between invocations.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Start a Durable Task Scheduler (e.g., using Docker)
"""
import asyncio
from collections.abc import Generator
import logging
import os
from typing import Any
from agent_framework import AgentRunResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentOrchestrationContext, DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from durabletask.task import OrchestrationContext, Task
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Agent name
WRITER_AGENT_NAME = "WriterAgent"
def create_writer_agent():
"""Create the Writer agent using Azure OpenAI.
This agent refines short pieces of text, enhancing initial sentences
and polishing improved versions further.
Returns:
AgentProtocol: The configured Writer agent
"""
instructions = (
"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()).create_agent(
name=WRITER_AGENT_NAME,
instructions=instructions,
)
def single_agent_chaining_orchestration(
context: OrchestrationContext, _: str
) -> Generator[Task[Any], Any, str]:
"""Orchestration that runs the writer agent twice on the same thread.
This demonstrates chaining behavior where the output of the first agent run
becomes part of the input for the second run, all while maintaining the
conversation context through a shared thread.
Args:
context: The orchestration context
_: Input parameter (unused)
Returns:
str: The final refined text from the second agent run
"""
logger.info("[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.info(f"[Orchestration] Created thread: {writer_thread.session_id}")
# First run: Generate an initial inspirational sentence
logger.info("[Orchestration] First agent run: Generating initial sentence...")
initial_response: AgentRunResponse = yield writer.run(
messages="Write a concise inspirational sentence about learning.",
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...")
refined_response: AgentRunResponse = yield writer.run(
messages=improved_prompt,
thread=writer_thread,
)
logger.info(f"[Orchestration] Refined response: {refined_response.text}")
logger.info("[Orchestration] Chaining complete")
return refined_response.text
async def main():
"""Main entry point for the worker process."""
logger.info("Starting Durable Task Single Agent Chaining Worker with Orchestration...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a worker using Azure Managed Durable Task
worker = DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
# Wrap it with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register the Writer agent
logger.info("Creating and registering Writer agent...")
writer_agent = create_writer_agent()
agent_worker.add_agent(writer_agent)
logger.info(f"✓ Registered agent: {writer_agent.name}")
logger.info(f" Entity name: dafx-{writer_agent.name}")
logger.info("")
# Register the orchestration function
logger.info("Registering orchestration function...")
worker.add_orchestrator(single_agent_chaining_orchestration)
logger.info(f"✓ Registered orchestration: {single_agent_chaining_orchestration.__name__}")
logger.info("")
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.info("Worker shutdown initiated")
logger.info("Worker stopped")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,75 @@
# Multi-Agent Orchestration with Concurrency Sample
This sample demonstrates how to host multiple agents and run them concurrently using a durable orchestration, aggregating their responses into a single result.
## Key Concepts Demonstrated
- Running multiple specialized agents in parallel within an orchestration.
- Using `OrchestrationAgentExecutor` to get `DurableAgentTask` objects for concurrent execution.
- Aggregating results from multiple agents using `task.when_all()`.
- Creating separate conversation threads for independent agent contexts.
## Environment Setup
See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
## Running the Sample
With the environment setup, you can run the sample using one of two approaches:
### Option 1: Combined Worker + Client (Quick Start)
```bash
cd samples/getting_started/durabletask/05_multi_agent_orchestration_concurrency
python sample.py
```
This runs both worker and client in a single process.
### Option 2: Separate Worker and Client
**Start the worker in one terminal:**
```bash
python worker.py
```
**In a new terminal, run the client:**
```bash
python client.py
```
The orchestration will execute both agents concurrently, and you'll see output like:
```
Prompt: What is temperature?
Starting multi-agent concurrent orchestration...
Orchestration started with instance ID: abc123...
Orchestration status: COMPLETED
Results:
Physicist's response:
Temperature measures the average kinetic energy of particles in a system...
Chemist's response:
Temperature reflects how molecular motion influences reaction rates...
```
## Viewing Orchestration State
You can view the state of the orchestration in the Durable Task Scheduler dashboard:
1. Open your browser and navigate to `http://localhost:8082`
2. In the dashboard, you can view the orchestration instance, including:
- The concurrent execution of both agents (Physicist and Chemist)
- Separate conversation threads for each agent
- Parallel task execution and completion timing
- Aggregated results from both agents
- Overall orchestration state and history
The orchestration demonstrates how multiple agents can be executed in parallel, with results collected and aggregated once all agents complete.
@@ -0,0 +1,114 @@
"""Client application for starting a multi-agent concurrent orchestration.
This client connects to the Durable Task Scheduler and starts an orchestration
that runs two agents (physicist and chemist) concurrently, then retrieves and
displays the aggregated results.
Prerequisites:
- The worker must be running with both agents and orchestration registered
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running
"""
import asyncio
import json
import logging
import os
from azure.identity import DefaultAzureCredential
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def main() -> None:
"""Main entry point for the client application."""
logger.info("Starting Durable Task Multi-Agent Orchestration Client...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a client using Azure Managed Durable Task
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
# Define the prompt to send to both agents
prompt = "What is temperature?"
logger.info(f"Prompt: {prompt}")
logger.info("")
logger.info("Starting multi-agent concurrent orchestration...")
try:
# Start the orchestration with the prompt as input
instance_id = client.schedule_new_orchestration(
orchestrator="multi_agent_concurrent_orchestration",
input=prompt,
)
logger.info(f"Orchestration started with instance ID: {instance_id}")
logger.info("Waiting for orchestration to complete...")
logger.info("")
# 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.info("=" * 80)
logger.info("Orchestration completed successfully!")
logger.info("=" * 80)
logger.info("")
logger.info(f"Prompt: {prompt}")
logger.info("")
logger.info("Results:")
logger.info("")
# Parse and display the result
if result:
result_dict = json.loads(result)
logger.info("Physicist's response:")
logger.info(f" {result_dict.get('physicist', 'N/A')}")
logger.info("")
logger.info("Chemist's response:")
logger.info(f" {result_dict.get('chemist', 'N/A')}")
logger.info("")
logger.info("=" * 80)
elif metadata:
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
if metadata.serialized_output:
logger.error(f"Output: {metadata.serialized_output}")
else:
logger.error("Orchestration did not complete within the timeout period")
except Exception as e:
logger.exception(f"Error during orchestration: {e}")
finally:
logger.info("")
logger.info("Client shutting down")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,6 @@
# Agent Framework packages (installing from local package until a package is published)
-e ../../../../
-e ../../../../packages/durabletask
# Azure authentication
azure-identity
@@ -0,0 +1,266 @@
"""Multi-Agent Orchestration Sample - Durable Task Integration (Combined Worker + Client)
This sample demonstrates running both the worker and client in a single process for
concurrent multi-agent orchestration. The worker registers two domain-specific agents
(physicist and chemist) and an orchestration function that runs them in parallel.
The orchestration uses OrchestrationAgentExecutor to execute agents concurrently
and aggregate their responses.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Durable Task Scheduler must be running (e.g., using Docker)
To run this sample:
python sample.py
"""
import asyncio
import json
import logging
import os
from collections.abc import Generator
from typing import Any
from agent_framework import AgentRunResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentOrchestrationContext, DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from dotenv import load_dotenv
from durabletask.task import OrchestrationContext, when_all, Task
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Agent names
PHYSICIST_AGENT_NAME = "PhysicistAgent"
CHEMIST_AGENT_NAME = "ChemistAgent"
def create_physicist_agent():
"""Create the Physicist agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Physicist agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name=PHYSICIST_AGENT_NAME,
instructions="You are an expert in physics. You answer questions from a physics perspective.",
)
def create_chemist_agent():
"""Create the Chemist agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Chemist agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name=CHEMIST_AGENT_NAME,
instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.",
)
def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt: str) -> Generator[Task[Any], Any, dict[str, str]]:
"""Orchestration that runs both agents in parallel and aggregates results.
Uses DurableAIAgentOrchestrationContext to wrap the orchestration context and
access agents via the OrchestrationAgentExecutor.
Args:
context: The orchestration context
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.info(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.info("[Orchestration] Created agent tasks, executing concurrently...")
# Execute both tasks concurrently using task.when_all
# The DurableAgentTask instances wrap the underlying entity calls
task_results = yield when_all([physicist_task, chemist_task])
logger.info("[Orchestration] Both agents completed")
# Extract results from the tasks - DurableAgentTask yields AgentRunResponse
physicist_result: AgentRunResponse = task_results[0]
chemist_result: AgentRunResponse = task_results[1]
result = {
"physicist": physicist_result.text,
"chemist": chemist_result.text,
}
logger.info(f"[Orchestration] Aggregated results ready")
return result
async def run_client(endpoint: str, taskhub_name: str, credential: DefaultAzureCredential | None, prompt: str):
"""Run the client to start and monitor the orchestration.
Args:
endpoint: The durable task scheduler endpoint
taskhub_name: The task hub name
credential: The credential for authentication
prompt: The prompt to send to both agents
"""
logger.info("")
logger.info("=" * 80)
logger.info("CLIENT: Starting orchestration...")
logger.info("=" * 80)
# Create a client
client = DurableTaskSchedulerClient(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
logger.info(f"Prompt: {prompt}")
logger.info("")
try:
# Start the orchestration with the prompt as input
instance_id = client.schedule_new_orchestration(
multi_agent_concurrent_orchestration,
input=prompt,
)
logger.info(f"Orchestration started with instance ID: {instance_id}")
logger.info("Waiting for orchestration to complete...")
logger.info("")
# 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.info("")
logger.info("=" * 80)
logger.info("ORCHESTRATION COMPLETED SUCCESSFULLY!")
logger.info("=" * 80)
logger.info("")
logger.info(f"Prompt: {prompt}")
logger.info("")
logger.info("Results:")
logger.info("")
# Parse and display the result
if result:
result_dict = json.loads(result)
logger.info("Physicist's response:")
logger.info(f" {result_dict.get('physicist', 'N/A')}")
logger.info("")
logger.info("Chemist's response:")
logger.info(f" {result_dict.get('chemist', 'N/A')}")
logger.info("")
logger.info("=" * 80)
elif metadata:
logger.error(f"Orchestration ended with status: {metadata.runtime_status.name}")
if metadata.serialized_output:
logger.error(f"Output: {metadata.serialized_output}")
else:
logger.error("Orchestration did not complete within the timeout period")
except Exception as e:
logger.exception(f"Error during orchestration: {e}")
def main():
"""Main entry point - runs both worker and client in single process."""
logger.info("Starting Durable Task Multi-Agent Orchestration Sample (Combined Worker + Client)...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
logger.info("")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
secure_channel = endpoint != "http://localhost:8080"
# Create and start the worker using a context manager
with DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=secure_channel,
taskhub=taskhub_name,
token_credential=credential
) as worker:
# Wrap with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register both agents
logger.info("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.info(f"✓ Registered agent: {physicist_agent.name}")
logger.info(f" Entity name: dafx-{physicist_agent.name}")
logger.info(f"✓ Registered agent: {chemist_agent.name}")
logger.info(f" Entity name: dafx-{chemist_agent.name}")
logger.info("")
# Register the orchestration function
logger.info("Registering orchestration function...")
worker.add_orchestrator(multi_agent_concurrent_orchestration)
logger.info(f"✓ Registered orchestration: {multi_agent_concurrent_orchestration.__name__}")
logger.info("")
# Start the worker
worker.start()
logger.info("Worker started and listening for requests...")
# Define the prompt
prompt = "What is temperature?"
try:
# Run the client to start the orchestration
asyncio.run(run_client(endpoint, taskhub_name, credential, prompt))
except Exception as e:
logger.exception(f"Error during sample execution: {e}")
logger.info("")
logger.info("Sample completed. Worker shutting down...")
if __name__ == "__main__":
load_dotenv()
main()
@@ -0,0 +1,175 @@
"""Worker process for hosting multiple agents with orchestration using Durable Task.
This worker registers two domain-specific agents (physicist and chemist) and an orchestration
function that runs them concurrently. The orchestration uses OrchestrationAgentExecutor
to execute agents in parallel and aggregate their responses.
Prerequisites:
- Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
(plus AZURE_OPENAI_API_KEY or Azure CLI authentication)
- Start a Durable Task Scheduler (e.g., using Docker)
"""
import asyncio
from collections.abc import Generator
import logging
import os
from typing import Any
from agent_framework import AgentRunResponse
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_durabletask import DurableAIAgentOrchestrationContext, DurableAIAgentWorker
from azure.identity import AzureCliCredential, DefaultAzureCredential
from durabletask.task import OrchestrationContext, when_all, Task
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Agent names
PHYSICIST_AGENT_NAME = "PhysicistAgent"
CHEMIST_AGENT_NAME = "ChemistAgent"
def create_physicist_agent():
"""Create the Physicist agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Physicist agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name=PHYSICIST_AGENT_NAME,
instructions="You are an expert in physics. You answer questions from a physics perspective.",
)
def create_chemist_agent():
"""Create the Chemist agent using Azure OpenAI.
Returns:
AgentProtocol: The configured Chemist agent
"""
return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent(
name=CHEMIST_AGENT_NAME,
instructions="You are an expert in chemistry. You answer questions from a chemistry perspective.",
)
def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt: str) -> Generator[Task[Any], Any, dict[str, str]]:
"""Orchestration that runs both agents in parallel and aggregates results.
Uses DurableAIAgentOrchestrationContext to wrap the orchestration context and
access agents via the OrchestrationAgentExecutor.
Args:
context: The orchestration context
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.info(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.info("[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.info("[Orchestration] Both agents completed")
# Extract results from the tasks - DurableAgentTask yields AgentRunResponse
physicist_result: AgentRunResponse = task_results[0]
chemist_result: AgentRunResponse = task_results[1]
result = {
"physicist": physicist_result.text,
"chemist": chemist_result.text,
}
logger.info(f"[Orchestration] Aggregated results ready")
return result
async def main():
"""Main entry point for the worker process."""
logger.info("Starting Durable Task Multi-Agent Worker with Orchestration...")
# Get environment variables for taskhub and endpoint with defaults
taskhub_name = os.getenv("TASKHUB", "default")
endpoint = os.getenv("ENDPOINT", "http://localhost:8080")
logger.info(f"Using taskhub: {taskhub_name}")
logger.info(f"Using endpoint: {endpoint}")
# Set credential to None for emulator, or DefaultAzureCredential for Azure
credential = None if endpoint == "http://localhost:8080" else DefaultAzureCredential()
# Create a worker using Azure Managed Durable Task
worker = DurableTaskSchedulerWorker(
host_address=endpoint,
secure_channel=endpoint != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential
)
# Wrap it with the agent worker
agent_worker = DurableAIAgentWorker(worker)
# Create and register both agents
logger.info("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.info(f"✓ Registered agent: {physicist_agent.name}")
logger.info(f" Entity name: dafx-{physicist_agent.name}")
logger.info(f"✓ Registered agent: {chemist_agent.name}")
logger.info(f" Entity name: dafx-{chemist_agent.name}")
logger.info("")
# Register the orchestration function
logger.info("Registering orchestration function...")
worker.add_orchestrator(multi_agent_concurrent_orchestration)
logger.info(f"✓ Registered orchestration: {multi_agent_concurrent_orchestration.__name__}")
logger.info("")
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.info("Worker shutdown initiated")
logger.info("Worker stopped")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,124 @@
# Durable Task Samples
This directory contains samples for durable agent hosting using the Durable Task Scheduler. These samples demonstrate the worker-client architecture pattern, enabling distributed agent execution with persistent conversation state.
- **[01_single_agent](01_single_agent/)**: A sample that demonstrates how to host a single conversational agent using the Durable Task Scheduler and interact with it via a client.
- **[04_single_agent_orchestration_chaining](04_single_agent_orchestration_chaining/)**: A sample that demonstrates how to chain multiple invocations of the same agent using a durable orchestration.
- **[05_multi_agent_orchestration_concurrency](05_multi_agent_orchestration_concurrency/)**: A sample that demonstrates how to host multiple agents and run them concurrently using a durable orchestration.
## Running the Samples
These samples are designed to be run locally in a cloned repository.
### Prerequisites
The following prerequisites are required to run the samples:
- [Python 3.9 or later](https://www.python.org/downloads/)
- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed and authenticated (`az login`) or an API key for the Azure OpenAI service
- [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource) with a deployed model (gpt-4o-mini or better is recommended)
- [Durable Task Scheduler](https://learn.microsoft.com/azure/azure-functions/durable/durable-task-scheduler/develop-with-durable-task-scheduler) (local emulator or Azure-hosted)
- [Docker](https://docs.docker.com/get-docker/) installed if running the Durable Task Scheduler emulator locally
### Configuring RBAC Permissions for Azure OpenAI
These samples are configured to use the Azure OpenAI service with RBAC permissions to access the model. You'll need to configure the RBAC permissions for the Azure OpenAI service to allow the Python app to access the model.
Below is an example of how to configure the RBAC permissions for the Azure OpenAI service to allow the current user to access the model.
Bash (Linux/macOS/WSL):
```bash
az role assignment create \
--assignee "yourname@contoso.com" \
--role "Cognitive Services OpenAI User" \
--scope /subscriptions/<your-subscription-id>/resourceGroups/<your-resource-group-name>/providers/Microsoft.CognitiveServices/accounts/<your-openai-resource-name>
```
PowerShell:
```powershell
az role assignment create `
--assignee "yourname@contoso.com" `
--role "Cognitive Services OpenAI User" `
--scope /subscriptions/<your-subscription-id>/resourceGroups/<your-resource-group-name>/providers/Microsoft.CognitiveServices/accounts/<your-openai-resource-name>
```
More information on how to configure RBAC permissions for Azure OpenAI can be found in the [Azure OpenAI documentation](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource?pivots=cli).
### Setting an API key for the Azure OpenAI service
As an alternative to configuring Azure RBAC permissions, you can set an API key for the Azure OpenAI service by setting the `AZURE_OPENAI_API_KEY` environment variable.
Bash (Linux/macOS/WSL):
```bash
export AZURE_OPENAI_API_KEY="your-api-key"
```
PowerShell:
```powershell
$env:AZURE_OPENAI_API_KEY="your-api-key"
```
### Start Durable Task Scheduler
Most samples use the Durable Task Scheduler (DTS) to support hosted agents and durable orchestrations. DTS also allows you to view the status of orchestrations and their inputs and outputs from a web UI.
To run the Durable Task Scheduler locally, you can use the following `docker` command:
```bash
docker run -d --name dts-emulator -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
```
The DTS dashboard will be available at `http://localhost:8082`.
### Environment Configuration
Each sample reads configuration from environment variables. You'll need to set the following environment variables:
```bash
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/"
export AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="your-deployment-name"
```
### Installing Dependencies
Navigate to the sample directory and install dependencies:
```bash
cd samples/getting_started/durabletask/01_single_agent
pip install -r requirements.txt
```
### Running the Samples
Each sample follows a worker-client architecture. Most samples provide separate `worker.py` and `client.py` files, though some include a combined `sample.py` for convenience.
**Running with separate worker and client:**
In one terminal, start the worker:
```bash
python worker.py
```
In another terminal, run the client:
```bash
python client.py
```
**Running with combined sample:**
```bash
python sample.py
```
### Viewing the Sample Output
The sample output is displayed directly in the terminal where you ran the Python script. Agent responses are printed to stdout with log formatting for better readability.
You can also see the state of agents and orchestrations in the Durable Task Scheduler dashboard at `http://localhost:8082`.