mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
a5b36dc379
commit
e3eff65a6b
@@ -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())
|
||||
+72
@@ -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.
|
||||
|
||||
+104
@@ -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())
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Agent Framework packages (installing from local package until a package is published)
|
||||
-e ../../../../
|
||||
-e ../../../../packages/durabletask
|
||||
|
||||
# Azure authentication
|
||||
azure-identity
|
||||
+255
@@ -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()
|
||||
+167
@@ -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())
|
||||
+75
@@ -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.
|
||||
|
||||
|
||||
+114
@@ -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())
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Agent Framework packages (installing from local package until a package is published)
|
||||
-e ../../../../
|
||||
-e ../../../../packages/durabletask
|
||||
|
||||
# Azure authentication
|
||||
azure-identity
|
||||
+266
@@ -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()
|
||||
+175
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user