Python: (samples): adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs (#3873)

* adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs

* Updates

* add comment
This commit is contained in:
Evan Mattson
2026-02-12 19:46:58 +09:00
committed by GitHub
Unverified
parent 8457533c69
commit 1b10b051fd
73 changed files with 1612 additions and 686 deletions
@@ -38,14 +38,10 @@ Once comfortable with these, explore the rest of the samples below.
| Azure AI Agents (Streaming) | [agents/azure_ai_agents_streaming.py](./agents/azure_ai_agents_streaming.py) | Add Azure AI agents as edges and handle streaming events |
| Azure AI Agents (Shared Thread) | [agents/azure_ai_agents_with_shared_thread.py](./agents/azure_ai_agents_with_shared_thread.py) | Share a common message thread between multiple Azure AI agents in a workflow |
| Custom Agent Executors | [agents/custom_agent_executors.py](./agents/custom_agent_executors.py) | Create executors to handle agent run methods |
| Sequential Workflow as Agent | [agents/sequential_workflow_as_agent.py](./agents/sequential_workflow_as_agent.py) | Build a sequential workflow orchestrating agents, then expose it as a reusable agent |
| Concurrent Workflow as Agent | [agents/concurrent_workflow_as_agent.py](./agents/concurrent_workflow_as_agent.py) | Build a concurrent fan-out/fan-in workflow, then expose it as a reusable agent |
| Magentic Workflow as Agent | [agents/magentic_workflow_as_agent.py](./agents/magentic_workflow_as_agent.py) | Configure Magentic orchestration with callbacks, then expose the workflow as an agent |
| Workflow as Agent (Reflection Pattern) | [agents/workflow_as_agent_reflection_pattern.py](./agents/workflow_as_agent_reflection_pattern.py) | Wrap a workflow so it can behave like an agent (reflection pattern) |
| Workflow as Agent + HITL | [agents/workflow_as_agent_human_in_the_loop.py](./agents/workflow_as_agent_human_in_the_loop.py) | Extend workflow-as-agent with human-in-the-loop capability |
| Workflow as Agent with Thread | [agents/workflow_as_agent_with_thread.py](./agents/workflow_as_agent_with_thread.py) | Use AgentThread to maintain conversation history across workflow-as-agent invocations |
| Workflow as Agent kwargs | [agents/workflow_as_agent_kwargs.py](./agents/workflow_as_agent_kwargs.py) | Pass custom context (data, user tokens) via kwargs through workflow.as_agent() to @ai_function tools |
| Handoff Workflow as Agent | [agents/handoff_workflow_as_agent.py](./agents/handoff_workflow_as_agent.py) | Use a HandoffBuilder workflow as an agent with HITL via FunctionCallContent/FunctionResultContent |
### checkpoint
@@ -54,7 +50,7 @@ Once comfortable with these, explore the rest of the samples below.
| Checkpoint & Resume | [checkpoint/checkpoint_with_resume.py](./checkpoint/checkpoint_with_resume.py) | Create checkpoints, inspect them, and resume execution |
| Checkpoint & HITL Resume | [checkpoint/checkpoint_with_human_in_the_loop.py](./checkpoint/checkpoint_with_human_in_the_loop.py) | Combine checkpointing with human approvals and resume pending HITL requests |
| Checkpointed Sub-Workflow | [checkpoint/sub_workflow_checkpoint.py](./checkpoint/sub_workflow_checkpoint.py) | Save and resume a sub-workflow that pauses for human approval |
| Handoff + Tool Approval Resume | [checkpoint/handoff_with_tool_approval_checkpoint_resume.py](./checkpoint/handoff_with_tool_approval_checkpoint_resume.py) | Handoff workflow that captures tool-call approvals in checkpoints and resumes with human decisions |
| Handoff + Tool Approval Resume | Moved to orchestration samples | Handoff workflow that captures tool-call approvals in checkpoints and resumes with human decisions |
| Workflow as Agent Checkpoint | [checkpoint/workflow_as_agent_checkpoint.py](./checkpoint/workflow_as_agent_checkpoint.py) | Enable checkpointing when using workflow.as_agent() with checkpoint_storage parameter |
### composition
@@ -85,19 +81,13 @@ Once comfortable with these, explore the rest of the samples below.
| Human-In-The-Loop (Guessing Game) | [human-in-the-loop/guessing_game_with_human_input.py](./human-in-the-loop/guessing_game_with_human_input.py) | Interactive request/response prompts with a human via `ctx.request_info()` |
| Agents with Approval Requests in Workflows | [human-in-the-loop/agents_with_approval_requests.py](./human-in-the-loop/agents_with_approval_requests.py) | Agents that create approval requests during workflow execution and wait for human approval to proceed |
| Agents with Declaration-Only Tools | [human-in-the-loop/agents_with_declaration_only_tools.py](./human-in-the-loop/agents_with_declaration_only_tools.py) | Workflow pauses when agent calls a client-side tool (`func=None`), caller supplies the result |
| SequentialBuilder Request Info | [human-in-the-loop/sequential_request_info.py](./human-in-the-loop/sequential_request_info.py) | Request info for agent responses mid-workflow using `.with_request_info()` on SequentialBuilder |
| ConcurrentBuilder Request Info | [human-in-the-loop/concurrent_request_info.py](./human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` on ConcurrentBuilder |
| GroupChatBuilder Request Info | [human-in-the-loop/group_chat_request_info.py](./human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` on GroupChatBuilder |
Builder-oriented request-info samples are maintained in the orchestration sample set
(sequential, concurrent, and group-chat builder variants).
### tool-approval
Tool approval samples demonstrate using `@tool(approval_mode="always_require")` to gate sensitive tool executions with human approval. These work with the high-level builder APIs.
| Sample | File | Concepts |
| ------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| SequentialBuilder Tool Approval | [tool-approval/sequential_builder_tool_approval.py](./tool-approval/sequential_builder_tool_approval.py) | Sequential workflow with tool approval gates for sensitive operations |
| ConcurrentBuilder Tool Approval | [tool-approval/concurrent_builder_tool_approval.py](./tool-approval/concurrent_builder_tool_approval.py) | Concurrent workflow with tool approvals across parallel agents |
| GroupChatBuilder Tool Approval | [tool-approval/group_chat_builder_tool_approval.py](./tool-approval/group_chat_builder_tool_approval.py) | Group chat workflow with tool approval for multi-agent collaboration |
Builder-based tool approval samples are maintained in the orchestration sample set.
### observability
@@ -109,7 +99,8 @@ For additional observability samples in Agent Framework, see the [observability
### orchestration
Orchestration samples (Sequential, Concurrent, Handoff, GroupChat, Magentic) have moved to the dedicated [orchestrations samples directory](../orchestrations/README.md).
Orchestration-focused samples (Sequential, Concurrent, Handoff, GroupChat, Magentic), including builder-based
`workflow.as_agent(...)` variants, are documented in the [orchestrations](../orchestrations/README.md) directory.
### parallelism
@@ -169,9 +160,9 @@ Sequential orchestration uses a few small adapter nodes for plumbing:
### Environment Variables
- **AzureOpenAIChatClient**: Set Azure OpenAI environment variables as documented [here](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/chat_client/README.md#environment-variables).
These variables are required for samples that construct `AzureOpenAIChatClient`
Workflow samples that use `AzureOpenAIResponsesClient` expect:
- **OpenAI** (used in orchestration samples):
- [OpenAIChatClient env vars](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai_chat_client/README.md)
- [OpenAIResponsesClient env vars](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/openai_responses_client/README.md)
- `AZURE_AI_PROJECT_ENDPOINT` (Azure AI Foundry Agent Service (V2) project endpoint)
- `AZURE_AI_MODEL_DEPLOYMENT_NAME` (model deployment name)
These values are passed directly into the client constructor via `os.getenv()` in sample code.
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import AgentResponse, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -14,11 +15,12 @@ This sample creates two agents: a Writer agent creates or edits content, and a R
evaluates and provides feedback.
Purpose:
Show how to create agents from AzureOpenAIChatClient and use them directly in a workflow. Demonstrate
Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate
how agents can be used in a workflow.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming or non-streaming runs.
"""
@@ -27,7 +29,11 @@ Prerequisites:
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer_agent = client.as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
@@ -1,9 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import AgentResponseUpdate, Message, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -13,11 +14,12 @@ This sample creates two agents: a Writer agent creates or edits content, and a R
evaluates and provides feedback.
Purpose:
Show how to create agents from AzureOpenAIChatClient and use them directly in a workflow. Demonstrate
Show how to create agents from AzureOpenAIResponsesClient and use them directly in a workflow. Demonstrate
how agents can be used in a workflow.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
"""
@@ -26,7 +28,11 @@ Prerequisites:
async def main():
"""Build the two node workflow and run it with streaming to observe events."""
# Create the Azure chat client. AzureCliCredential uses your current az login.
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer_agent = client.as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
@@ -1,65 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import AgentResponseUpdate, WorkflowBuilder
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
Sample: Azure AI Agents in a Workflow with Streaming
This sample shows how to create Azure AI Agents and use them in a workflow with streaming.
This sample shows how to create agents backed by Azure OpenAI Responses and use them in a workflow with streaming.
Prerequisites:
- Azure AI Agent Service configured, along with the required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
"""
async def main() -> None:
async with AzureCliCredential() as cred, AzureAIAgentClient(credential=cred) as client:
# Create two agents: a Writer and a Reviewer.
writer_agent = client.as_agent(
name="Writer",
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
reviewer_agent = client.as_agent(
name="Reviewer",
instructions=(
"You are an excellent content reviewer. "
"Provide actionable feedback to the writer about the provided content. "
"Provide the feedback in the most concise manner possible."
),
)
# Create two agents: a Writer and a Reviewer.
writer_agent = client.as_agent(
name="Writer",
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
# Build the workflow by adding agents directly as edges.
# Agents adapt to workflow mode: run(stream=True) for incremental updates, run() for complete responses.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
reviewer_agent = client.as_agent(
name="Reviewer",
instructions=(
"You are an excellent content reviewer. "
"Provide actionable feedback to the writer about the provided content. "
"Provide the feedback in the most concise manner possible."
),
)
# Track the last author to format streaming output.
last_author: str | None = None
# Build the workflow by adding agents directly as edges.
# Agents adapt to workflow mode: run(stream=True) for incremental updates, run() for complete responses.
workflow = WorkflowBuilder(start_executor=writer_agent).add_edge(writer_agent, reviewer_agent).build()
events = workflow.run(
"Create a slogan for a new electric SUV that is affordable and fun to drive.", stream=True
)
async for event in events:
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
if last_author is not None:
print() # Newline between different authors
print(f"{author}: {update.text}", end="", flush=True)
last_author = author
else:
print(update.text, end="", flush=True)
# Track the last author to format streaming output.
last_author: str | None = None
events = workflow.run("Create a slogan for a new electric SUV that is affordable and fun to drive.", stream=True)
async for event in events:
# The outputs of the workflow are whatever the agents produce. So the events are expected to
# contain `AgentResponseUpdate` from the agents in the workflow.
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
update = event.data
author = update.author_name
if author != last_author:
if last_author is not None:
print() # Newline between different authors
print(f"{author}: {update.text}", end="", flush=True)
last_author = author
else:
print(update.text, end="", flush=True)
if __name__ == "__main__":
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
AgentExecutor,
@@ -12,8 +13,8 @@ from agent_framework import (
WorkflowRunState,
executor,
)
from agent_framework.azure import AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
Sample: Agents with a shared thread in a workflow
@@ -28,11 +29,12 @@ Notes:
- Not all agents can share threads; usually only the same type of agents can share threads.
Demonstrate:
- Creating multiple agents with Azure AI Agent Service (V2 API).
- Creating multiple agents with AzureOpenAIResponsesClient.
- Setting up a shared thread between agents.
Prerequisites:
- Azure AI Agent Service configured, along with the required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- AZURE_AI_MODEL_DEPLOYMENT_NAME must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with agents, workflows, and executors in the agent framework.
"""
@@ -51,49 +53,49 @@ async def intercept_agent_response(
async def main() -> None:
async with (
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
writer = await provider.create_agent(
instructions=(
"You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."
),
name="writer",
)
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
reviewer = await provider.create_agent(
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)
writer = client.as_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
shared_thread = writer.get_new_thread()
# Set the message store to store messages in memory.
shared_thread.message_store = ChatMessageStore()
reviewer = client.as_agent(
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)
writer_executor = AgentExecutor(writer, agent_thread=shared_thread)
reviewer_executor = AgentExecutor(reviewer, agent_thread=shared_thread)
shared_thread = writer.get_new_thread()
# Set the message store to store messages in memory.
shared_thread.message_store = ChatMessageStore()
workflow = (
WorkflowBuilder(start_executor=writer_executor)
.add_chain([writer_executor, intercept_agent_response, reviewer_executor])
.build()
)
writer_executor = AgentExecutor(writer, agent_thread=shared_thread)
reviewer_executor = AgentExecutor(reviewer, agent_thread=shared_thread)
result = await workflow.run(
"Write a tagline for a budget-friendly eBike.",
# Keyword arguments will be passed to each agent call.
# Setting store=False to avoid storing messages in the service for this example.
options={"store": False},
)
# The final state should be IDLE since the workflow no longer has messages to
# process after the reviewer agent responds.
assert result.get_final_state() == WorkflowRunState.IDLE
workflow = (
WorkflowBuilder(start_executor=writer_executor)
.add_chain([writer_executor, intercept_agent_response, reviewer_executor])
.build()
)
# The shared thread now contains the conversation between the writer and reviewer. Print it out.
print("=== Shared Thread Conversation ===")
for message in shared_thread.message_store.messages:
print(f"{message.author_name or message.role}: {message.text}")
result = await workflow.run(
"Write a tagline for a budget-friendly eBike.",
# Keyword arguments will be passed to each agent call.
# Setting store=False to avoid storing messages in the service for this example.
options={"store": False},
)
# The final state should be IDLE since the workflow no longer has messages to
# process after the reviewer agent responds.
assert result.get_final_state() == WorkflowRunState.IDLE
# The shared thread now contains the conversation between the writer and reviewer. Print it out.
print("=== Shared Thread Conversation ===")
for message in shared_thread.message_store.messages:
print(f"{message.author_name or message.role}: {message.text}")
if __name__ == "__main__":
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Final
from agent_framework import (
@@ -12,7 +13,7 @@ from agent_framework import (
WorkflowContext,
executor,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -30,7 +31,8 @@ Demonstrates:
- Consuming an AgentExecutorResponse and forwarding an AgentExecutorRequest for the next agent.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Run `az login` before executing.
"""
@@ -94,14 +96,22 @@ async def enrich_with_references(
async def main() -> None:
"""Run the workflow and stream combined updates from both agents."""
# Create the agents
research_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
research_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="research_agent",
instructions=(
"Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'."
),
)
final_editor_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
final_editor_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="final_editor_agent",
instructions=(
"Use all conversation context (including external notes) to produce the final answer. "
@@ -1,9 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import AgentResponseUpdate, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -12,7 +13,8 @@ Sample: AzureOpenAI Chat Agents in a Workflow with Streaming
This sample shows how to create AzureOpenAI Chat Agents and use them in a workflow with streaming.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, and streaming runs.
"""
@@ -21,14 +23,22 @@ Prerequisites:
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the agents
writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
writer_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
name="writer",
)
reviewer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
reviewer_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an excellent content reviewer."
"Provide actionable feedback to the writer about the provided content."
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from dataclasses import dataclass, field
from typing import Annotated
@@ -21,7 +22,7 @@ from agent_framework import (
response_handler,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import Field
from typing_extensions import Never
@@ -43,7 +44,8 @@ Demonstrates:
- Streaming AgentRunUpdateEvent updates alongside human-in-the-loop pauses.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Run `az login` before executing.
"""
@@ -170,7 +172,11 @@ class Coordinator(Executor):
def create_writer_agent() -> Agent:
"""Creates a writer agent with tools."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="writer_agent",
instructions=(
"You are a marketing writer. Call the available tools before drafting copy so you are precise. "
@@ -184,7 +190,11 @@ def create_writer_agent() -> Agent:
def create_final_editor_agent() -> Agent:
"""Creates a final editor agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="final_editor_agent",
instructions=(
"You are an editor who polishes marketing copy after human approval. "
@@ -1,83 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
"""
Sample: Build a concurrent workflow orchestration and wrap it as an agent.
This script wires up a fan-out/fan-in workflow using `ConcurrentBuilder`, and then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
downstream coordinators can reuse the orchestration as a single agent.
Demonstrates:
- Fan-out to multiple agents, fan-in aggregation of final ChatMessages.
- Reusing the orchestrated workflow as an agent entry point with `workflow.as_agent(...)`.
- Workflow completion when idle with no pending work
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- Familiarity with Workflow events (WorkflowEvent with type "output")
"""
def clear_and_redraw(buffers: dict[str, str], agent_order: list[str]) -> None:
"""Clear terminal and redraw all agent outputs grouped together."""
# ANSI escape: clear screen and move cursor to top-left
print("\033[2J\033[H", end="")
print("===== Concurrent Agent Streaming (Live) =====\n")
for name in agent_order:
print(f"--- {name} ---")
print(buffers.get(name, ""))
print()
print("", end="", flush=True)
async def main() -> None:
# 1) Create three domain agents using AzureOpenAIChatClient
client = AzureOpenAIChatClient(credential=AzureCliCredential())
researcher = client.as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
marketer = client.as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
legal = client.as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
),
name="legal",
)
# 2) Build a concurrent workflow
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
# 3) Expose the concurrent workflow as an agent for easy reuse
agent = workflow.as_agent(name="ConcurrentWorkflowAgent")
prompt = "We are launching a new budget-friendly electric bike for urban commuters."
agent_response = await agent.run(prompt)
print("===== Final Aggregated Response =====\n")
for message in agent_response.messages:
# The agent_response contains messages from all participants concatenated
# into a single message.
print(f"{message.author_name}: {message.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
@@ -10,7 +11,7 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -20,14 +21,15 @@ This sample uses two custom executors. A Writer agent creates or edits content,
then hands the conversation to a Reviewer agent which evaluates and finalizes the result.
Purpose:
Show how to wrap chat agents created by AzureOpenAIChatClient inside workflow executors. Demonstrate the @handler
Show how to wrap chat agents created by AzureOpenAIResponsesClient inside workflow executors. Demonstrate the @handler
pattern with typed inputs and typed WorkflowContext[T] outputs, connect executors with the fluent WorkflowBuilder,
and finish by yielding outputs from the terminal node.
Note: When an agent is passed to a workflow, the workflow essenatially wrap the agent in a more sophisticated executor.
Note: When an agent is passed to a workflow, the workflow wraps the agent in a more sophisticated executor.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming or non streaming runs.
"""
@@ -44,8 +46,12 @@ class Writer(Executor):
agent: Agent
def __init__(self, id: str = "writer"):
# Create a domain specific agent using your configured AzureOpenAIChatClient.
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
# Create a domain specific agent using your configured AzureOpenAIResponsesClient.
self.agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
@@ -87,7 +93,11 @@ class Reviewer(Executor):
def __init__(self, id: str = "reviewer"):
# Create a domain specific agent that evaluates and refines content.
self.agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
self.agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an excellent content reviewer. You review the content and provide feedback to the writer."
),
@@ -1,70 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import Agent
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
"""
Sample: Group Chat Orchestration
What it does:
- Demonstrates the generic GroupChatBuilder with a agent orchestrator directing two agents.
- The orchestrator coordinates a researcher (chat completions) and a writer (responses API) to solve a task.
Prerequisites:
- OpenAI environment variables configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
"""
async def main() -> None:
researcher = Agent(
name="Researcher",
description="Collects relevant background information.",
instructions="Gather concise facts that help a teammate answer the question.",
client=OpenAIChatClient(model_id="gpt-4o-mini"),
)
writer = Agent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose clear and structured answers using any notes provided.",
client=OpenAIResponsesClient(),
)
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = GroupChatBuilder(
participants=[researcher, writer],
intermediate_outputs=True,
orchestrator_agent=OpenAIChatClient().as_agent(
name="Orchestrator",
instructions="You coordinate a team conversation to solve the user's task.",
),
).build()
task = "Outline the core considerations for planning a community hackathon, and finish with a concise action plan."
print("\nStarting Group Chat Workflow...\n")
print(f"Input: {task}\n")
try:
workflow_agent = workflow.as_agent(name="GroupChatWorkflowAgent")
agent_result = await workflow_agent.run(task)
if agent_result.messages:
# The output should contain a message from the researcher, a message from the writer,
# and a final synthesized answer from the orchestrator.
print("\n===== as_agent() Transcript =====")
for i, msg in enumerate(agent_result.messages, start=1):
role_value = getattr(msg.role, "value", msg.role)
speaker = msg.author_name or role_value
print(f"{'-' * 50}\n{i:02d} [{speaker}]\n{msg.text}")
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,221 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import (
Agent,
AgentResponse,
Content,
Message,
WorkflowAgent,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
"""Sample: Handoff Workflow as Agent with Human-in-the-Loop.
This sample demonstrates how to use a handoff workflow as an agent, enabling
human-in-the-loop interactions through the agent interface.
A handoff workflow defines a pattern that assembles agents in a mesh topology, allowing
them to transfer control to each other based on the conversation context.
Prerequisites:
- `az login` (Azure CLI authentication)
- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
Key Concepts:
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
for each participant, allowing the coordinator to transfer control to specialists
- Termination condition: Controls when the workflow stops requesting user input
- Request/response cycle: Workflow requests input, user responds, cycle continues
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# See:
# samples/getting_started/tools/function_tool_with_approval.py
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str:
"""Simulated function to process a refund for a given order number."""
return f"Refund processed successfully for order {order_number}."
@tool(approval_mode="never_require")
def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str:
"""Simulated function to check the status of a given order number."""
return f"Order {order_number} is currently being processed and will ship in 2 business days."
@tool(approval_mode="never_require")
def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str:
"""Simulated function to process a return for a given order number."""
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The AzureOpenAIChatClient to use for creating agents.
Returns:
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
"""
# Triage agent: Acts as the frontline dispatcher
triage_agent = client.as_agent(
instructions=(
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
"based on the problem described."
),
name="triage_agent",
)
# Refund specialist: Handles refund requests
refund_agent = client.as_agent(
instructions="You process refund requests.",
name="refund_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_refund],
)
# Order/shipping specialist: Resolves delivery issues
order_agent = client.as_agent(
instructions="You handle order and shipping inquiries.",
name="order_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[check_order_status],
)
# Return specialist: Handles return requests
return_agent = client.as_agent(
instructions="You manage product return requests.",
name="return_agent",
# In a real application, an agent can have multiple tools; here we keep it simple
tools=[process_return],
)
return triage_agent, refund_agent, order_agent, return_agent
def handle_response_and_requests(response: AgentResponse) -> dict[str, HandoffAgentUserRequest]:
"""Process agent response messages and extract any user requests.
This function inspects the agent response and:
- Displays agent messages to the console
- Collects HandoffAgentUserRequest instances for response handling
Args:
response: The AgentResponse from the agent run call.
Returns:
A dictionary mapping request IDs to HandoffAgentUserRequest instances.
"""
pending_requests: dict[str, HandoffAgentUserRequest] = {}
for message in response.messages:
if message.text:
print(f"- {message.author_name or message.role}: {message.text}")
for content in message.contents:
if content.type == "function_call":
if isinstance(content.arguments, dict):
request = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments)
elif isinstance(content.arguments, str):
request = WorkflowAgent.RequestInfoFunctionArgs.from_json(content.arguments)
else:
raise ValueError("Invalid arguments type. Expecting a request info structure for this sample.")
if isinstance(request.data, HandoffAgentUserRequest):
pending_requests[request.request_id] = request.data
return pending_requests
async def main() -> None:
"""Main entry point for the handoff workflow demo.
This function demonstrates:
1. Creating triage and specialist agents
2. Building a handoff workflow with custom termination condition
3. Running the workflow with scripted user responses
4. Processing events and handling user input requests
The workflow uses scripted responses instead of interactive input to make
the demo reproducible and testable. In a production application, you would
replace the scripted_responses with actual user input collection.
"""
# Initialize the Azure OpenAI chat client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create all agents: triage + specialists
triage, refund, order, support = create_agents(client)
# Build the handoff workflow
# - participants: All agents that can participate in the workflow
# - with_start_agent: The triage agent is designated as the start agent, which means
# it receives all user input first and orchestrates handoffs to specialists
# - termination_condition: Custom logic to stop the request/response loop.
# Without this, the default behavior continues requesting user input until max_turns
# is reached. Here we use a custom condition that checks if the conversation has ended
# naturally (when one of the agents says something like "you're welcome").
agent = (
HandoffBuilder(
name="customer_support_handoff",
participants=[triage, refund, order, support],
# Custom termination: Check if one of the agents has provided a closing message.
# This looks for the last message containing "welcome", which indicates the
# conversation has concluded naturally.
termination_condition=lambda conversation: (
len(conversation) > 0 and "welcome" in conversation[-1].text.lower()
),
)
.with_start_agent(triage)
.build()
.as_agent() # Convert workflow to agent interface
)
# Scripted user responses for reproducible demo
# In a console application, replace this with:
# user_input = input("Your response: ")
# or integrate with a UI/chat interface
scripted_responses = [
"My order 1234 arrived damaged and the packaging was destroyed. I'd like to return it.",
"Please also process a refund for order 1234.",
"Thanks for resolving this.",
]
# Start the workflow with the initial user message
print("[Starting workflow with initial user message...]\n")
initial_message = "Hello, I need assistance with my recent purchase."
print(f"- User: {initial_message}")
response = await agent.run(initial_message)
pending_requests = handle_response_and_requests(response)
# Process the request/response cycle
# The workflow will continue requesting input until:
# 1. The termination condition is met, OR
# 2. We run out of scripted responses
while pending_requests:
if not scripted_responses:
# No more scripted responses; terminate the workflow
responses = {req_id: HandoffAgentUserRequest.terminate() for req_id in pending_requests}
else:
# Get the next scripted response
user_response = scripted_responses.pop(0)
print(f"\n- User: {user_response}")
# Send response(s) to all pending requests
# In this demo, there's typically one request per cycle, but the API supports multiple
responses = {req_id: HandoffAgentUserRequest.create_response(user_response) for req_id in pending_requests}
function_results = [
Content.from_function_result(call_id=req_id, result=response) for req_id, response in responses.items()
]
response = await agent.run(Message("tool", function_results))
pending_requests = handle_response_and_requests(response)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,100 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import (
Agent,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder
"""
Sample: Build a Magentic orchestration and wrap it as an agent.
The script configures a Magentic workflow with streaming callbacks, then invokes the
orchestration through `workflow.as_agent(...)` so the entire Magentic loop can be reused
like any other agent while still emitting callback telemetry.
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
"""
async def main() -> None:
researcher_agent = Agent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
)
# Create code interpreter tool using instance method
coder_client = OpenAIResponsesClient()
code_interpreter_tool = coder_client.get_code_interpreter_tool()
coder_agent = Agent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
client=coder_client,
tools=code_interpreter_tool,
)
# Create a manager agent for orchestration
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the research and coding workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=OpenAIChatClient(),
)
print("\nBuilding Magentic Workflow...")
# intermediate_outputs=True: Enable intermediate outputs to observe the conversation as it unfolds
# (Intermediate outputs will be emitted as WorkflowOutputEvent events)
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_outputs=True,
manager_agent=manager_agent,
max_round_count=10,
max_stall_count=3,
max_reset_count=2,
).build()
task = (
"I am preparing a report on the energy efficiency of different machine learning model architectures. "
"Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 "
"on standard datasets (e.g., ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). "
"Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 "
"VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model "
"per task type (image classification, text classification, and text generation)."
)
print(f"\nTask: {task}")
print("\nStarting workflow execution...")
try:
# Wrap the workflow as an agent for composition scenarios
print("\nWrapping workflow as an agent and running...")
workflow_agent = workflow.as_agent(name="MagenticWorkflowAgent")
last_response_id: str | None = None
async for update in workflow_agent.run(task, stream=True):
# Fallback for any other events with text
if last_response_id != update.response_id:
if last_response_id is not None:
print() # Newline between different responses
print(f"{update.author_name}: ", end="", flush=True)
last_response_id = update.response_id
else:
print(update.text, end="", flush=True)
except Exception as e:
print(f"Workflow execution failed: {e}")
if __name__ == "__main__":
asyncio.run(main())
@@ -1,85 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Build a sequential workflow orchestration and wrap it as an agent.
The script assembles a sequential conversation flow with `SequentialBuilder`, then
invokes the entire orchestration through the `workflow.as_agent(...)` interface so
other coordinators can reuse the chain as a single participant.
Note on internal adapters:
- Sequential orchestration includes small adapter nodes for input normalization
("input-conversation"), agent-response conversion ("to-conversation:<participant>"),
and completion ("complete"). These may appear as ExecutorInvoke/Completed events in
the stream—similar to how concurrent orchestration includes a dispatcher/aggregator.
You can safely ignore them when focusing on agent progress.
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
"""
async def main() -> None:
# 1) Create agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
writer = client.as_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = client.as_agent(
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
)
# 2) Build sequential workflow: writer -> reviewer
workflow = SequentialBuilder(participants=[writer, reviewer]).build()
# 3) Treat the workflow itself as an agent for follow-up invocations
agent = workflow.as_agent(name="SequentialWorkflowAgent")
prompt = "Write a tagline for a budget-friendly eBike."
agent_response = await agent.run(prompt)
if agent_response.messages:
print("\n===== Conversation =====")
for i, msg in enumerate(agent_response.messages, start=1):
name = msg.author_name or msg.role
print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}")
"""
Sample Output:
===== Final Conversation =====
------------------------------------------------------------
01 [user]
Write a tagline for a budget-friendly eBike.
------------------------------------------------------------
02 [writer]
Ride farther, spend less—your affordable eBike adventure starts here.
------------------------------------------------------------
03 [reviewer]
This tagline clearly communicates affordability and the benefit of extended travel, making it
appealing to budget-conscious consumers. It has a friendly and motivating tone, though it could
be slightly shorter for more punch. Overall, a strong and effective suggestion!
===== as_agent() Conversation =====
------------------------------------------------------------
01 [writer]
Go electric, save big—your affordable ride awaits!
------------------------------------------------------------
02 [reviewer]
Catchy and straightforward! The tagline clearly emphasizes both the electric aspect and the affordability of the
eBike. It's inviting and actionable. For even more impact, consider making it slightly shorter:
"Go electric, save big." Overall, this is an effective and appealing suggestion for a budget-friendly eBike.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,13 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
# Ensure local getting_started package can be imported when running as a script.
@@ -42,7 +43,8 @@ to a human, receives the human response, and then forwards that response back
to the Worker. The workflow completes when idle.
Prerequisites:
- OpenAI account configured and accessible for OpenAIChatClient.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI account configured and accessible for AzureOpenAIResponsesClient.
- Familiarity with WorkflowBuilder, Executor, and WorkflowContext from agent_framework.
- Understanding of request-response message handling in executors.
- (Optional) Review of reflection and escalation patterns, such as those in
@@ -100,7 +102,11 @@ async def main() -> None:
# and escalation paths for human review.
worker = Worker(
id="worker",
chat_client=AzureOpenAIChatClient(credential=AzureCliCredential()),
chat_client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
reviewer = ReviewerWithHumanInTheLoop(worker_id="worker")
@@ -2,11 +2,13 @@
import asyncio
import json
import os
from typing import Annotated, Any
from agent_framework import tool
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from pydantic import Field
"""
@@ -28,7 +30,8 @@ When to use workflow.as_agent():
- To maintain a consistent agent interface for callers
Prerequisites:
- OpenAI environment variables configured
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured
"""
@@ -80,7 +83,11 @@ async def main() -> None:
print("=" * 70)
# Create chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agent with tools that use kwargs
agent = client.as_agent(
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from dataclasses import dataclass
from uuid import uuid4
@@ -13,7 +14,8 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
"""
@@ -33,7 +35,8 @@ Key Concepts Demonstrated:
- State management for pending requests and retry logic.
Prerequisites:
- OpenAI account configured and accessible for OpenAIChatClient.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- OpenAI account configured and accessible for AzureOpenAIResponsesClient.
- Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling.
- Understanding of how agent messages are generated, reviewed, and re-submitted.
"""
@@ -186,8 +189,22 @@ async def main() -> None:
print("=" * 50)
print("Building workflow with Worker ↔ Reviewer cycle...")
worker = Worker(id="worker", chat_client=OpenAIChatClient(model_id="gpt-4.1-nano"))
reviewer = Reviewer(id="reviewer", chat_client=OpenAIChatClient(model_id="gpt-4.1"))
worker = Worker(
id="worker",
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
reviewer = Reviewer(
id="reviewer",
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
agent = (
WorkflowBuilder(start_executor=worker)
@@ -1,10 +1,12 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import AgentThread, ChatMessageStore
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Workflow as Agent with Thread Conversation History and Checkpointing
@@ -31,13 +33,18 @@ Use cases:
- Long-running workflows that need pause/resume capability
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for AzureOpenAIResponsesClient
"""
async def main() -> None:
# Create a chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
assistant = client.as_agent(
name="assistant",
@@ -119,7 +126,11 @@ async def demonstrate_thread_serialization() -> None:
This shows how conversation history can be persisted and restored,
enabling long-running conversational workflows.
"""
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
memory_assistant = client.as_agent(
name="memory_assistant",
@@ -1,12 +1,15 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from azure.identity import AzureCliCredential
if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
@@ -30,8 +33,7 @@ from agent_framework import (
handler,
response_handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from agent_framework.azure import AzureOpenAIResponsesClient
"""
Sample: Checkpoint + human-in-the-loop quickstart.
@@ -178,7 +180,11 @@ def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow:
# Wire the workflow DAG. Edges mirror the numbered steps described in the
# module docstring. Because `WorkflowBuilder` is declarative, reading these
# edges is often the quickest way to understand execution order.
writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
writer_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions="Write concise, warm release notes that sound human and helpful.",
name="writer",
)
@@ -20,18 +20,21 @@ Key concepts:
- These are complementary: threads track conversation, checkpoints track workflow state
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for AzureOpenAIResponsesClient
"""
import asyncio
import os
from agent_framework import (
AgentThread,
ChatMessageStore,
InMemoryCheckpointStorage,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
async def basic_checkpointing() -> None:
@@ -40,7 +43,11 @@ async def basic_checkpointing() -> None:
print("Basic Checkpointing with Workflow as Agent")
print("=" * 60)
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
assistant = client.as_agent(
name="assistant",
@@ -81,7 +88,11 @@ async def checkpointing_with_thread() -> None:
print("Checkpointing with Thread Conversation History")
print("=" * 60)
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
assistant = client.as_agent(
name="memory_assistant",
@@ -124,7 +135,11 @@ async def streaming_with_checkpoints() -> None:
print("Streaming with Checkpointing")
print("=" * 60)
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
assistant = client.as_agent(
name="streaming_assistant",
@@ -58,13 +58,13 @@ class TextProcessor(Executor):
) -> None:
"""Process a text string and return statistics."""
text_preview = f"'{request.text[:50]}{'...' if len(request.text) > 50 else ''}'"
print(f"🔍 Sub-workflow processing text (Task {request.task_id}): {text_preview}")
print(f"Sub-workflow processing text (Task {request.task_id}): {text_preview}")
# Simple text processing
word_count = len(request.text.split()) if request.text.strip() else 0
char_count = len(request.text)
print(f"📊 Task {request.task_id}: {word_count} words, {char_count} characters")
print(f"Task {request.task_id}: {word_count} words, {char_count} characters")
# Create result
result = TextProcessingResult(
@@ -74,7 +74,7 @@ class TextProcessor(Executor):
char_count=char_count,
)
print(f"Sub-workflow completed task {request.task_id}")
print(f"Sub-workflow completed task {request.task_id}")
# Signal completion by yielding the result
await ctx.yield_output(result)
@@ -92,7 +92,7 @@ class TextProcessingOrchestrator(Executor):
@handler
async def start_processing(self, texts: list[str], ctx: WorkflowContext[TextProcessingRequest]) -> None:
"""Start processing multiple text strings."""
print(f"📄 Starting processing of {len(texts)} text strings")
print(f"Starting processing of {len(texts)} text strings")
print("=" * 60)
self.expected_count = len(texts)
@@ -101,7 +101,7 @@ class TextProcessingOrchestrator(Executor):
for i, text in enumerate(texts):
task_id = f"task_{i + 1}"
request = TextProcessingRequest(text=text, task_id=task_id)
print(f"📤 Dispatching {task_id} to sub-workflow")
print(f"Dispatching {task_id} to sub-workflow")
await ctx.send_message(request, target_id="text_processor_workflow")
@handler
@@ -111,12 +111,12 @@ class TextProcessingOrchestrator(Executor):
ctx: WorkflowContext[Never, list[TextProcessingResult]],
) -> None:
"""Collect results from sub-workflows."""
print(f"📥 Collected result from {result.task_id}")
print(f"Collected result from {result.task_id}")
self.results.append(result)
# Check if all results are collected
if len(self.results) == self.expected_count:
print("\n🎉 All tasks completed!")
print("\nAll tasks completed!")
await ctx.yield_output(self.results)
@@ -138,7 +138,7 @@ def get_result_summary(results: list[TextProcessingResult]) -> dict[str, Any]:
def create_sub_workflow() -> WorkflowExecutor:
"""Create the text processing sub-workflow."""
print("🚀 Setting up sub-workflow...")
print("Setting up sub-workflow...")
text_processor = TextProcessor()
processing_workflow = (
@@ -151,7 +151,7 @@ def create_sub_workflow() -> WorkflowExecutor:
async def main():
"""Main function to run the basic sub-workflow example."""
print("🔧 Setting up parent workflow...")
print("Setting up parent workflow...")
# Step 1: Create the parent workflow
orchestrator = TextProcessingOrchestrator()
sub_workflow_executor = create_sub_workflow()
@@ -172,14 +172,14 @@ async def main():
" Spaces around text ",
]
print(f"\n🧪 Testing with {len(test_texts)} text strings")
print(f"\nTesting with {len(test_texts)} text strings")
print("=" * 60)
# Step 3: Run the workflow
result = await main_workflow.run(test_texts)
# Step 4: Display results
print("\n📊 Processing Results:")
print("\nProcessing Results:")
print("=" * 60)
# Sort results by task_id for consistent display
@@ -190,19 +190,19 @@ async def main():
for result in sorted_results:
preview = result.text[:30] + "..." if len(result.text) > 30 else result.text
preview = preview.replace("\n", " ").strip() or "(empty)"
print(f"{result.task_id}: '{preview}' -> {result.word_count} words, {result.char_count} chars")
print(f"{result.task_id}: '{preview}' -> {result.word_count} words, {result.char_count} chars")
# Step 6: Display summary
summary = get_result_summary(sorted_results)
print("\n📈 Summary:")
print("\nSummary:")
print("=" * 60)
print(f"📄 Total texts processed: {summary['total_texts']}")
print(f"📝 Total words: {summary['total_words']}")
print(f"🔤 Total characters: {summary['total_characters']}")
print(f"📊 Average words per text: {summary['average_words_per_text']}")
print(f"📏 Average characters per text: {summary['average_characters_per_text']}")
print(f"Total texts processed: {summary['total_texts']}")
print(f"Total words: {summary['total_words']}")
print(f"Total characters: {summary['total_characters']}")
print(f"Average words per text: {summary['average_words_per_text']}")
print(f"Average characters per text: {summary['average_characters_per_text']}")
print("\n🏁 Processing complete!")
print("\nProcessing complete!")
if __name__ == "__main__":
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from typing import Annotated, Any
from agent_framework import (
@@ -9,8 +10,9 @@ from agent_framework import (
WorkflowExecutor,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
"""
Sample: Sub-Workflow kwargs Propagation
@@ -26,7 +28,8 @@ Key Concepts:
- Useful for passing authentication tokens, configuration, or request context
Prerequisites:
- OpenAI environment variables configured
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured
"""
@@ -74,7 +77,11 @@ async def main() -> None:
print("=" * 70)
# Create chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create an agent with tools that use kwargs
inner_agent = client.as_agent(
@@ -319,14 +319,14 @@ async def main() -> None:
]
# Run the workflow
print(f"🧪 Testing with {len(test_requests)} mixed requests.")
print("🚀 Starting main workflow...")
print(f"Testing with {len(test_requests)} mixed requests.")
print("Starting main workflow...")
run_result = await main_workflow.run(test_requests)
# Handle request info events
request_info_events = run_result.get_request_info_events()
if request_info_events:
print(f"\n🔍 Handling {len(request_info_events)} request info events...\n")
print(f"\nHandling {len(request_info_events)} request info events...\n")
responses: dict[str, ResourceResponse | PolicyResponse] = {}
for event in request_info_events:
@@ -73,7 +73,7 @@ def build_email_address_validation_workflow() -> Workflow:
email address to the next executor in the workflow.
"""
sanitized = email_address.strip()
print(f"✂️ Sanitized email address: '{sanitized}'")
print(f"Sanitized email address: '{sanitized}'")
await ctx.send_message(SanitizedEmailResult(original=email_address, sanitized=sanitized, is_valid=False))
class EmailFormatValidator(Executor):
@@ -91,14 +91,14 @@ def build_email_address_validation_workflow() -> Workflow:
When the format is valid, it sends the validated email address to the next executor in the workflow.
"""
if "@" not in partial_result.sanitized or "." not in partial_result.sanitized.split("@")[-1]:
print(f"Invalid email format: '{partial_result.sanitized}'")
print(f"Invalid email format: '{partial_result.sanitized}'")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
)
)
return
print(f"Validated email format: '{partial_result.sanitized}'")
print(f"Validated email format: '{partial_result.sanitized}'")
await ctx.send_message(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
@@ -120,7 +120,7 @@ def build_email_address_validation_workflow() -> Workflow:
to an external system to user for validation.
"""
domain = partial_result.sanitized.split("@")[-1]
print(f"🔍 Validating domain: '{domain}'")
print(f"Validating domain: '{domain}'")
self._pending_domains[domain] = partial_result
# Send a request to the external system via the request_info mechanism
await ctx.request_info(request_data=domain, response_type=bool)
@@ -138,14 +138,14 @@ def build_email_address_validation_workflow() -> Workflow:
raise ValueError(f"Received response for unknown domain: '{original_request}'")
partial_result = self._pending_domains.pop(original_request)
if is_valid:
print(f"Domain '{original_request}' is valid.")
print(f"Domain '{original_request}' is valid.")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=True
)
)
else:
print(f"Domain '{original_request}' is invalid.")
print(f"Domain '{original_request}' is invalid.")
await ctx.yield_output(
SanitizedEmailResult(
original=partial_result.original, sanitized=partial_result.sanitized, is_valid=False
@@ -201,15 +201,15 @@ class SmartEmailOrchestrator(Executor):
"""
recipient = email.recipient
if recipient in self._approved_recipients:
print(f"📧 Recipient '{recipient}' has been previously approved.")
print(f"Recipient '{recipient}' has been previously approved.")
await ctx.send_message(email)
return
if recipient in self._disapproved_recipients:
print(f"🚫 Blocking email to previously disapproved recipient: '{recipient}'")
print(f"Blocking email to previously disapproved recipient: '{recipient}'")
await ctx.yield_output(False)
return
print(f"🔍 Validating new recipient email address: '{recipient}'")
print(f"Validating new recipient email address: '{recipient}'")
self._pending_emails[recipient] = email
await ctx.send_message(recipient)
@@ -227,7 +227,7 @@ class SmartEmailOrchestrator(Executor):
raise TypeError(f"Expected domain string, got {type(request.source_event.data)}")
domain = request.source_event.data
is_valid = domain in self._approved_domains
print(f"🌐 External domain validation for '{domain}': {'valid' if is_valid else 'invalid'}")
print(f"External domain validation for '{domain}': {'valid' if is_valid else 'invalid'}")
await ctx.send_message(request.create_response(is_valid), target_id=request.executor_id)
@handler
@@ -243,11 +243,11 @@ class SmartEmailOrchestrator(Executor):
email = self._pending_emails.pop(result.original)
email.recipient = result.sanitized # Use the sanitized email address
if result.is_valid:
print(f"Email address '{result.original}' is valid.")
print(f"Email address '{result.original}' is valid.")
self._approved_recipients.add(result.original)
await ctx.send_message(email)
else:
print(f"🚫 Email address '{result.original}' is invalid. Blocking email.")
print(f"Email address '{result.original}' is invalid. Blocking email.")
self._disapproved_recipients.add(result.original)
await ctx.yield_output(False)
@@ -258,9 +258,9 @@ class EmailDelivery(Executor):
@handler
async def handle(self, email: Email, ctx: WorkflowContext[Never, bool]) -> None:
"""Simulate sending the email and yield True as the final result."""
print(f"📤 Sending email to '{email.recipient}' with subject '{email.subject}'")
print(f"Sending email to '{email.recipient}' with subject '{email.subject}'")
await asyncio.sleep(1) # Simulate network delay
print(f"Email sent to '{email.recipient}' successfully.")
print(f"Email sent to '{email.recipient}' successfully.")
await ctx.yield_output(True)
@@ -294,10 +294,10 @@ async def main() -> None:
# Execute the workflow
for email in test_emails:
print(f"\n🚀 Processing email to '{email.recipient}'")
print(f"\nProcessing email to '{email.recipient}'")
async for event in workflow.run(email, stream=True):
if event.type == "output":
print(f"🎉 Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
print(f"Final result for '{email.recipient}': {'Delivered' if event.data else 'Blocked'}")
if __name__ == "__main__":
@@ -14,7 +14,7 @@ from agent_framework import ( # Core chat primitives used to build requests
WorkflowContext, # Per-run context and event bus
executor, # Decorator to declare a Python function as a workflow executor
)
from agent_framework.azure import AzureOpenAIChatClient # Thin client wrapper for Azure OpenAI chat models
from agent_framework.azure import AzureOpenAIResponsesClient # Thin client wrapper for Azure OpenAI chat models
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
from pydantic import BaseModel # Structured outputs for safer parsing
from typing_extensions import Never
@@ -32,10 +32,11 @@ Purpose:
- Illustrate how to transform one agent's structured result into a new AgentExecutorRequest for a downstream agent.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- You understand the basics of WorkflowBuilder, executors, and events in this framework.
- You know the concept of edge conditions and how they gate routes using a predicate function.
- Azure OpenAI access is configured for AzureOpenAIChatClient. You should be logged in with Azure CLI (AzureCliCredential)
and have the Azure OpenAI environment variables set as documented in the getting started chat client README.
- Azure OpenAI access is configured for AzureOpenAIResponsesClient. You should be logged in with Azure CLI (AzureCliCredential)
and have the Foundry V2 Project environment variables set as documented in the getting started chat client README.
- The sample email resource file exists at workflow/resources/email.txt.
High level flow:
@@ -131,7 +132,11 @@ async def to_email_assistant_request(
def create_spam_detector_agent() -> Agent:
"""Helper to create a spam detection agent."""
# AzureCliCredential uses your current az login. This avoids embedding secrets in code.
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are a spam detection assistant that identifies spam emails. "
"Always return JSON with fields is_spam (bool), reason (string), and email_content (string). "
@@ -145,7 +150,11 @@ def create_spam_detector_agent() -> Agent:
def create_email_assistant_agent() -> Agent:
"""Helper to create an email assistant agent."""
# AzureCliCredential uses your current az login. This avoids embedding secrets in code.
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an email assistant that helps users draft professional responses to emails. "
"Your input may be a JSON object that includes 'email_content'; base your reply on that content. "
@@ -178,7 +187,7 @@ async def main() -> None:
# Read Email content from the sample resource file.
# This keeps the sample deterministic since the model sees the same email every run.
email_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "email.txt")
email_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "resources", "email.txt") # noqa: ASYNC240
with open(email_path) as email_file: # noqa: ASYNC230
email = email_file.read()
@@ -13,13 +13,14 @@ from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponseUpdate,
Message,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
executor,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
from typing_extensions import Never
@@ -42,6 +43,7 @@ Show how to:
- Apply conditional persistence logic (short vs long emails).
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Familiarity with WorkflowBuilder, executors, edges, and events.
- Understanding of multi-selection edge groups and how their selection function maps to target ids.
- Experience with workflow state for persisting and reusing objects.
@@ -177,12 +179,16 @@ async def handle_uncertain(analysis: AnalysisResult, ctx: WorkflowContext[Never,
async def database_access(analysis: AnalysisResult, ctx: WorkflowContext[Never, str]) -> None:
# Simulate DB writes for email and analysis (and summary if present)
await asyncio.sleep(0.05)
await ctx.add_event(DatabaseEvent(f"Email {analysis.email_id} saved to database."))
await ctx.add_event(DatabaseEvent(type="database_event", data=f"Email {analysis.email_id} saved to database.")) # type: ignore
def create_email_analysis_agent() -> Agent:
"""Creates the email analysis agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are a spam detection assistant that identifies spam emails. "
"Always return JSON with fields 'spam_decision' (one of NotSpam, Spam, Uncertain) "
@@ -195,7 +201,11 @@ def create_email_analysis_agent() -> Agent:
def create_email_assistant_agent() -> Agent:
"""Creates the email assistant agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=("You are an email assistant that helps users draft responses to emails with professionalism."),
name="email_assistant_agent",
default_options={"response_format": EmailResponse},
@@ -204,7 +214,11 @@ def create_email_assistant_agent() -> Agent:
def create_email_summary_agent() -> Agent:
"""Creates the email summary agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=("You are an assistant that helps users summarize emails."),
name="email_summary_agent",
default_options={"response_format": EmailSummaryModel},
@@ -267,6 +281,10 @@ async def main() -> None:
if isinstance(event, DatabaseEvent):
print(f"{event}")
elif event.type == "output":
if isinstance(event.data, AgentResponseUpdate):
# Agent executors stream token-level updates. Skip these to keep sample
# output focused on final workflow results.
continue
print(f"Workflow output: {event.data}")
"""
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from enum import Enum
from agent_framework import (
@@ -8,13 +9,14 @@ from agent_framework import (
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponseUpdate,
Executor,
Message,
WorkflowBuilder,
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
@@ -26,7 +28,8 @@ What it does:
- The workflow completes when the correct number is guessed.
Prerequisites:
- Azure AI/ Azure OpenAI for `AzureOpenAIChatClient` agent.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure AI/ Azure OpenAI for `AzureOpenAIResponsesClient` agent.
- Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`).
"""
@@ -116,7 +119,11 @@ class ParseJudgeResponse(Executor):
def create_judge_agent() -> Agent:
"""Create a judge agent that evaluates guesses."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=("You strictly respond with one of: MATCHED, ABOVE, BELOW based on the given target and guess."),
name="judge_agent",
)
@@ -140,12 +147,16 @@ async def main():
.build()
)
# Step 2: Run the workflow and print the events.
# Step 2: Run the workflow with concise streaming output.
iterations = 0
async for event in workflow.run(NumberSignal.INIT, stream=True):
if event.type == "executor_completed" and event.executor_id == "guess_number":
iterations += 1
print(f"Event: {event}")
elif event.type == "output":
if isinstance(event.data, AgentResponseUpdate):
# Agent executor streams token-level updates; skip to avoid noisy logs.
continue
print(f"Workflow output: {event.data}")
# This is essentially a binary search, so the number of iterations should be logarithmic.
# The maximum number of iterations is [log2(range size)]. For a range of 1 to 100, this is log2(100) which is 7.
@@ -18,7 +18,7 @@ from agent_framework import ( # Core chat primitives used to form LLM requests
WorkflowContext, # Per-run context and event bus
executor, # Decorator to turn a function into a workflow executor
)
from agent_framework.azure import AzureOpenAIChatClient # Thin client for Azure OpenAI chat models
from agent_framework.azure import AzureOpenAIResponsesClient # Thin client for Azure OpenAI chat models
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
from pydantic import BaseModel # Structured outputs with validation
from typing_extensions import Never
@@ -39,9 +39,10 @@ on that type.
- Use ctx.yield_output() to provide workflow results - the workflow completes when idle with no pending work.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Familiarity with WorkflowBuilder, executors, edges, and events.
- Understanding of switch-case edge groups and how Case and Default are evaluated in order.
- Working Azure OpenAI configuration for AzureOpenAIChatClient, with Azure CLI login and required environment variables.
- Working Azure OpenAI configuration for AzureOpenAIResponsesClient, with Azure CLI login and required environment variables.
- Access to workflow/resources/ambiguous_email.txt, or accept the inline fallback string.
"""
@@ -154,7 +155,11 @@ async def handle_uncertain(detection: DetectionResult, ctx: WorkflowContext[Neve
def create_spam_detection_agent() -> Agent:
"""Create and return the spam detection agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are a spam detection assistant that identifies spam emails. "
"Be less confident in your assessments. "
@@ -168,7 +173,11 @@ def create_spam_detection_agent() -> Agent:
def create_email_assistant_agent() -> Agent:
"""Create and return the email assistant agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=("You are an email assistant that helps users draft responses to emails with professionalism."),
name="email_assistant_agent",
default_options={"response_format": EmailResponse},
@@ -23,10 +23,11 @@ The workflow:
import asyncio
import json
import logging
import os
import uuid
from pathlib import Path
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.declarative import (
AgentExternalInputRequest,
AgentExternalInputResponse,
@@ -164,7 +165,11 @@ async def main() -> None:
plugin = TicketingPlugin()
# Create Azure OpenAI client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agents with structured outputs
self_service_agent = client.as_agent(
@@ -260,7 +265,9 @@ async def main() -> None:
async for event in stream:
if event.type == "output":
data = event.data
source_id = getattr(event, "source_executor_id", "")
# source_executor_id is only available on request_info events.
# For output events, use executor_id to identify the emitting node.
source_id = event.executor_id or ""
# Check if this is a SendActivity output (activity text from log_ticket, log_route, etc.)
if "log_" in source_id.lower():
@@ -22,9 +22,10 @@ Usage:
"""
import asyncio
import os
from pathlib import Path
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
from pydantic import BaseModel, Field
@@ -122,7 +123,11 @@ class ManagerResponse(BaseModel):
async def main() -> None:
"""Run the deep research workflow."""
# Create Azure OpenAI client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agents
research_agent = client.as_agent(
@@ -6,12 +6,13 @@ function tools assigned. Exits the loop when the user enters "exit".
"""
import asyncio
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated, Any
from agent_framework import FileCheckpointStorage, tool
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework_declarative import ExternalInputRequest, ExternalInputResponse, WorkflowFactory
from azure.identity import AzureCliCredential
from pydantic import Field
@@ -62,7 +63,11 @@ def get_item_price(name: Annotated[str, Field(description="Menu item name")]) ->
async def main():
# Create agent with tools
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
menu_agent = client.as_agent(
name="MenuAgent",
instructions="Answer questions about menu items, specials, and prices.",
@@ -13,9 +13,10 @@ Demonstrates sequential multi-agent pipeline:
"""
import asyncio
import os
from pathlib import Path
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
@@ -49,7 +50,11 @@ Return the final polished version."""
async def main() -> None:
"""Run the marketing workflow with real Azure AI agents."""
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
analyst_agent = client.as_agent(
name="AnalystAgent",
@@ -15,14 +15,15 @@ The workflow loops until the teacher gives congratulations or max turns reached.
Prerequisites:
- Azure OpenAI deployment with chat completion capability
- Environment variables:
AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint
AZURE_OPENAI_DEPLOYMENT_NAME: Your deployment name (optional, defaults to gpt-4o)
AZURE_AI_PROJECT_ENDPOINT: Your Azure AI Foundry Agent Service (V2) project endpoint
AZURE_AI_MODEL_DEPLOYMENT_NAME: Your model deployment name
"""
import asyncio
import os
from pathlib import Path
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
@@ -51,7 +52,11 @@ Focus on building understanding, not just getting the right answer."""
async def main() -> None:
"""Run the student-teacher workflow with real Azure AI agents."""
# Create chat client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create student and teacher agents
student_agent = client.as_agent(
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from dataclasses import dataclass, field
@@ -17,7 +18,7 @@ from agent_framework import (
handler,
response_handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from typing_extensions import Never
@@ -37,7 +38,8 @@ Demonstrates:
- Handling human feedback and routing it to the appropriate agents.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Run `az login` before executing.
"""
@@ -161,13 +163,21 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
async def main() -> None:
"""Run the workflow and bridge human feedback between two agents."""
# Create the agents
writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
writer_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="writer_agent",
instructions=("You are a marketing writer."),
tool_choice="required",
)
final_editor_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
final_editor_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="final_editor_agent",
instructions=(
"You are an editor who polishes marketing copy after human approval. "
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from dataclasses import dataclass
from typing import Annotated
@@ -15,7 +16,8 @@ from agent_framework import (
handler,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from typing_extensions import Never
"""
@@ -45,6 +47,7 @@ Demonstrate:
- Handling approval requests during workflow execution.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure AI Agent Service configured, along with the required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, request_info events (type='request_info'), and streaming runs.
@@ -193,13 +196,20 @@ class EmailPreprocessor(Executor):
@handler
async def preprocess(self, email: Email, ctx: WorkflowContext[str]) -> None:
"""Preprocess the incoming email."""
message = str(email)
email_payload = (
f"Incoming email:\n"
f"From: {email.sender}\n"
f"Subject: {email.subject}\n"
f"Body: {email.body}"
)
message = email_payload
if email.sender in self.special_email_addresses:
note = (
"Pay special attention to this sender. This email is very important. "
"Gather relevant information from all previous emails within my team before responding."
"Priority sender context: this message is business-critical. "
"If additional context is needed, use available tools to retrieve only the minimum relevant "
"prior team communication related to this request."
)
message = f"{note}\n\n{message}"
message = f"{note}\n\n{email_payload}"
await ctx.send_message(message)
@@ -215,7 +225,11 @@ async def conclude_workflow(
async def main() -> None:
# Create agent
email_writer_agent = OpenAIChatClient().as_agent(
email_writer_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="EmailWriter",
instructions=("You are an excellent email assistant. You respond to incoming emails."),
# tools with `approval_mode="always_require"` will trigger approval requests
@@ -16,16 +16,18 @@ Flow:
4. The workflow resumes — the agent sees the tool result and finishes.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI endpoint configured via environment variables.
- `az login` for AzureCliCredential.
"""
import asyncio
import json
import os
from typing import Any
from agent_framework import Content, FunctionTool, WorkflowBuilder
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
# A declaration-only tool: the schema is sent to the LLM, but the framework
@@ -45,7 +47,11 @@ get_user_location = FunctionTool(
async def main() -> None:
agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="WeatherBot",
instructions=(
"You are a helpful weather assistant. "
@@ -1,197 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with ConcurrentBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
ConcurrentBuilder workflow for specific agents, allowing human review and
modification of individual agent outputs before aggregation.
Purpose:
Show how to use the request info API that pauses for selected concurrent agents,
allowing review and steering of their results.
Demonstrate:
- Configuring request info with `.with_request_info()` for specific agents
- Reviewing output from individual agents during concurrent execution
- Injecting human guidance for specific agents before aggregation
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
from azure.identity import AzureCliCredential
# Store chat client at module level for aggregator access
_chat_client: AzureOpenAIChatClient | None = None
async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any:
"""Custom aggregator that synthesizes concurrent agent outputs using an LLM.
This aggregator extracts the outputs from each parallel agent and uses the
chat client to create a unified summary, incorporating any human feedback
that was injected into the conversation.
Args:
results: List of responses from all concurrent agents
Returns:
The synthesized summary text
"""
if not _chat_client:
return "Error: Chat client not initialized"
# Extract each agent's final output
expert_sections: list[str] = []
human_guidance = ""
for r in results:
try:
messages = getattr(r.agent_response, "messages", [])
final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)"
expert_sections.append(f"{getattr(r, 'executor_id', 'analyst')}:\n{final_text}")
# Check for human feedback in the conversation (will be last user message if present)
if r.full_conversation:
for msg in reversed(r.full_conversation):
if msg.role == "user" and msg.text and "perspectives" not in msg.text.lower():
human_guidance = msg.text
break
except Exception:
expert_sections.append(f"{getattr(r, 'executor_id', 'analyst')}: (error extracting output)")
# Build prompt with human guidance if provided
guidance_text = f"\n\nHuman guidance: {human_guidance}" if human_guidance else ""
system_msg = Message(
"system",
text=(
"You are a synthesis expert. Consolidate the following analyst perspectives "
"into one cohesive, balanced summary (3-4 sentences). If human guidance is provided, "
"prioritize aspects as directed."
),
)
user_msg = Message("user", text="\n\n".join(expert_sections) + guidance_text)
response = await _chat_client.get_response([system_msg, user_msg])
return response.messages[-1].text if response.messages else ""
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
if event.type == "output":
# The output of the workflow comes from the aggregator and it's a single string
print("\n" + "=" * 60)
print("ANALYSIS COMPLETE")
print("=" * 60)
print("Final synthesized analysis:")
print(event.data)
# Process any requests for human feedback
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer this agent's contribution
user_input = input("Your guidance for the analysts (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
global _chat_client
_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents that analyze from different perspectives
technical_analyst = _chat_client.as_agent(
name="technical_analyst",
instructions=(
"You are a technical analyst. When given a topic, provide a technical "
"perspective focusing on implementation details, performance, and architecture. "
"Keep your analysis to 2-3 sentences."
),
)
business_analyst = _chat_client.as_agent(
name="business_analyst",
instructions=(
"You are a business analyst. When given a topic, provide a business "
"perspective focusing on ROI, market impact, and strategic value. "
"Keep your analysis to 2-3 sentences."
),
)
user_experience_analyst = _chat_client.as_agent(
name="ux_analyst",
instructions=(
"You are a UX analyst. When given a topic, provide a user experience "
"perspective focusing on usability, accessibility, and user satisfaction. "
"Keep your analysis to 2-3 sentences."
),
)
# Build workflow with request info enabled and custom aggregator
workflow = (
ConcurrentBuilder(participants=[technical_analyst, business_analyst, user_experience_analyst])
.with_aggregator(aggregate_with_synthesis)
# Only enable request info for the technical analyst agent
.with_request_info(agents=["technical_analyst"])
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run("Analyze the impact of large language models on software development.", stream=True)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,168 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with GroupChatBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
GroupChatBuilder workflow BEFORE specific participants speak. By using the
`agents=` filter parameter, you can target only certain participants rather
than pausing before every turn.
Purpose:
Show how to use the request info API with selective filtering to pause before
specific participants speak, allowing human input to steer their response.
Demonstrate:
- Configuring request info with `.with_request_info(agents=[...])`
- Using agent filtering to reduce interruptions
- Steering agent behavior with pre-agent human input
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
from azure.identity import AzureCliCredential
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
if event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("DISCUSSION COMPLETE")
print("=" * 60)
print("Final discussion summary:")
# To make the type checker happy, we cast event.data to the expected type
outputs = cast(list[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
# Display pre-agent context for human input
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer the agent
user_input = input(f"Feedback for {request.executor_id} (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents for a group discussion
optimist = client.as_agent(
name="optimist",
instructions=(
"You are an optimistic team member. You see opportunities and potential "
"in ideas. Engage constructively with the discussion, building on others' "
"points while maintaining a positive outlook. Keep responses to 2-3 sentences."
),
)
pragmatist = client.as_agent(
name="pragmatist",
instructions=(
"You are a pragmatic team member. You focus on practical implementation "
"and realistic timelines. Sometimes you disagree with overly optimistic views. "
"Keep responses to 2-3 sentences."
),
)
creative = client.as_agent(
name="creative",
instructions=(
"You are a creative team member. You propose innovative solutions and "
"think outside the box. You may suggest alternatives to conventional approaches. "
"Keep responses to 2-3 sentences."
),
)
# Orchestrator coordinates the discussion
orchestrator = client.as_agent(
name="orchestrator",
instructions=(
"You are a discussion manager coordinating a team conversation between participants. "
"Your job is to select who speaks next.\n\n"
"RULES:\n"
"1. Rotate through ALL participants - do not favor any single participant\n"
"2. Each participant should speak at least once before any participant speaks twice\n"
"3. Continue for at least 5 rounds before ending the discussion\n"
"4. Do NOT select the same participant twice in a row"
),
)
# Build workflow with request info enabled
# Using agents= filter to only pause before pragmatist speaks (not every turn)
# max_rounds=6: Limit to 6 rounds
workflow = (
GroupChatBuilder(
participants=[optimist, pragmatist, creative],
max_rounds=6,
orchestrator_agent=orchestrator,
)
.with_request_info(agents=[pragmatist]) # Only pause before pragmatist speaks
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Discuss how our team should approach adopting AI tools for productivity. "
"Consider benefits, risks, and implementation strategies.",
stream=True,
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from dataclasses import dataclass
@@ -16,7 +17,7 @@ from agent_framework import (
handler,
response_handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
@@ -37,7 +38,8 @@ Demonstrate:
- Driving the loop in application code with run and responses parameter.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
"""
@@ -183,7 +185,11 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
async def main() -> None:
"""Run the human-in-the-loop guessing game workflow."""
# Create agent and executor
guessing_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
guessing_agent = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
name="GuessingAgent",
instructions=(
"You guess a number between 1 and 10. "
@@ -1,136 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with SequentialBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
SequentialBuilder workflow AFTER each agent runs, allowing external input
(e.g., human feedback) for review and optional iteration.
Purpose:
Show how to use the request info API that pauses after every agent response,
using the standard request_info pattern for consistency.
Demonstrate:
- Configuring request info with `.with_request_info()`
- Handling request_info events with AgentInputRequest data
- Injecting responses back into the workflow via run(responses=..., stream=True)
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
from azure.identity import AzureCliCredential
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, AgentRequestInfoResponse] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, AgentExecutorResponse] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, AgentExecutorResponse):
requests[event.request_id] = event.data
elif event.type == "output":
# The output of the sequential workflow is a list of ChatMessages
print("\n" + "=" * 60)
print("WORKFLOW COMPLETE")
print("=" * 60)
print("Final output:")
outputs = cast(list[Message], event.data)
for message in outputs:
print(f"[{message.author_name or message.role}]: {message.text}")
responses: dict[str, AgentRequestInfoResponse] = {}
if requests:
for request_id, request in requests.items():
# Display agent response and conversation context for review
print("\n" + "-" * 40)
print("REQUEST INFO: INPUT REQUESTED")
print(
f"Agent {request.executor_id} just responded with: '{request.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if request.full_conversation:
print("Conversation context:")
recent = (
request.full_conversation[-2:] if len(request.full_conversation) > 2 else request.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role
text = (msg.text or "")[:150]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get feedback on the agent's response (approve or request iteration)
user_input = input("Your guidance (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
user_input = AgentRequestInfoResponse.approve()
else:
user_input = AgentRequestInfoResponse.from_strings([user_input])
responses[request_id] = user_input
return responses if responses else None
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents for a sequential document review workflow
drafter = client.as_agent(
name="drafter",
instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."),
)
editor = client.as_agent(
name="editor",
instructions=(
"You are an editor. Review the draft and make improvements. "
"Incorporate any human feedback that was provided."
),
)
finalizer = client.as_agent(
name="finalizer",
instructions=(
"You are a finalizer. Take the edited content and create a polished final version. "
"Incorporate any additional feedback provided."
),
)
# Build workflow with request info enabled (pauses after each agent responds)
workflow = (
SequentialBuilder(participants=[drafter, editor, finalizer])
# Only enable request info for the editor agent
.with_request_info(agents=["editor"])
.build()
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run("Write a brief introduction to artificial intelligence.", stream=True)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
if __name__ == "__main__":
asyncio.run(main())
@@ -1,19 +1,21 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from dataclasses import dataclass
from agent_framework import (
AgentExecutor, # Wraps a ChatAgent as an Executor for use in workflows
AgentExecutorRequest, # The message bundle sent to an AgentExecutor
AgentExecutorResponse, # The structured result returned by an AgentExecutor
AgentResponseUpdate,
Executor, # Base class for custom Python executors
Message, # Chat message structure
WorkflowBuilder, # Fluent builder for wiring the workflow graph
WorkflowContext, # Per run context and event bus
handler, # Decorator to mark an Executor method as invokable
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential # Uses your az CLI login for credentials
from typing_extensions import Never
@@ -29,8 +31,9 @@ Show how to construct a parallel branch pattern in workflows. Demonstrate:
- Fan in by collecting a list of AgentExecutorResponse objects and reducing them to a single result.
Prerequisites:
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
- Azure OpenAI access configured for AzureOpenAIChatClient. Log in with Azure CLI and set any required environment variables.
- Azure OpenAI access configured for AzureOpenAIResponsesClient. Log in with Azure CLI and set any required environment variables.
- Comfort reading AgentExecutorResponse.agent_response.text for assistant output aggregation.
"""
@@ -87,13 +90,31 @@ class AggregateInsights(Executor):
await ctx.yield_output(consolidated)
def render_live_streams(buffers: dict[str, str], order: list[str], completed: set[str]) -> None:
"""Render concurrent agent streams in separate sections."""
# Clear terminal and move cursor to top-left for a live dashboard effect.
print("\033[2J\033[H", end="")
print("=== Expert Streams (Live) ===")
print("Concurrent agent updates are shown below as they stream.\n")
for agent_id in order:
state = "completed" if agent_id in completed else "streaming"
print(f"[{agent_id}] ({state})")
print(buffers.get(agent_id, ""))
print("-" * 80)
print("", end="", flush=True)
async def main() -> None:
# 1) Create executor and agent instances
dispatcher = DispatchToExperts(id="dispatcher")
aggregator = AggregateInsights(id="aggregator")
researcher = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
@@ -102,7 +123,11 @@ async def main() -> None:
)
)
marketer = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
@@ -111,7 +136,11 @@ async def main() -> None:
)
)
legal = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."
@@ -128,18 +157,32 @@ async def main() -> None:
.build()
)
# 3) Run with a single prompt and print progress plus the final consolidated output
# 3) Run with a single prompt and render live expert streams plus final consolidated output.
expert_order = ["researcher", "marketer", "legal"]
expert_buffers: dict[str, str] = {expert_id: "" for expert_id in expert_order}
completed_experts: set[str] = set()
final_output: str | None = None
async for event in workflow.run(
"We are launching a new budget-friendly electric bike for urban commuters.", stream=True
):
if event.type == "executor_invoked":
# Show when executors are invoked and completed for lightweight observability.
print(f"{event.executor_id} invoked")
elif event.type == "executor_completed":
print(f"{event.executor_id} completed")
if event.type == "executor_completed" and event.executor_id in expert_buffers:
completed_experts.add(event.executor_id)
render_live_streams(expert_buffers, expert_order, completed_experts)
elif event.type == "output":
print("===== Final Aggregated Output =====")
print(event.data)
if isinstance(event.data, AgentResponseUpdate):
executor_id = event.executor_id or ""
if executor_id in expert_buffers:
expert_buffers[executor_id] += event.data.text
render_live_streams(expert_buffers, expert_order, completed_experts)
continue
if event.executor_id == "aggregator":
final_output = str(event.data)
if final_output:
print("\n=== Final Consolidated Output ===\n")
print(final_output)
if __name__ == "__main__":
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -15,7 +16,7 @@ from agent_framework import (
WorkflowContext,
executor,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
from typing_extensions import Never
@@ -34,7 +35,8 @@ Show how to:
- Compose agent backed executors with function style executors and yield the final output when the workflow completes.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Familiarity with WorkflowBuilder, executors, conditional edges, and streaming runs.
"""
@@ -156,7 +158,11 @@ async def handle_spam(detection: DetectionResult, ctx: WorkflowContext[Never, st
def create_spam_detection_agent() -> Agent:
"""Creates a spam detection agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are a spam detection assistant that identifies spam emails. "
"Always return JSON with fields is_spam (bool) and reason (string)."
@@ -169,7 +175,11 @@ def create_spam_detection_agent() -> Agent:
def create_email_assistant_agent() -> Agent:
"""Creates an email assistant agent."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You are an email assistant that helps users draft responses to emails with professionalism. "
"Return JSON with a single field 'response' containing the drafted reply."
@@ -2,11 +2,13 @@
import asyncio
import json
import os
from typing import Annotated, Any, cast
from agent_framework import Message, tool
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from pydantic import Field
"""
@@ -22,7 +24,8 @@ Key Concepts:
- Works with Sequential, Concurrent, GroupChat, Handoff, and Magentic patterns
Prerequisites:
- OpenAI environment variables configured
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured
"""
@@ -74,7 +77,11 @@ async def main() -> None:
print("=" * 70)
# Create chat client
client = OpenAIChatClient()
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create agent with tools that use kwargs
agent = client.as_agent(
@@ -1,200 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterable
from typing import Annotated
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import ConcurrentBuilder
"""
Sample: Concurrent Workflow with Tool Approval Requests
This sample demonstrates how to use ConcurrentBuilder with tools that require human
approval before execution. Multiple agents run in parallel, and any tool requiring
approval will pause the workflow until the human responds.
This sample works as follows:
1. A ConcurrentBuilder workflow is created with two agents running in parallel.
2. Both agents have the same tools, including one requiring approval (execute_trade).
3. Both agents receive the same task and work concurrently on their respective stocks.
4. When either agent tries to execute a trade, it triggers an approval request.
5. The sample simulates human approval and the workflow completes.
6. Results from both agents are aggregated and output.
Purpose:
Show how tool call approvals work in parallel execution scenarios where multiple
agents may independently trigger approval requests.
Demonstrate:
- Handling multiple approval requests from different agents in concurrent workflows.
- Handling during concurrent agent execution.
- Understanding that approval pauses only the agent that triggered it, not all agents.
Prerequisites:
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with ConcurrentBuilder and streaming workflow events.
"""
# 1. Define market data tools (no approval required)
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# See:
# samples/getting_started/tools/function_tool_with_approval.py
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_stock_price(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
"""Get the current stock price for a given symbol."""
# Mock data for demonstration
prices = {"AAPL": 175.50, "GOOGL": 140.25, "MSFT": 378.90, "AMZN": 178.75}
price = prices.get(symbol.upper(), 100.00)
return f"{symbol.upper()}: ${price:.2f}"
@tool(approval_mode="never_require")
def get_market_sentiment(symbol: Annotated[str, "The stock ticker symbol"]) -> str:
"""Get market sentiment analysis for a stock."""
# Mock sentiment data
mock_data = {
"AAPL": "Market sentiment for AAPL: Bullish (68% positive mentions in last 24h)",
"GOOGL": "Market sentiment for GOOGL: Neutral (50% positive mentions in last 24h)",
"MSFT": "Market sentiment for MSFT: Bullish (72% positive mentions in last 24h)",
"AMZN": "Market sentiment for AMZN: Bearish (40% positive mentions in last 24h)",
}
return mock_data.get(symbol.upper(), f"Market sentiment for {symbol.upper()}: Unknown")
# 2. Define trading tools (approval required)
@tool(approval_mode="always_require")
def execute_trade(
symbol: Annotated[str, "The stock ticker symbol"],
action: Annotated[str, "Either 'buy' or 'sell'"],
quantity: Annotated[int, "Number of shares to trade"],
) -> str:
"""Execute a stock trade. Requires human approval due to financial impact."""
return f"Trade executed: {action.upper()} {quantity} shares of {symbol.upper()}"
@tool(approval_mode="never_require")
def get_portfolio_balance() -> str:
"""Get current portfolio balance and available funds."""
return "Portfolio: $50,000 invested, $10,000 cash available. Holdings: AAPL, GOOGL, MSFT."
def _print_output(event: WorkflowEvent) -> None:
if not event.data:
raise ValueError("WorkflowEvent has no data")
if not isinstance(event.data, list) and not all(isinstance(msg, Message) for msg in event.data):
raise ValueError("WorkflowEvent data is not a list of Message")
messages: list[Message] = event.data # type: ignore
print("\n" + "-" * 60)
print("Workflow completed. Aggregated results from both agents:")
for msg in messages:
if msg.text:
print(f"- {msg.author_name or msg.role}: {msg.text}")
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif event.type == "output":
_print_output(event)
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print(f"\nSimulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 3. Create two agents focused on different stocks but with the same tool sets
client = OpenAIChatClient()
microsoft_agent = client.as_agent(
name="MicrosoftAgent",
instructions=(
"You are a personal trading assistant focused on Microsoft (MSFT). "
"You manage my portfolio and take actions based on market data."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
)
google_agent = client.as_agent(
name="GoogleAgent",
instructions=(
"You are a personal trading assistant focused on Google (GOOGL). "
"You manage my trades and portfolio based on market conditions."
),
tools=[get_stock_price, get_market_sentiment, get_portfolio_balance, execute_trade],
)
# 4. Build a concurrent workflow with both agents
# ConcurrentBuilder requires at least 2 participants for fan-out
workflow = ConcurrentBuilder(participants=[microsoft_agent, google_agent]).build()
# 5. Start the workflow - both agents will process the same task in parallel
print("Starting concurrent workflow with tool approval...")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Manage my portfolio. Use a max of 5000 dollars to adjust my position using "
"your best judgment based on market sentiment. No need to confirm trades with me.",
stream=True,
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting concurrent workflow with tool approval...
------------------------------------------------------------
Approval requested for tool: execute_trade
Arguments: {"symbol":"MSFT","action":"buy","quantity":13}
Approval requested for tool: execute_trade
Arguments: {"symbol":"GOOGL","action":"buy","quantity":35}
Simulating human approval for: execute_trade
Simulating human approval for: execute_trade
------------------------------------------------------------
Workflow completed. Aggregated results from both agents:
- user: Manage my portfolio. Use a max of 5000 dollars to adjust my position using your best judgment based on
market sentiment. No need to confirm trades with me.
- MicrosoftAgent: I have successfully executed the trade, purchasing 13 shares of Microsoft (MSFT). This action
was based on the positive market sentiment and available funds within the specified limit.
Your portfolio has been adjusted accordingly.
- GoogleAgent: I have successfully executed the trade, purchasing 35 shares of GOOGL. If you need further
assistance or any adjustments, feel free to ask!
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,214 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterable
from typing import Annotated, cast
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
"""
Sample: Group Chat Workflow with Tool Approval Requests
This sample demonstrates how to use GroupChatBuilder with tools that require human
approval before execution. A group of specialized agents collaborate on a task, and
sensitive tool calls trigger human-in-the-loop approval.
This sample works as follows:
1. A GroupChatBuilder workflow is created with multiple specialized agents.
2. A selector function determines which agent speaks next based on conversation state.
3. Agents collaborate on a software deployment task.
4. When the deployment agent tries to deploy to production, it triggers an approval request.
5. The sample simulates human approval and the workflow completes.
Purpose:
Show how tool call approvals integrate with multi-agent group chat workflows where
different agents have different levels of tool access.
Demonstrate:
- Using set_select_speakers_func with agents that have approval-required tools.
- Handling request_info events (type='request_info') in group chat scenarios.
- Multi-round group chat with tool approval interruption and resumption.
Prerequisites:
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with GroupChatBuilder and streaming workflow events.
"""
# 1. Define tools for different agents
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def run_tests(test_suite: Annotated[str, "Name of the test suite to run"]) -> str:
"""Run automated tests for the application."""
return f"Test suite '{test_suite}' completed: 47 passed, 0 failed, 0 skipped"
@tool(approval_mode="never_require")
def check_staging_status() -> str:
"""Check the current status of the staging environment."""
return "Staging environment: Healthy, Version 2.3.0 deployed, All services running"
@tool(approval_mode="always_require")
def deploy_to_production(
version: Annotated[str, "The version to deploy"],
components: Annotated[str, "Comma-separated list of components to deploy"],
) -> str:
"""Deploy specified components to production. Requires human approval."""
return f"Production deployment complete: Version {version}, Components: {components}"
@tool(approval_mode="never_require")
def create_rollback_plan(version: Annotated[str, "The version being deployed"]) -> str:
"""Create a rollback plan for the deployment."""
return (
f"Rollback plan created for version {version}: "
"Automated rollback to v2.2.0 if health checks fail within 5 minutes"
)
# 2. Define the speaker selector function
def select_next_speaker(state: GroupChatState) -> str:
"""Select the next speaker based on the conversation flow.
This simple selector follows a predefined flow:
1. QA Engineer runs tests
2. DevOps Engineer checks staging and creates rollback plan
3. DevOps Engineer deploys to production (triggers approval)
"""
if not state.conversation:
raise RuntimeError("Conversation is empty; cannot select next speaker.")
if len(state.conversation) == 1:
return "QAEngineer" # First speaker
return "DevOpsEngineer" # Subsequent speakers
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("Workflow summary:")
outputs = cast(list[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 3. Create specialized agents
client = OpenAIChatClient()
qa_engineer = client.as_agent(
name="QAEngineer",
instructions=(
"You are a QA engineer responsible for running tests before deployment. "
"Run the appropriate test suites and report results clearly."
),
tools=[run_tests],
)
devops_engineer = client.as_agent(
name="DevOpsEngineer",
instructions=(
"You are a DevOps engineer responsible for deployments. First check staging "
"status and create a rollback plan, then proceed with production deployment. "
"Always ensure safety measures are in place before deploying."
),
tools=[check_staging_status, create_rollback_plan, deploy_to_production],
)
# 4. Build a group chat workflow with the selector function
# max_rounds=4: Set a hard limit to 4 rounds
# First round: QAEngineer speaks
# Second round: DevOpsEngineer speaks (check staging + create rollback)
# Third round: DevOpsEngineer speaks with an approval request (deploy to production)
# Fourth round: DevOpsEngineer speaks again after approval
workflow = GroupChatBuilder(
participants=[qa_engineer, devops_engineer],
max_rounds=4,
selection_func=select_next_speaker,
).build()
# 5. Start the workflow
print("Starting group chat workflow for software deployment...")
print(f"Agents: {[qa_engineer.name, devops_engineer.name]}")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"We need to deploy version 2.4.0 to production. Please coordinate the deployment.", stream=True
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting group chat workflow for software deployment...
Agents: QA Engineer, DevOps Engineer
------------------------------------------------------------
[QAEngineer]: Running the integration test suite to verify the application
before deployment... Test suite 'integration' completed: 47 passed, 0 failed.
All tests passing - ready for deployment.
[DevOpsEngineer]: Checking staging environment status... Staging is healthy
with version 2.3.0. Creating rollback plan for version 2.4.0... Rollback plan
created with automated rollback to v2.2.0 if health checks fail.
[APPROVAL REQUIRED]
Tool: deploy_to_production
Arguments: {"version": "2.4.0", "components": "api,web,worker"}
============================================================
Human review required for production deployment!
In a real scenario, you would review the deployment details here.
Simulating approval for demo purposes...
============================================================
[DevOpsEngineer]: Production deployment complete! Version 2.4.0 has been
successfully deployed with components: api, web, worker.
------------------------------------------------------------
Deployment workflow completed successfully!
All agents have finished their tasks.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,153 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections.abc import AsyncIterable
from typing import Annotated, cast
from agent_framework import (
Content,
Message,
WorkflowEvent,
tool,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.orchestrations import SequentialBuilder
"""
Sample: Sequential Workflow with Tool Approval Requests
This sample demonstrates how to use SequentialBuilder with tools that require human
approval before execution. The approval flow uses the existing @tool decorator
with approval_mode="always_require" to trigger human-in-the-loop interactions.
This sample works as follows:
1. A SequentialBuilder workflow is created with a single agent that has tools requiring approval.
2. The agent receives a user task and determines it needs to call a sensitive tool.
3. The tool call triggers a function_approval_request Content, pausing the workflow.
4. The sample simulates human approval by responding to the .
5. Once approved, the tool executes and the agent completes its response.
6. The workflow outputs the final conversation with all messages.
Purpose:
Show how tool call approvals integrate seamlessly with SequentialBuilder without
requiring any additional builder configuration.
Demonstrate:
- Using @tool(approval_mode="always_require") for sensitive operations.
- Handling request_info events with function_approval_request Content in sequential workflows.
- Resuming workflow execution after approval via run(responses=..., stream=True).
Prerequisites:
- OpenAI or Azure OpenAI configured with the required environment variables.
- Basic familiarity with SequentialBuilder and streaming workflow events.
"""
# 1. Define tools - one requiring approval, one that doesn't
@tool(approval_mode="always_require")
def execute_database_query(
query: Annotated[str, "The SQL query to execute against the production database"],
) -> str:
"""Execute a SQL query against the production database. Requires human approval."""
# In a real implementation, this would execute the query
return f"Query executed successfully. Results: 3 rows affected by '{query}'"
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py and
# samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_database_schema() -> str:
"""Get the current database schema. Does not require approval."""
return """
Tables:
- users (id, name, email, created_at)
- orders (id, user_id, total, status, created_at)
- products (id, name, price, stock)
"""
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, Content] | None:
"""Process events from the workflow stream to capture human feedback requests."""
requests: dict[str, Content] = {}
async for event in stream:
if event.type == "request_info" and isinstance(event.data, Content):
# We are only expecting tool approval requests in this sample
requests[event.request_id] = event.data
elif event.type == "output":
# The output of the workflow comes from the orchestrator and it's a list of messages
print("\n" + "=" * 60)
print("Workflow summary:")
outputs = cast(list[Message], event.data)
for msg in outputs:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
responses: dict[str, Content] = {}
if requests:
for request_id, request in requests.items():
if request.type == "function_approval_request":
print("\n[APPROVAL REQUIRED]")
print(f" Tool: {request.function_call.name}") # type: ignore
print(f" Arguments: {request.function_call.arguments}") # type: ignore
print(f"Simulating human approval for: {request.function_call.name}") # type: ignore
# Create approval response
responses[request_id] = request.to_function_approval_response(approved=True)
return responses if responses else None
async def main() -> None:
# 2. Create the agent with tools (approval mode is set per-tool via decorator)
client = OpenAIChatClient()
database_agent = client.as_agent(
name="DatabaseAgent",
instructions=(
"You are a database assistant. You can view the database schema and execute "
"queries. Always check the schema before running queries. Be careful with "
"queries that modify data."
),
tools=[get_database_schema, execute_database_query],
)
# 3. Build a sequential workflow with the agent
workflow = SequentialBuilder(participants=[database_agent]).build()
# 4. Start the workflow with a user task
print("Starting sequential workflow with tool approval...")
print("-" * 60)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Check the schema and then update all orders with status 'pending' to 'processing'", stream=True
)
pending_responses = await process_event_stream(stream)
while pending_responses is not None:
# Run the workflow until there is no more human feedback to provide,
# in which case this workflow completes.
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await process_event_stream(stream)
"""
Sample Output:
Starting sequential workflow with tool approval...
------------------------------------------------------------
Approval requested for tool: execute_database_query
Arguments: {"query": "UPDATE orders SET status = 'processing' WHERE status = 'pending'"}
Simulating human approval (auto-approving for demo)...
------------------------------------------------------------
Workflow completed. Final conversation:
[user]: Check the schema and then update all orders with status 'pending' to 'processing'
[assistant]: I've checked the schema and executed the update query. The query
"UPDATE orders SET status = 'processing' WHERE status = 'pending'"
was executed successfully, affecting 3 rows.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from dataclasses import dataclass
from agent_framework import (
@@ -14,7 +15,7 @@ from agent_framework import (
WorkflowViz,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from typing_extensions import Never
@@ -27,7 +28,8 @@ What it does:
- Visualization: generate Mermaid and GraphViz representations via `WorkflowViz` and optionally export SVG.
Prerequisites:
- Azure AI/ Azure OpenAI for `AzureOpenAIChatClient` agents.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure AI/ Azure OpenAI for `AzureOpenAIResponsesClient` agents.
- Authentication via `azure-identity` — uses `AzureCliCredential()` (run `az login`).
- For visualization export: `pip install graphviz>=0.20.0` and install GraphViz binaries.
"""
@@ -90,7 +92,11 @@ async def main() -> None:
# Create agent instances
researcher = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
@@ -100,7 +106,11 @@ async def main() -> None:
)
marketer = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
@@ -110,7 +120,11 @@ async def main() -> None:
)
legal = AgentExecutor(
AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
).as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
" based on the prompt."