Skip to content

Commit d7bd3a3

Browse files
committed
docs(samples): add standalone durabletask workflow and HITL samples
Add two samples under samples/04-hosting/durabletask demonstrating MAF workflows on a standalone Durable Task worker (no Azure Functions): - 08_workflow: conditional spam-detection workflow started via DurableWorkflowClient.start_workflow / await_workflow_output. - 09_workflow_hitl: content-moderation workflow that pauses with ctx.request_info and is resumed via DurableWorkflowClient.get_pending_hitl_requests / send_hitl_response. Also add the durabletask workflow integration test (test_08_dt_workflow).
1 parent ce9debb commit d7bd3a3

7 files changed

Lines changed: 970 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
"""Integration tests for the standalone durabletask workflow sample (08_workflow).
4+
5+
Exercises the standalone (non-Azure-Functions) workflow path:
6+
- ``DurableAIAgentWorker.configure_workflow`` auto-registers the agent entities,
7+
non-agent executor activities, and the workflow orchestrator.
8+
- A client starts the workflow by scheduling ``WORKFLOW_ORCHESTRATOR_NAME``.
9+
- Conditional routing sends spam to a non-agent handler and legitimate email
10+
through a second agent and a sender executor.
11+
"""
12+
13+
import logging
14+
15+
import pytest
16+
from durabletask.client import OrchestrationStatus
17+
18+
from agent_framework_durabletask import WORKFLOW_ORCHESTRATOR_NAME
19+
20+
logging.basicConfig(level=logging.WARNING)
21+
22+
# Module-level markers
23+
pytestmark = [
24+
pytest.mark.flaky,
25+
pytest.mark.integration,
26+
pytest.mark.sample("08_workflow"),
27+
pytest.mark.integration_test,
28+
pytest.mark.requires_dts,
29+
]
30+
31+
32+
class TestStandaloneWorkflow:
33+
"""Standalone (non-Azure-Functions) workflow execution on a durabletask worker."""
34+
35+
@pytest.fixture(autouse=True)
36+
def setup(self, agent_client_factory: type, orchestration_helper) -> None:
37+
"""Provide a DTS client and orchestration helper for each test."""
38+
self.dts_client, self.agent_client = agent_client_factory.create()
39+
self.orch_helper = orchestration_helper
40+
41+
def test_legitimate_email_drafts_response(self) -> None:
42+
"""A legitimate email routes through the email agent and is 'sent'."""
43+
instance_id = self.dts_client.schedule_new_orchestration(
44+
orchestrator=WORKFLOW_ORCHESTRATOR_NAME,
45+
input=(
46+
"Hi team, just a reminder about our sprint planning meeting tomorrow at 10 AM. "
47+
"Please review the agenda in Jira."
48+
),
49+
)
50+
51+
metadata, output = self.orch_helper.wait_for_orchestration_with_output(
52+
instance_id=instance_id,
53+
timeout=180.0,
54+
)
55+
56+
assert metadata.runtime_status == OrchestrationStatus.COMPLETED
57+
assert output is not None
58+
assert "Email sent" in str(output)
59+
60+
def test_spam_email_handled(self) -> None:
61+
"""A spam email routes to the non-agent spam handler."""
62+
instance_id = self.dts_client.schedule_new_orchestration(
63+
orchestrator=WORKFLOW_ORCHESTRATOR_NAME,
64+
input="URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer!",
65+
)
66+
67+
metadata, output = self.orch_helper.wait_for_orchestration_with_output(
68+
instance_id=instance_id,
69+
timeout=180.0,
70+
)
71+
72+
assert metadata.runtime_status == OrchestrationStatus.COMPLETED
73+
assert output is not None
74+
assert "spam" in str(output).lower()
75+
76+
77+
if __name__ == "__main__":
78+
pytest.main([__file__, "-v"])
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Workflow on a Standalone Durable Task Worker
2+
3+
This sample demonstrates running an agent-framework `Workflow` as a durable
4+
orchestration on a **standalone Durable Task worker** — no Azure Functions
5+
required. It is the durabletask counterpart to the Azure Functions workflow
6+
samples (`samples/04-hosting/azure_functions/10_workflow_no_shared_state`).
7+
8+
## Key Concepts Demonstrated
9+
10+
- Hosting a MAF `Workflow` outside Azure Functions via
11+
`DurableAIAgentWorker.configure_workflow(workflow)`, which auto-registers:
12+
- a durable **entity** for each agent executor,
13+
- a durable **activity** for each non-agent executor, and
14+
- the **workflow orchestrator** (registered as `WORKFLOW_ORCHESTRATOR_NAME`).
15+
- Conditional routing with `add_switch_case_edge_group` (spam vs. legitimate email).
16+
- Mixing AI agents with non-agent executors in one workflow graph.
17+
- Starting the workflow from a client with
18+
`DurableWorkflowClient.start_workflow(input=...)` and reading its result with
19+
`await_workflow_output(instance_id)`.
20+
21+
## Environment Setup
22+
23+
See the [README.md](../README.md) in the parent directory for environment setup.
24+
25+
This sample uses Azure AI Foundry credentials:
26+
27+
- `FOUNDRY_PROJECT_ENDPOINT`
28+
- `FOUNDRY_MODEL`
29+
30+
It also needs a Durable Task Scheduler. For local development, start the
31+
emulator (defaults to `http://localhost:8080`):
32+
33+
```bash
34+
docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
35+
```
36+
37+
## Running the Sample
38+
39+
Start the worker in one terminal:
40+
41+
```bash
42+
cd samples/04-hosting/durabletask/08_workflow
43+
python worker.py
44+
```
45+
46+
In a second terminal, run the client:
47+
48+
```bash
49+
python client.py
50+
```
51+
52+
The client runs two cases:
53+
54+
- **Legitimate email**`SpamDetectionAgent``EmailAssistantAgent`
55+
`email_sender``"Email sent: ..."`.
56+
- **Spam email**`SpamDetectionAgent``spam_handler`
57+
`"Email marked as spam: ..."`.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
"""Client that starts the standalone workflow orchestration and prints the result.
4+
5+
The worker (``worker.py``) must be running first. The workflow is started via
6+
``DurableAIAgentClient.start_workflow`` - which schedules the orchestrator that
7+
``DurableAIAgentWorker.configure_workflow`` auto-registers, so the caller never
8+
needs to know its internal name.
9+
10+
Prerequisites:
11+
- ``worker.py`` running and connected to the same Durable Task Scheduler.
12+
- A Durable Task Scheduler reachable at ``ENDPOINT`` (default ``http://localhost:8080``).
13+
"""
14+
15+
import asyncio
16+
import logging
17+
import os
18+
19+
from agent_framework.azure import DurableWorkflowClient
20+
from azure.identity import AzureCliCredential
21+
from dotenv import load_dotenv
22+
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
23+
24+
load_dotenv()
25+
26+
logging.basicConfig(level=logging.INFO)
27+
logger = logging.getLogger(__name__)
28+
29+
30+
def get_client(taskhub: str | None = None, endpoint: str | None = None) -> DurableTaskSchedulerClient:
31+
"""Create a configured DurableTaskSchedulerClient."""
32+
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
33+
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
34+
35+
credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential()
36+
37+
return DurableTaskSchedulerClient(
38+
host_address=endpoint_url,
39+
secure_channel=endpoint_url != "http://localhost:8080",
40+
taskhub=taskhub_name,
41+
token_credential=credential,
42+
)
43+
44+
45+
def run_workflow(client: DurableWorkflowClient, email_content: str) -> None:
46+
"""Start the workflow with an email and wait for the result."""
47+
instance_id = client.start_workflow(input=email_content)
48+
logger.info("Started workflow instance: %s", instance_id)
49+
50+
output = client.await_workflow_output(instance_id)
51+
logger.info("Workflow output: %s", output)
52+
53+
54+
async def main() -> None:
55+
"""Run the workflow against a legitimate email and a spam email."""
56+
client = DurableWorkflowClient(get_client())
57+
58+
logger.info("TEST 1: Legitimate email")
59+
run_workflow(
60+
client,
61+
"Hi team, just a reminder about our sprint planning meeting tomorrow at 10 AM. "
62+
"Please review the agenda in Jira.",
63+
)
64+
65+
logger.info("TEST 2: Spam email")
66+
run_workflow(
67+
client,
68+
"URGENT! You've won $1,000,000! Click here now to claim your prize! Limited time offer!",
69+
)
70+
71+
72+
if __name__ == "__main__":
73+
asyncio.run(main())

0 commit comments

Comments
 (0)