-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathclient.py
More file actions
131 lines (103 loc) · 4.66 KB
/
Copy pathclient.py
File metadata and controls
131 lines (103 loc) · 4.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# Copyright (c) Microsoft. All rights reserved.
"""Client that drives the standalone HITL workflow to completion.
The worker (``worker.py``) must be running first. This client:
1. Starts the workflow with ``DurableWorkflowClient.start_workflow``.
2. Polls ``get_pending_hitl_requests`` until the workflow pauses for human input.
3. Sends a decision with ``send_hitl_response`` (the request_id correlates the
response back to the paused executor).
4. Reads the final output with ``await_workflow_output``.
It runs two cases: appropriate content (approved) and spammy content (rejected).
Prerequisites:
- ``worker.py`` running and connected to the same Durable Task Scheduler.
- A Durable Task Scheduler reachable at ``ENDPOINT`` (default ``http://localhost:8080``).
"""
import asyncio
import logging
import os
import time
from typing import Any
from agent_framework.azure import DurableWorkflowClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from durabletask.azuremanaged.client import DurableTaskSchedulerClient
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
WORKFLOW_NAME = "content_moderation"
def get_client(taskhub: str | None = None, endpoint: str | None = None) -> DurableTaskSchedulerClient:
"""Create a configured DurableTaskSchedulerClient."""
taskhub_name = taskhub or os.getenv("TASKHUB", "default")
endpoint_url = endpoint or os.getenv("ENDPOINT", "http://localhost:8080")
credential = None if endpoint_url == "http://localhost:8080" else AzureCliCredential()
return DurableTaskSchedulerClient(
host_address=endpoint_url,
secure_channel=endpoint_url != "http://localhost:8080",
taskhub=taskhub_name,
token_credential=credential,
)
def _wait_for_hitl_request(
client: DurableWorkflowClient, instance_id: str, timeout_seconds: int = 60
) -> list[dict[str, Any]]:
"""Poll until the workflow has at least one pending HITL request.
Stops early if the workflow reaches a terminal state (e.g. completed or failed)
without pausing, so a misconfiguration or early failure surfaces the real
status instead of a misleading timeout.
"""
terminal_statuses = {"COMPLETED", "FAILED", "TERMINATED"}
deadline = time.time() + timeout_seconds
while time.time() < deadline:
pending = client.get_pending_hitl_requests(instance_id)
if pending:
return pending
status = client.get_runtime_status(instance_id)
if status in terminal_statuses:
raise RuntimeError(
f"Workflow instance {instance_id} reached terminal state '{status}' before pausing for human input."
)
time.sleep(2)
raise TimeoutError(f"Timed out waiting for a HITL request on instance {instance_id}")
def run_case(client: DurableWorkflowClient, submission: dict[str, Any], *, approve: bool) -> None:
"""Run one moderation case: start, respond to the HITL pause, print the result."""
instance_id = client.start_workflow(input=submission)
logger.info("Started workflow instance: %s", instance_id)
pending = _wait_for_hitl_request(client, instance_id)
request = pending[0]
logger.info("Pending HITL request %s from %s", request["request_id"], request["source_executor_id"])
decision = {
"approved": approve,
"reviewer_notes": "Looks good." if approve else "Violates content policy.",
}
client.send_hitl_response(instance_id, request["request_id"], decision)
logger.info("Sent decision: approved=%s", approve)
output = client.await_workflow_output(instance_id)
logger.info("Workflow output: %s", output)
async def main() -> None:
"""Run an approved case and a rejected case."""
client = DurableWorkflowClient(get_client(), workflow_name=WORKFLOW_NAME)
logger.info("CASE 1: Appropriate content (will approve)")
run_case(
client,
{
"content_id": "article-001",
"title": "Introduction to AI in Healthcare",
"body": (
"Artificial intelligence is improving healthcare by enabling faster diagnosis, "
"personalized treatment plans, and better patient outcomes."
),
"author": "Dr. Jane Smith",
},
approve=True,
)
logger.info("CASE 2: Spammy content (will reject)")
run_case(
client,
{
"content_id": "article-002",
"title": "Get Rich Quick",
"body": "Click here NOW to make $10,000 overnight! GUARANTEED! Limited time offer!",
"author": "Definitely Not Spam",
},
approve=False,
)
if __name__ == "__main__":
asyncio.run(main())