chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# Streaming Workflow Progress on a Standalone Durable Task Worker
|
||||
|
||||
This sample demonstrates **streaming a durable workflow's progress** from a
|
||||
standalone Durable Task worker — no Azure Functions required. It is the
|
||||
streaming counterpart to [`08_workflow`](../08_workflow/README.md).
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- The async `DurableWorkflowClient` API:
|
||||
- `run_workflow(input, wait=False)` — start a workflow without blocking and get
|
||||
its instance id.
|
||||
- `stream_workflow(instance_id)` — an async iterator that yields typed
|
||||
`WorkflowEvent` objects (`executor_invoked` / `executor_completed` / `output`
|
||||
/ ...) as the workflow progresses, ending when it reaches a terminal state.
|
||||
Each event's `data` is already reconstructed into its original typed object,
|
||||
so the client never deserializes anything by hand.
|
||||
- `await_workflow_output(instance_id)` — read the final reconstructed output.
|
||||
- **Brokerless streaming.** The orchestrator publishes accumulated events to the
|
||||
orchestration **custom status** after each superstep (only on live execution,
|
||||
not replay), and the client streams them by polling. No Redis or other message
|
||||
broker is required.
|
||||
- **Per-executor granularity.** Events fire per executor and per yielded output,
|
||||
not at the token level. Non-agent executors carry their captured event data;
|
||||
agent executors surface coarse `executor_invoked` / `executor_completed`
|
||||
lifecycle events. (Token-level streaming through a durable boundary would
|
||||
require an external broker.)
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) in the parent directory for environment setup.
|
||||
|
||||
This sample uses Azure AI Foundry credentials:
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT`
|
||||
- `FOUNDRY_MODEL`
|
||||
|
||||
It also needs a Durable Task Scheduler. For local development, start the
|
||||
emulator (defaults to `http://localhost:8080`):
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
Start the worker in one terminal:
|
||||
|
||||
```bash
|
||||
cd samples/04-hosting/durabletask/10_workflow_streaming
|
||||
python worker.py
|
||||
```
|
||||
|
||||
In a second terminal, run the client:
|
||||
|
||||
```bash
|
||||
python client.py
|
||||
```
|
||||
|
||||
The workflow is a linear pipeline — `WriterAgent` → `ReviewerAgent` → `publish` —
|
||||
so the client prints the progress events as each executor runs, for example:
|
||||
|
||||
```text
|
||||
Streaming workflow events:
|
||||
[executor_invoked] WriterAgent
|
||||
[executor_completed] WriterAgent
|
||||
[executor_invoked] ReviewerAgent
|
||||
[executor_completed] ReviewerAgent
|
||||
[executor_invoked] publish
|
||||
[executor_completed] publish
|
||||
[output] from publish: Published: ...
|
||||
Final output: Published: ...
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Client that starts the workflow and streams its progress event-by-event.
|
||||
|
||||
The worker (``worker.py``) must be running first. This client demonstrates the
|
||||
async ``DurableWorkflowClient`` API:
|
||||
|
||||
1. ``run_workflow(input, wait=False)`` starts the workflow and returns its
|
||||
instance id without blocking.
|
||||
2. ``stream_workflow(instance_id)`` yields typed ``WorkflowEvent`` objects
|
||||
(``executor_invoked`` / ``executor_completed`` / ``output`` / ...) as the
|
||||
workflow progresses, by polling the orchestration custom status. This is
|
||||
brokerless; each event's ``data`` is already reconstructed into its original
|
||||
typed object, so the client never deserializes anything by hand. Granularity
|
||||
is per executor / per yielded output, not token-level.
|
||||
3. ``await_workflow_output(...)`` returns the final reconstructed output.
|
||||
|
||||
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
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Start a workflow and stream its typed progress events to the console."""
|
||||
client = DurableWorkflowClient(get_client())
|
||||
|
||||
# Start without waiting so we can stream progress as it happens.
|
||||
instance_id = await client.run_workflow(input="Write a short note about durable workflows.", wait=False)
|
||||
logger.info("Started workflow instance: %s", instance_id)
|
||||
|
||||
logger.info("Streaming workflow events:")
|
||||
async for event in client.stream_workflow(instance_id, poll_interval_seconds=1.0):
|
||||
if event.type == "output":
|
||||
logger.info(" [output] from %s: %s", event.executor_id, event.data)
|
||||
else:
|
||||
logger.info(" [%s] %s", event.type, event.executor_id)
|
||||
|
||||
# The stream ends when the workflow reaches a terminal state; read the result.
|
||||
output = await client.await_workflow_output(instance_id)
|
||||
logger.info("Final output: %s", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,142 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Worker that hosts a multi-step MAF Workflow for streaming (no Azure Functions).
|
||||
|
||||
This sample is the streaming counterpart to ``08_workflow``. It hosts a simple
|
||||
linear content pipeline so a client can watch progress event-by-event:
|
||||
|
||||
``writer`` (agent) -> ``reviewer`` (agent) -> ``publish`` (non-agent executor)
|
||||
|
||||
``DurableAIAgentWorker.configure_workflow`` auto-registers a durable entity for
|
||||
each agent executor, a durable activity for each non-agent executor, and the
|
||||
workflow orchestrator. As each executor runs, the orchestrator publishes coarse
|
||||
workflow events (``executor_invoked`` / ``executor_completed`` / ``output``) to
|
||||
the orchestration custom status, which the client streams by polling.
|
||||
|
||||
Prerequisites:
|
||||
- Set ``FOUNDRY_PROJECT_ENDPOINT`` and ``FOUNDRY_MODEL``.
|
||||
- Sign in with Azure CLI (``az login``) for ``AzureCliCredential``.
|
||||
- Start a Durable Task Scheduler (e.g. the DTS emulator on ``localhost:8080``).
|
||||
|
||||
Run the worker (this process), then run ``client.py`` in another process.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
AgentExecutorResponse,
|
||||
Executor,
|
||||
Workflow,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
handler,
|
||||
)
|
||||
from agent_framework.azure import DurableAIAgentWorker
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.identity.aio import AzureCliCredential as AsyncAzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from durabletask.azuremanaged.worker import DurableTaskSchedulerWorker
|
||||
from typing_extensions import Never
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WRITER_AGENT_NAME = "WriterAgent"
|
||||
REVIEWER_AGENT_NAME = "ReviewerAgent"
|
||||
|
||||
WRITER_INSTRUCTIONS = (
|
||||
"You are a concise technical writer. Write a short, single-paragraph draft on the requested topic."
|
||||
)
|
||||
REVIEWER_INSTRUCTIONS = (
|
||||
"You are an editor. Improve the draft you receive for clarity and tone, "
|
||||
"and return only the improved single-paragraph version."
|
||||
)
|
||||
|
||||
|
||||
class PublishExecutor(Executor):
|
||||
"""Non-agent executor that 'publishes' the reviewed draft as the final output."""
|
||||
|
||||
@handler
|
||||
async def publish(self, agent_response: AgentExecutorResponse, ctx: WorkflowContext[Never, str]) -> None:
|
||||
reviewed_text = agent_response.agent_response.text
|
||||
await ctx.yield_output(f"Published:\n{reviewed_text}")
|
||||
|
||||
|
||||
def _create_chat_client() -> FoundryChatClient:
|
||||
"""Create an Azure AI Foundry chat client using AzureCliCredential."""
|
||||
return FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AsyncAzureCliCredential(),
|
||||
)
|
||||
|
||||
|
||||
def create_workflow() -> Workflow:
|
||||
"""Build the linear writer -> reviewer -> publish pipeline."""
|
||||
chat_client = _create_chat_client()
|
||||
|
||||
writer_agent = Agent(client=chat_client, name=WRITER_AGENT_NAME, instructions=WRITER_INSTRUCTIONS)
|
||||
reviewer_agent = Agent(client=chat_client, name=REVIEWER_AGENT_NAME, instructions=REVIEWER_INSTRUCTIONS)
|
||||
publish = PublishExecutor(id="publish")
|
||||
|
||||
return (
|
||||
WorkflowBuilder(start_executor=writer_agent)
|
||||
.add_edge(writer_agent, reviewer_agent)
|
||||
.add_edge(reviewer_agent, publish)
|
||||
.build()
|
||||
)
|
||||
|
||||
|
||||
def get_worker(
|
||||
taskhub: str | None = None, endpoint: str | None = None, log_handler: logging.Handler | None = None
|
||||
) -> DurableTaskSchedulerWorker:
|
||||
"""Create a configured DurableTaskSchedulerWorker."""
|
||||
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 DurableTaskSchedulerWorker(
|
||||
host_address=endpoint_url,
|
||||
secure_channel=endpoint_url != "http://localhost:8080",
|
||||
taskhub=taskhub_name,
|
||||
token_credential=credential,
|
||||
log_handler=log_handler,
|
||||
)
|
||||
|
||||
|
||||
def setup_worker(worker: DurableTaskSchedulerWorker) -> DurableAIAgentWorker:
|
||||
"""Register the workflow (agents + activities + orchestrator) on the worker."""
|
||||
agent_worker = DurableAIAgentWorker(worker)
|
||||
|
||||
workflow = create_workflow()
|
||||
agent_worker.configure_workflow(workflow)
|
||||
logger.info("✓ Configured streaming workflow with %d executors", len(workflow.executors))
|
||||
|
||||
return agent_worker
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Start the worker and block until interrupted."""
|
||||
worker = get_worker()
|
||||
setup_worker(worker)
|
||||
|
||||
logger.info("Worker is ready and listening for work items. Press Ctrl+C to stop.")
|
||||
try:
|
||||
worker.start()
|
||||
while True: # noqa: ASYNC110
|
||||
await asyncio.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Worker shutdown initiated")
|
||||
|
||||
logger.info("Worker stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user