mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add load_dotenv() to samples for .env file support (#4043)
* Initial plan * Add load_dotenv() to 303 Python samples for environment variable loading Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Update SAMPLE_GUIDELINES.md to document load_dotenv() requirement Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Update samples README.md to document .env file usage Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Run ruff format on all changed sample files Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Clarify load_dotenv() usage in README - local dev vs production Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove deprecated getting_started folder as requested Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Document env_file_path parameter for per-client configuration Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Merge main branch to resolve conflicts Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix run_evaluation.py file that was empty in merge commit Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove dotnet changes from merge - out of scope for this PR Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove package and test changes from merge - only sample changes needed Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove test_func_utils.py - only sample changes needed Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Revert sample files not in original changeset - keep only load_dotenv additions Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Move load_dotenv() outside snippet tag in 06_host_your_agent.py Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix comment placement - move load_dotenv before code comments Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix load_dotenv() placement across all samples - after docstring, before code comments Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Merge latest main branch with load_dotenv changes Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Remove non-sample changes from merge - keep only load_dotenv additions Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Revert non-load_dotenv sample changes from merge Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Fix run_evaluation.py - use main's improved version (file already had load_dotenv) Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> * Manual update * Manual update 2 * Fix Role usage and load_dotenv placement per PR review feedback Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Fix Role usage - use string literals not enum attributes Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Fix SAMPLE_GUIDELINES.md example - load_dotenv before docstring per guidance Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Move load_dotenv() before docstrings in all samples per SAMPLE_GUIDELINES ordering Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Address PR review: rename files, fix placement, add session usage, remove note Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> * Update Redis README to reference renamed file redis_history_provider.py Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com> Co-authored-by: Tao Chen <taochen@microsoft.com> Co-authored-by: eavanvalkenburg <13749212+eavanvalkenburg@users.noreply.github.com> Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
3ea9c5fa5d
commit
b05fc9e849
@@ -18,17 +18,19 @@ import os
|
||||
|
||||
from agent_framework.azure import DurableAIAgentClient
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableAIAgentClient:
|
||||
"""Create a configured DurableAIAgentClient.
|
||||
|
||||
@@ -53,7 +55,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
|
||||
@@ -18,8 +18,12 @@ import os
|
||||
from agent_framework import Agent
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.WARNING)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,9 +42,7 @@ def create_joker_agent() -> Agent:
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -65,7 +67,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -19,17 +19,19 @@ import os
|
||||
|
||||
from agent_framework.azure import DurableAIAgentClient
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableAIAgentClient:
|
||||
"""Create a configured DurableAIAgentClient.
|
||||
|
||||
@@ -54,7 +56,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
|
||||
@@ -20,8 +20,12 @@ from typing import Any
|
||||
from agent_framework import tool
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,9 +22,13 @@ from datetime import timedelta
|
||||
import redis.asyncio as aioredis
|
||||
from agent_framework.azure import DurableAIAgentClient
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
|
||||
from redis_stream_response_handler import RedisStreamResponseHandler
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,9 +58,7 @@ async def get_stream_handler() -> RedisStreamResponseHandler:
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableAIAgentClient:
|
||||
"""Create a configured DurableAIAgentClient.
|
||||
|
||||
@@ -81,7 +83,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
return DurableAIAgentClient(dts_client)
|
||||
@@ -106,7 +108,9 @@ async def stream_from_redis(thread_id: str, cursor: str | None = None) -> None:
|
||||
chunk_count = 0
|
||||
async for chunk in stream_handler.read_stream(thread_id, cursor):
|
||||
chunk_count += 1
|
||||
logger.debug(f"Received chunk #{chunk_count}: error={chunk.error}, is_done={chunk.is_done}, text_len={len(chunk.text) if chunk.text else 0}")
|
||||
logger.debug(
|
||||
f"Received chunk #{chunk_count}: error={chunk.error}, is_done={chunk.is_done}, text_len={len(chunk.text) if chunk.text else 0}"
|
||||
)
|
||||
|
||||
if chunk.error:
|
||||
logger.error(f"Stream error: {chunk.error}")
|
||||
@@ -175,9 +179,6 @@ def run_client(agent_client: DurableAIAgentClient) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Create the client
|
||||
client = get_client()
|
||||
|
||||
|
||||
+7
-4
@@ -25,6 +25,7 @@ class StreamChunk:
|
||||
is_done: Whether this is the final chunk in the stream.
|
||||
error: Error message if an error occurred, otherwise None.
|
||||
"""
|
||||
|
||||
entry_id: str
|
||||
text: str | None = None
|
||||
is_done: bool = False
|
||||
@@ -60,7 +61,9 @@ class RedisStreamResponseHandler:
|
||||
"""Enter async context manager."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object) -> None:
|
||||
async def __aexit__(
|
||||
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object
|
||||
) -> None:
|
||||
"""Exit async context manager and close Redis connection."""
|
||||
await self._redis.aclose()
|
||||
|
||||
@@ -84,7 +87,7 @@ class RedisStreamResponseHandler:
|
||||
"text": text,
|
||||
"sequence": str(sequence),
|
||||
"timestamp": str(int(time.time() * 1000)),
|
||||
}
|
||||
},
|
||||
)
|
||||
await self._redis.expire(stream_key, self._stream_ttl)
|
||||
|
||||
@@ -107,7 +110,7 @@ class RedisStreamResponseHandler:
|
||||
"sequence": str(sequence),
|
||||
"timestamp": str(int(time.time() * 1000)),
|
||||
"done": "true",
|
||||
}
|
||||
},
|
||||
)
|
||||
await self._redis.expire(stream_key, self._stream_ttl)
|
||||
|
||||
@@ -152,7 +155,7 @@ class RedisStreamResponseHandler:
|
||||
timeout_seconds = self.MAX_EMPTY_READS * self.POLL_INTERVAL_MS / 1000
|
||||
yield StreamChunk(
|
||||
entry_id=start_id,
|
||||
error=f"Stream not found or timed out after {timeout_seconds} seconds"
|
||||
error=f"Stream not found or timed out after {timeout_seconds} seconds",
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
In a real application, these would call actual weather and events APIs.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
|
||||
@@ -26,10 +26,14 @@ from agent_framework.azure import (
|
||||
DurableAIAgentWorker,
|
||||
)
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from redis_stream_response_handler import RedisStreamResponseHandler
|
||||
from tools import get_local_events, get_weather_forecast
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -169,9 +173,7 @@ to make the itinerary easy to scan and visually appealing.""",
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -196,7 +198,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+4
-9
@@ -27,9 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerClient:
|
||||
"""Create a configured DurableTaskSchedulerClient.
|
||||
|
||||
@@ -54,7 +52,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +65,7 @@ def run_client(client: DurableTaskSchedulerClient) -> None:
|
||||
logger.debug("Starting single agent chaining orchestration...")
|
||||
|
||||
# Start the orchestration
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="single_agent_chaining_orchestration",
|
||||
input="",
|
||||
)
|
||||
@@ -76,10 +74,7 @@ def run_client(client: DurableTaskSchedulerClient) -> None:
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
|
||||
# Retrieve the final state
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=300
|
||||
)
|
||||
metadata = client.wait_for_orchestration_completion(instance_id=instance_id, timeout=300)
|
||||
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
|
||||
+8
-9
@@ -20,9 +20,13 @@ from collections.abc import Generator
|
||||
from agent_framework import Agent, AgentResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import OrchestrationContext, Task
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -102,10 +106,7 @@ def single_agent_chaining_orchestration(
|
||||
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}"
|
||||
)
|
||||
improved_prompt = f"Improve this further while keeping it under 25 words: {initial_response.text}"
|
||||
|
||||
logger.info("[Orchestration] Second agent run: Refining the sentence: %s", improved_prompt)
|
||||
refined_response = yield writer.run(
|
||||
@@ -120,9 +121,7 @@ def single_agent_chaining_orchestration(
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -147,7 +146,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -172,7 +171,7 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(single_agent_chaining_orchestration) # type: ignore
|
||||
worker.add_orchestrator(single_agent_chaining_orchestration) # type: ignore
|
||||
logger.debug(f"✓ Registered orchestration: {single_agent_chaining_orchestration.__name__}")
|
||||
|
||||
return agent_worker
|
||||
|
||||
+3
-5
@@ -27,9 +27,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerClient:
|
||||
"""Create a configured DurableTaskSchedulerClient.
|
||||
|
||||
@@ -54,7 +52,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -66,7 +64,7 @@ def run_client(client: DurableTaskSchedulerClient, prompt: str = "What is temper
|
||||
prompt: The prompt to send to both agents
|
||||
"""
|
||||
# Start the orchestration with the prompt as input
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="multi_agent_concurrent_orchestration",
|
||||
input=prompt,
|
||||
)
|
||||
|
||||
+13
-7
@@ -21,9 +21,13 @@ from typing import Any
|
||||
from agent_framework import Agent, AgentResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import OrchestrationContext, Task, when_all
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,7 +61,9 @@ def create_chemist_agent() -> "Agent":
|
||||
)
|
||||
|
||||
|
||||
def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt: str) -> Generator[Task[Any], Any, dict[str, str]]:
|
||||
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
|
||||
@@ -84,7 +90,9 @@ def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt:
|
||||
physicist_session = physicist.create_session()
|
||||
chemist_session = chemist.create_session()
|
||||
|
||||
logger.debug(f"[Orchestration] Created sessions - Physicist: {physicist_session.session_id}, Chemist: {chemist_session.session_id}")
|
||||
logger.debug(
|
||||
f"[Orchestration] Created sessions - Physicist: {physicist_session.session_id}, Chemist: {chemist_session.session_id}"
|
||||
)
|
||||
|
||||
# Create tasks from agent.run() calls - these return DurableAgentTask instances
|
||||
physicist_task = physicist.run(messages=str(prompt), session=physicist_session)
|
||||
@@ -112,9 +120,7 @@ def multi_agent_concurrent_orchestration(context: OrchestrationContext, prompt:
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -139,7 +145,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -167,7 +173,7 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(multi_agent_concurrent_orchestration) # type: ignore
|
||||
worker.add_orchestrator(multi_agent_concurrent_orchestration) # type: ignore
|
||||
logger.debug(f"✓ Registered orchestration: {multi_agent_concurrent_orchestration.__name__}")
|
||||
|
||||
return agent_worker
|
||||
|
||||
+7
-12
@@ -25,9 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerClient:
|
||||
"""Create a configured DurableTaskSchedulerClient.
|
||||
|
||||
@@ -52,14 +50,14 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
def run_client(
|
||||
client: DurableTaskSchedulerClient,
|
||||
email_id: str = "email-001",
|
||||
email_content: str = "Hello! I wanted to reach out about our upcoming project meeting."
|
||||
email_content: str = "Hello! I wanted to reach out about our upcoming project meeting.",
|
||||
) -> None:
|
||||
"""Run client to start and monitor the spam detection orchestration.
|
||||
|
||||
@@ -76,7 +74,7 @@ def run_client(
|
||||
logger.debug("Starting spam detection orchestration...")
|
||||
|
||||
# Start the orchestration with the email payload
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="spam_detection_orchestration",
|
||||
input=payload,
|
||||
)
|
||||
@@ -85,10 +83,7 @@ def run_client(
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
|
||||
# Retrieve the final state
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=300
|
||||
)
|
||||
metadata = client.wait_for_orchestration_completion(instance_id=instance_id, timeout=300)
|
||||
|
||||
if metadata and metadata.runtime_status.name == "COMPLETED":
|
||||
result = metadata.serialized_output
|
||||
@@ -124,7 +119,7 @@ async def main() -> None:
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-001",
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week."
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week.",
|
||||
)
|
||||
|
||||
# Test with a spam email
|
||||
@@ -133,7 +128,7 @@ async def main() -> None:
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-002",
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+3
-6
@@ -25,10 +25,7 @@ from client import get_client, run_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
force=True
|
||||
)
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
@@ -57,7 +54,7 @@ def main():
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-001",
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week."
|
||||
email_content="Hello! I wanted to reach out about our upcoming project meeting scheduled for next week.",
|
||||
)
|
||||
|
||||
# Test 2: Spam email
|
||||
@@ -66,7 +63,7 @@ def main():
|
||||
run_client(
|
||||
client,
|
||||
email_id="email-002",
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!"
|
||||
email_content="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer! Don't miss out!",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
+10
-5
@@ -21,10 +21,14 @@ from typing import Any, cast
|
||||
from agent_framework import Agent, AgentResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,17 +40,20 @@ EMAIL_AGENT_NAME = "EmailAssistantAgent"
|
||||
|
||||
class SpamDetectionResult(BaseModel):
|
||||
"""Result from spam detection agent."""
|
||||
|
||||
is_spam: bool
|
||||
reason: str
|
||||
|
||||
|
||||
class EmailResponse(BaseModel):
|
||||
"""Result from email assistant agent."""
|
||||
|
||||
response: str
|
||||
|
||||
|
||||
class EmailPayload(BaseModel):
|
||||
"""Input payload for the orchestration."""
|
||||
|
||||
email_id: str
|
||||
email_content: str
|
||||
|
||||
@@ -195,9 +202,7 @@ def spam_detection_orchestration(context: OrchestrationContext, payload_raw: Any
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -222,7 +227,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -257,7 +262,7 @@ def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
|
||||
# Register the orchestration function
|
||||
logger.debug("Registering orchestration function...")
|
||||
worker.add_orchestrator(spam_detection_orchestration) # type: ignore[arg-type]
|
||||
worker.add_orchestrator(spam_detection_orchestration) # type: ignore[arg-type]
|
||||
logger.debug(f"✓ Registered orchestration: {spam_detection_orchestration.__name__}")
|
||||
|
||||
return agent_worker
|
||||
|
||||
+10
-35
@@ -31,9 +31,7 @@ HUMAN_APPROVAL_EVENT = "HumanApproval"
|
||||
|
||||
|
||||
def get_client(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerClient:
|
||||
"""Create a configured DurableTaskSchedulerClient.
|
||||
|
||||
@@ -58,7 +56,7 @@ def get_client(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -90,11 +88,7 @@ def _log_completion_result(
|
||||
logger.error("Orchestration did not complete within the timeout period")
|
||||
|
||||
|
||||
def _wait_and_log_completion(
|
||||
client: DurableTaskSchedulerClient,
|
||||
instance_id: str,
|
||||
timeout: int = 60
|
||||
) -> None:
|
||||
def _wait_and_log_completion(client: DurableTaskSchedulerClient, instance_id: str, timeout: int = 60) -> None:
|
||||
"""Wait for orchestration completion and log the result.
|
||||
|
||||
Args:
|
||||
@@ -103,20 +97,12 @@ def _wait_and_log_completion(
|
||||
timeout: Maximum time to wait for completion in seconds
|
||||
"""
|
||||
logger.debug("Waiting for orchestration to complete...")
|
||||
metadata = client.wait_for_orchestration_completion(
|
||||
instance_id=instance_id,
|
||||
timeout=timeout
|
||||
)
|
||||
metadata = client.wait_for_orchestration_completion(instance_id=instance_id, timeout=timeout)
|
||||
|
||||
_log_completion_result(metadata)
|
||||
|
||||
|
||||
def send_approval(
|
||||
client: DurableTaskSchedulerClient,
|
||||
instance_id: str,
|
||||
approved: bool,
|
||||
feedback: str = ""
|
||||
) -> None:
|
||||
def send_approval(client: DurableTaskSchedulerClient, instance_id: str, approved: bool, feedback: str = "") -> None:
|
||||
"""Send approval or rejection event to the orchestration.
|
||||
|
||||
Args:
|
||||
@@ -125,30 +111,19 @@ def send_approval(
|
||||
approved: Whether to approve or reject
|
||||
feedback: Optional feedback message (used when rejected)
|
||||
"""
|
||||
approval_data = {
|
||||
"approved": approved,
|
||||
"feedback": feedback
|
||||
}
|
||||
approval_data = {"approved": approved, "feedback": feedback}
|
||||
|
||||
logger.debug(f"Sending {'APPROVAL' if approved else 'REJECTION'} to instance {instance_id}")
|
||||
if feedback:
|
||||
logger.debug(f"Feedback: {feedback}")
|
||||
|
||||
# Raise the external event
|
||||
client.raise_orchestration_event(
|
||||
instance_id=instance_id,
|
||||
event_name=HUMAN_APPROVAL_EVENT,
|
||||
data=approval_data
|
||||
)
|
||||
client.raise_orchestration_event(instance_id=instance_id, event_name=HUMAN_APPROVAL_EVENT, data=approval_data)
|
||||
|
||||
logger.debug("Event sent successfully")
|
||||
|
||||
|
||||
def wait_for_notification(
|
||||
client: DurableTaskSchedulerClient,
|
||||
instance_id: str,
|
||||
timeout_seconds: int = 10
|
||||
) -> bool:
|
||||
def wait_for_notification(client: DurableTaskSchedulerClient, instance_id: str, timeout_seconds: int = 10) -> bool:
|
||||
"""Wait for the orchestration to reach a notification point.
|
||||
|
||||
Polls the orchestration status until it appears to be waiting for approval.
|
||||
@@ -226,14 +201,14 @@ def run_interactive_client(client: DurableTaskSchedulerClient) -> None:
|
||||
payload = {
|
||||
"topic": topic,
|
||||
"max_review_attempts": max_review_attempts,
|
||||
"approval_timeout_seconds": approval_timeout_seconds
|
||||
"approval_timeout_seconds": approval_timeout_seconds,
|
||||
}
|
||||
|
||||
logger.debug(f"Configuration: Topic={topic}, Max attempts={max_review_attempts}, Timeout={timeout_hours}h")
|
||||
|
||||
# Start the orchestration
|
||||
logger.debug("Starting content generation orchestration...")
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
instance_id = client.schedule_new_orchestration( # type: ignore
|
||||
orchestrator="content_generation_hitl_orchestration",
|
||||
input=payload,
|
||||
)
|
||||
|
||||
@@ -25,10 +25,7 @@ from client import get_client, run_interactive_client
|
||||
from dotenv import load_dotenv
|
||||
from worker import get_worker, setup_worker
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
force=True
|
||||
)
|
||||
logging.basicConfig(level=logging.INFO, force=True)
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
|
||||
+20
-24
@@ -22,10 +22,14 @@ from typing import Any, cast
|
||||
from agent_framework import Agent, AgentResponse
|
||||
from agent_framework.azure import AzureOpenAIChatClient, DurableAIAgentOrchestrationContext, DurableAIAgentWorker
|
||||
from azure.identity import AzureCliCredential, DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from durabletask.task import ActivityContext, OrchestrationContext, Task, when_any # type: ignore
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,6 +41,7 @@ HUMAN_APPROVAL_EVENT = "HumanApproval"
|
||||
|
||||
class ContentGenerationInput(BaseModel):
|
||||
"""Input for content generation orchestration."""
|
||||
|
||||
topic: str
|
||||
max_review_attempts: int = 3
|
||||
approval_timeout_seconds: float = 300 # 5 minutes for demo (72 hours in production)
|
||||
@@ -44,12 +49,14 @@ class ContentGenerationInput(BaseModel):
|
||||
|
||||
class GeneratedContent(BaseModel):
|
||||
"""Structured output from writer agent."""
|
||||
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
class HumanApproval(BaseModel):
|
||||
"""Human approval decision."""
|
||||
|
||||
approved: bool
|
||||
feedback: str = ""
|
||||
|
||||
@@ -103,8 +110,7 @@ def publish_content(context: ActivityContext, content: dict[str, str]) -> str:
|
||||
|
||||
|
||||
def content_generation_hitl_orchestration(
|
||||
context: OrchestrationContext,
|
||||
payload_raw: Any
|
||||
context: OrchestrationContext, payload_raw: Any
|
||||
) -> Generator[Task[Any], Any, dict[str, str]]:
|
||||
"""Human-in-the-loop orchestration for content generation with approval workflow.
|
||||
|
||||
@@ -160,7 +166,7 @@ def content_generation_hitl_orchestration(
|
||||
initial_response: AgentResponse = yield writer.run(
|
||||
messages=f"Write a short article about '{payload.topic}'.",
|
||||
session=writer_session,
|
||||
options={"response_format": GeneratedContent},
|
||||
options={"response_format": GeneratedContent},
|
||||
)
|
||||
content = cast(GeneratedContent, initial_response.value)
|
||||
|
||||
@@ -175,13 +181,12 @@ def content_generation_hitl_orchestration(
|
||||
attempt += 1
|
||||
logger.debug(f"[Orchestration] Review iteration #{attempt}/{payload.max_review_attempts}")
|
||||
|
||||
context.set_custom_status(f"Requesting human feedback (Attempt {attempt}, timeout {payload.approval_timeout_seconds}s)")
|
||||
context.set_custom_status(
|
||||
f"Requesting human feedback (Attempt {attempt}, timeout {payload.approval_timeout_seconds}s)"
|
||||
)
|
||||
|
||||
# Notify user for approval
|
||||
yield context.call_activity(
|
||||
"notify_user_for_approval",
|
||||
input=content.model_dump()
|
||||
)
|
||||
yield context.call_activity("notify_user_for_approval", input=content.model_dump())
|
||||
|
||||
logger.debug("[Orchestration] Waiting for human approval or timeout...")
|
||||
|
||||
@@ -217,16 +222,13 @@ def content_generation_hitl_orchestration(
|
||||
else:
|
||||
approval = HumanApproval(approved=False, feedback=approval_data)
|
||||
else:
|
||||
approval = HumanApproval(approved=False, feedback=str(approval_data)) # type: ignore
|
||||
approval = HumanApproval(approved=False, feedback=str(approval_data)) # type: ignore
|
||||
|
||||
if approval.approved:
|
||||
# Content approved - publish and return
|
||||
logger.debug("[Orchestration] Content approved! Publishing...")
|
||||
context.set_custom_status("Content approved by human reviewer. Publishing...")
|
||||
publish_task: Task[Any] = context.call_activity(
|
||||
"publish_content",
|
||||
input=content.model_dump()
|
||||
)
|
||||
publish_task: Task[Any] = context.call_activity("publish_content", input=content.model_dump())
|
||||
yield publish_task
|
||||
|
||||
logger.debug("[Orchestration] Content published successfully")
|
||||
@@ -256,7 +258,7 @@ def content_generation_hitl_orchestration(
|
||||
rewrite_response: AgentResponse = yield writer.run(
|
||||
messages=rewrite_prompt,
|
||||
session=writer_session,
|
||||
options={"response_format": GeneratedContent},
|
||||
options={"response_format": GeneratedContent},
|
||||
)
|
||||
rewritten_content = cast(GeneratedContent, rewrite_response.value)
|
||||
|
||||
@@ -270,21 +272,15 @@ def content_generation_hitl_orchestration(
|
||||
# Timeout occurred
|
||||
logger.error(f"[Orchestration] Approval timeout after {payload.approval_timeout_seconds}s")
|
||||
|
||||
raise TimeoutError(
|
||||
f"Human approval timed out after {payload.approval_timeout_seconds} second(s)."
|
||||
)
|
||||
raise TimeoutError(f"Human approval timed out after {payload.approval_timeout_seconds} second(s).")
|
||||
|
||||
# If we exit the loop without returning, max attempts were exhausted
|
||||
context.set_custom_status("Max review attempts exhausted.")
|
||||
raise RuntimeError(
|
||||
f"Content could not be approved after {payload.max_review_attempts} iteration(s)."
|
||||
)
|
||||
raise RuntimeError(f"Content could not be approved after {payload.max_review_attempts} iteration(s).")
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None,
|
||||
endpoint: str | None = None,
|
||||
log_handler: logging.Handler | None = None
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker.
|
||||
|
||||
@@ -309,7 +305,7 @@ def get_worker(
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user