chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Azure AI Agents 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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be the deployment name of a model in your Foundry project.
- 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:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create two agents: a Writer and a Reviewer.
writer_agent = Agent(
client=client,
name="Writer",
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
reviewer_agent = Agent(
client=client,
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."
),
)
# 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()
# 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__":
asyncio.run(main())
@@ -0,0 +1,113 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
InMemoryHistoryProvider,
WorkflowBuilder,
WorkflowContext,
WorkflowRunState,
executor,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Agents with a shared thread in a workflow
A Writer agent generates content, then a Reviewer agent critiques it, sharing a common message thread.
Purpose:
Show how to use a shared thread between multiple agents in a workflow.
By default, agents have individual threads, but sharing a thread allows them to share all messages.
Notes:
- Not all agents can share threads; usually only the same type of agents can share threads.
Demonstrate:
- Creating multiple agents with FoundryChatClient.
- Setting up a shared thread between agents.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL 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.
"""
@executor(id="intercept_agent_response")
async def intercept_agent_response(
agent_response: AgentExecutorResponse, ctx: WorkflowContext[AgentExecutorRequest]
) -> None:
"""This executor intercepts the agent response and sends a request without messages.
This essentially prevents duplication of messages in the shared thread. Without this
executor, the response will be added to the thread as input of the next agent call.
"""
await ctx.send_message(AgentExecutorRequest(messages=[]))
async def main() -> None:
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# set the same context provider (same default source_id) for both agents to share the thread
writer = Agent(
client=client,
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
context_providers=[InMemoryHistoryProvider()],
)
reviewer = Agent(
client=client,
instructions=("You are a thoughtful reviewer. Give brief feedback on the previous assistant message."),
name="reviewer",
context_providers=[InMemoryHistoryProvider()],
)
# Create the shared session
shared_session = writer.create_session()
writer_executor = AgentExecutor(writer, session=shared_session)
reviewer_executor = AgentExecutor(reviewer, session=shared_session)
workflow = (
WorkflowBuilder(start_executor=writer_executor)
.add_chain([writer_executor, intercept_agent_response, reviewer_executor])
.build()
)
result = await workflow.run(
"Write a tagline for a budget-friendly eBike.",
# client_kwargs are forwarded to each underlying chat client call.
# store=False tells the model API not to persist messages server-side
# for this example.
client_kwargs={"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 session now contains the conversation between the writer and reviewer. Print it out.
print("=== Shared Session Conversation ===")
memory_state = shared_session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
for message in memory_state.get("messages", []):
print(f"{message.author_name or message.role}: {message.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,159 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Final
from agent_framework import (
Agent,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponseUpdate,
Message,
WorkflowBuilder,
WorkflowContext,
executor,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: AzureOpenAI Chat Agents and an Executor in a Workflow with Streaming
Pipeline layout:
research_agent -> enrich_with_references (@executor) -> final_editor_agent
The first agent drafts a short answer. A lightweight @executor function simulates
an external data fetch and injects a follow-up user message containing extra context.
The final agent incorporates the new note and produces the polished output.
Demonstrates:
- Using the @executor decorator to create a function-style Workflow node.
- Consuming an AgentExecutorResponse and forwarding an AgentExecutorRequest for the next agent.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Run `az login` before executing.
"""
# Simulated external content keyed by a simple topic hint.
EXTERNAL_REFERENCES: Final[dict[str, str]] = {
"workspace": (
"From Workspace Weekly: Adjustable monitor arms and sit-stand desks can reduce "
"neck strain by up to 30%. Consider adding a reminder to move every 45 minutes."
),
"travel": (
"Checklist excerpt: Always confirm baggage limits for budget airlines. "
"Keep a photocopy of your passport stored separately from the original."
),
"wellness": (
"Recent survey: Employees who take two 5-minute breaks per hour report 18% higher focus "
"scores. Encourage scheduling micro-breaks alongside hydration reminders."
),
}
def _lookup_external_note(prompt: str) -> str | None:
"""Return the first matching external note based on a keyword search."""
lowered = prompt.lower()
for keyword, note in EXTERNAL_REFERENCES.items():
if keyword in lowered:
return note
return None
@executor(id="enrich_with_references")
async def enrich_with_references(
draft: AgentExecutorResponse,
ctx: WorkflowContext[AgentExecutorRequest],
) -> None:
"""Inject a follow-up user instruction that adds an external note for the next agent.
Args:
draft: The response from the research_agent containing the initial draft. This is
a `AgentExecutorResponse` because agents in workflows send their full response
wrapped in this type to connected executors.
ctx: The workflow context to send the next request.
"""
conversation = list(draft.full_conversation or draft.agent_response.messages)
original_prompt = next((message.text for message in conversation if message.role == "user"), "")
external_note = _lookup_external_note(original_prompt) or (
"No additional references were found. Please refine the previous assistant response for clarity."
)
follow_up = (
"External knowledge snippet:\n"
f"{external_note}\n\n"
"Please update the prior assistant answer so it weaves this note into the guidance."
)
conversation.append(Message("user", [follow_up]))
# Output a new AgentExecutorRequest for the next agent in the workflow.
# Agents in workflows handle this type and will generate a response based on the request.
await ctx.send_message(AgentExecutorRequest(messages=conversation))
async def main() -> None:
"""Run the workflow and stream combined updates from both agents."""
# Create the agents
research_agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="research_agent",
instructions=(
"Produce a short, bullet-style briefing with two actionable ideas. Label the section as 'Initial Draft'."
),
)
final_editor_agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="final_editor_agent",
instructions=(
"Use all conversation context (including external notes) to produce the final answer. "
"Merge the draft and extra note into a concise recommendation under 150 words."
),
)
workflow = (
WorkflowBuilder(start_executor=research_agent)
.add_edge(research_agent, enrich_with_references)
.add_edge(enrich_with_references, final_editor_agent)
.build()
)
events = workflow.run(
"Create quick workspace wellness tips for a remote analyst working across two monitors.", stream=True
)
# Track the last author to format streaming output.
last_author: str | None = None
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("\n") # 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__":
asyncio.run(main())
@@ -0,0 +1,83 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent, AgentResponseUpdate, WorkflowBuilder
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL 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():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the agents
_writer_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
writer_agent = Agent(
client=_writer_client,
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
name="writer",
)
_reviewer_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
reviewer_agent = Agent(
client=_reviewer_client,
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."
),
name="reviewer",
)
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
# 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()
# 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__":
asyncio.run(main())
@@ -0,0 +1,323 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from collections.abc import AsyncIterable
from dataclasses import dataclass, field
from typing import Annotated
from agent_framework import (
Agent,
AgentExecutor,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponse,
Executor,
Message,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
handler,
response_handler,
tool,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
from typing_extensions import Never
# Load environment variables from .env file
load_dotenv()
"""
Sample: Tool-enabled agents with human feedback
Pipeline layout:
writer_agent (uses Azure OpenAI tools) -> Coordinator -> writer_agent
-> Coordinator -> final_editor_agent -> Coordinator -> output
The writer agent calls tools to gather product facts before drafting copy. A custom executor
packages the draft and emits a request_info event (type='request_info') so a human can comment, then replays the human
guidance back into the conversation before the final editor agent produces the polished output.
Demonstrates:
- Attaching Python function tools to an agent inside a workflow.
- Capturing the writer's output for human review.
- Streaming AgentRunUpdateEvent updates alongside human-in-the-loop pauses.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Authentication via azure-identity. Run `az login` before executing.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/02-agents/tools/function_tool_with_approval.py and
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def fetch_product_brief(
product_name: Annotated[str, Field(description="Product name to look up.")],
) -> str:
"""Return a marketing brief for a product."""
briefs = {
"lumenx desk lamp": (
"Product: LumenX Desk Lamp\n"
"- Three-point adjustable arm with 270° rotation.\n"
"- Custom warm-to-neutral LED spectrum (2700K-4000K).\n"
"- USB-C charging pad integrated in the base.\n"
"- Designed for home offices and late-night study sessions."
)
}
return briefs.get(product_name.lower(), f"No stored brief for '{product_name}'.")
@tool(approval_mode="never_require")
def get_brand_voice_profile(
voice_name: Annotated[str, Field(description="Brand or campaign voice to emulate.")],
) -> str:
"""Return guidance for the requested brand voice."""
voices = {
"lumenx launch": (
"Voice guidelines:\n"
"- Friendly and modern with concise sentences.\n"
"- Highlight practical benefits before aesthetics.\n"
"- End with an invitation to imagine the product in daily use."
)
}
return voices.get(voice_name.lower(), f"No stored voice profile for '{voice_name}'.")
@dataclass
class DraftFeedbackRequest:
"""Payload sent for human review."""
prompt: str = ""
draft_text: str = ""
conversation: list[Message] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
class Coordinator(Executor):
"""Bridge between the writer agent, human feedback, and final editor."""
def __init__(self, id: str, writer_id: str, final_editor_id: str) -> None:
super().__init__(id)
self.writer_id = writer_id
self.final_editor_id = final_editor_id
@handler
async def on_writer_response(
self,
draft: AgentExecutorResponse,
ctx: WorkflowContext[Never, AgentResponse],
) -> None:
"""Handle responses from the other two agents in the workflow."""
if draft.executor_id == self.final_editor_id:
# Final editor response; yield output directly.
await ctx.yield_output(draft.agent_response)
return
# Writer agent response; request human feedback.
# Preserve the full conversation so the final editor
# can see tool traces and the initial prompt.
conversation = list(draft.full_conversation)
draft_text = draft.agent_response.text.strip()
if not draft_text:
draft_text = "No draft text was produced."
prompt = (
"Review the draft from the writer and provide a short directional note "
"(tone tweaks, must-have detail, target audience, etc.). "
"Keep it under 30 words."
)
await ctx.request_info(
request_data=DraftFeedbackRequest(prompt=prompt, draft_text=draft_text, conversation=conversation),
response_type=str,
)
@response_handler
async def on_human_feedback(
self,
original_request: DraftFeedbackRequest,
feedback: str,
ctx: WorkflowContext[AgentExecutorRequest],
) -> None:
note = feedback.strip()
if note.lower() == "approve":
# Human approved the draft as-is; forward it unchanged.
await ctx.send_message(
AgentExecutorRequest(
messages=[
*original_request.conversation,
*[Message("user", contents=["The draft is approved as-is."])],
],
should_respond=True,
),
target_id=self.final_editor_id,
)
return
# Human provided feedback; prompt the writer to revise.
instruction = (
"A human reviewer shared the following guidance:\n"
f"{note or 'No specific guidance provided.'}\n\n"
"Rewrite the draft from the previous assistant message into a polished final version. "
"Keep the response under 120 words and reflect any requested tone adjustments."
)
await ctx.send_message(
AgentExecutorRequest(messages=[Message("user", contents=[instruction])], should_respond=True),
target_id=self.writer_id,
)
def create_writer_agent() -> Agent:
"""Creates a writer agent with tools."""
return Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="writer_agent",
instructions=(
"You are a marketing writer. Call the available tools before drafting copy so you are precise. "
"Always call both tools once before drafting. Summarize tool outputs as bullet points, then "
"produce a 3-sentence draft."
),
tools=[fetch_product_brief, get_brand_voice_profile],
default_options={
"tool_choice": "required",
},
)
def create_final_editor_agent() -> Agent:
"""Creates a final editor agent."""
return Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="final_editor_agent",
instructions=(
"You are an editor who polishes marketing copy after human approval. "
"Correct any legal or factual issues. Return the final version even if no changes are made. "
),
)
def display_agent_run_update(event: WorkflowEvent, last_executor: str | None) -> None:
"""Display an AgentRunUpdateEvent in a readable format."""
printed_tool_calls: set[str] = set()
printed_tool_results: set[str] = set()
executor_id = event.executor_id
update = event.data
# Extract and print any new tool calls or results from the update.
function_calls = [c for c in update.contents if c.type == "function_call"] # type: ignore[union-attr]
function_results = [c for c in update.contents if c.type == "function_result"] # type: ignore[union-attr]
if executor_id != last_executor:
if last_executor is not None:
print()
print(f"{executor_id}:", end=" ", flush=True)
last_executor = executor_id
# Print any new tool calls before the text update.
for call in function_calls:
if call.call_id in printed_tool_calls:
continue
printed_tool_calls.add(call.call_id)
args = call.arguments
args_preview = json.dumps(args, ensure_ascii=False) if isinstance(args, dict) else (args or "").strip()
print(
f"\n{executor_id} [tool-call] {call.name}({args_preview})",
flush=True,
)
print(f"{executor_id}:", end=" ", flush=True)
# Print any new tool results before the text update.
for result in function_results:
if result.call_id in printed_tool_results:
continue
printed_tool_results.add(result.call_id)
result_text = result.result
if not isinstance(result_text, str):
result_text = json.dumps(result_text, ensure_ascii=False)
print(
f"\n{executor_id} [tool-result] {result.call_id}: {result_text}",
flush=True,
)
print(f"{executor_id}:", end=" ", flush=True)
# Finally, print the text update.
print(update, end="", flush=True)
async def consume_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, str] | None:
"""Consume a workflow event stream, printing outputs and returning any pending human responses."""
requests: list[WorkflowEvent] = []
async for event in stream:
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
# Stash the request so we can prompt the human after the stream completes.
requests.append(event)
if requests:
pending_responses: dict[str, str] = {}
for request in requests:
print("\n----- Writer draft -----")
print(request.data.draft_text.strip())
print("\nProvide guidance for the editor (or 'approve' to accept the draft).")
answer = input("Human feedback: ").strip() # noqa: ASYNC250
if answer.lower() == "exit":
print("Exiting...")
exit(0)
pending_responses[request.request_id] = answer
return pending_responses
return None
async def main() -> None:
"""Run the workflow and bridge human feedback between two agents."""
# Build the workflow.
writer_agent = AgentExecutor(create_writer_agent())
final_editor_agent = AgentExecutor(create_final_editor_agent())
coordinator = Coordinator(
id="coordinator",
writer_id="writer_agent",
final_editor_id="final_editor_agent",
)
workflow = (
WorkflowBuilder(start_executor=writer_agent)
.add_edge(writer_agent, coordinator)
.add_edge(coordinator, writer_agent)
.add_edge(final_editor_agent, coordinator)
.add_edge(coordinator, final_editor_agent)
.build()
)
print(
"Interactive mode. When prompted, provide a short feedback note for the editor.",
flush=True,
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run(
"Create a short launch blurb for the LumenX desk lamp. Emphasize adjustability and warm lighting.",
stream=True,
)
pending_responses = await consume_stream(stream)
# Run until there are no more requests
while pending_responses is not None:
stream = workflow.run(stream=True, responses=pending_responses)
pending_responses = await consume_stream(stream)
print("Workflow complete.")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 `Agent(client=workflow,...)` 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 `Agent(client=workflow,...)`.
- Workflow completion when idle with no pending work
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Familiarity with Workflow events (WorkflowEvent with type "output")
"""
async def main() -> None:
# 1) Create three domain agents using FoundryChatClient
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
researcher = Agent(
client=client,
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
" opportunities, and risks."
),
name="researcher",
)
marketer = Agent(
client=client,
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
" aligned to the prompt."
),
name="marketer",
)
legal = Agent(
client=client,
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()
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())
@@ -0,0 +1,146 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
Executor,
Message,
WorkflowBuilder,
WorkflowContext,
handler,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Custom Agent Executors in a Workflow
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 FoundryChatClient 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 wraps the agent in a more sophisticated executor.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL 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, executors, edges, events, and streaming or non streaming runs.
"""
class Writer(Executor):
"""Custom executor that owns a domain specific agent responsible for generating content.
This class demonstrates:
- Attaching a Agent to an Executor so it participates as a node in a workflow.
- Using a @handler method to accept a typed input and forward a typed output via ctx.send_message.
"""
agent: Agent
def __init__(self, id: str = "writer"):
# Create a domain specific agent using your configured FoundryChatClient.
self.agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
instructions=(
"You are an excellent content writer. You create new content and edit contents based on the feedback."
),
)
# Associate the agent with this executor node. The base Executor stores it on self.agent.
super().__init__(id=id)
@handler
async def handle(self, message: Message, ctx: WorkflowContext[list[Message], str]) -> None:
"""Generate content using the agent and forward the updated conversation.
Contract for this handler:
- message is the inbound user Message.
- ctx is a WorkflowContext that expects a list[Message] to be sent downstream.
Pattern shown here:
1) Seed the conversation with the inbound message.
2) Run the attached agent to produce assistant messages.
3) Forward the cumulative messages to the next executor with ctx.send_message.
"""
# Start the conversation with the incoming user message.
messages: list[Message] = [message]
# Run the agent and extend the conversation with the agent's messages.
response = await self.agent.run(messages)
messages.extend(response.messages)
# Forward the accumulated messages to the next executor in the workflow.
await ctx.send_message(messages)
class Reviewer(Executor):
"""Custom executor that owns a review agent and completes the workflow.
This class demonstrates:
- Consuming a typed payload produced upstream.
- Yielding the final text outcome to complete the workflow.
"""
agent: Agent
def __init__(self, id: str = "reviewer"):
# Create a domain specific agent that evaluates and refines content.
self.agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
instructions=(
"You are an excellent content reviewer. You review the content and provide feedback to the writer."
),
)
super().__init__(id=id)
@handler
async def handle(self, messages: list[Message], ctx: WorkflowContext[list[Message], str]) -> None:
"""Review the full conversation transcript and complete with a final string.
This node consumes all messages so far. It uses its agent to produce the final text,
then signals completion by yielding the output.
"""
response = await self.agent.run(messages)
await ctx.yield_output(response.text)
async def main():
"""Build and run a simple two node agent workflow: Writer then Reviewer."""
# Create the executors
writer = Writer()
reviewer = Reviewer()
# Build the workflow using the fluent builder.
# Set the start node and connect an edge from writer to reviewer.
workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build()
# Run the workflow with the user's initial message.
# For foundational clarity, use run (non streaming) and print the workflow output.
events = await workflow.run(
Message("user", ["Create a slogan for a new electric SUV that is affordable and fun to drive."])
)
# The terminal node yields output; print its contents.
outputs = events.get_outputs()
if outputs:
print(outputs[-1])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,92 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Environment variables configured for `FoundryChatClient`.
"""
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=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
writer = Agent(
name="Writer",
description="Synthesizes a polished answer using the gathered notes.",
instructions="Compose clear and structured answers using any notes provided.",
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
_orch_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Mark participant responses as intermediate so workflow.as_agent() maps
# them to text_reasoning content while the final answer remains normal text.
workflow = GroupChatBuilder(
participants=[researcher, writer],
intermediate_output_from=[researcher, writer],
orchestrator_agent=Agent(
client=_orch_client,
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()
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())
@@ -0,0 +1,235 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated
from agent_framework import (
Agent,
AgentResponse,
Content,
Message,
WorkflowAgent,
tool,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""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:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- `az login` (Azure CLI authentication)
- Environment variables configured for FoundryChatClient (FOUNDRY_MODEL)
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/02-agents/tools/function_tool_with_approval.py
# samples/02-agents/tools/function_tool_with_approval_and_sessions.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: FoundryChatClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The FoundryChatClient 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 = Agent(
client=client,
instructions=(
"You are frontline support triage. Route customer issues to the appropriate specialist agents "
"based on the problem described."
),
name="triage_agent",
require_per_service_call_history_persistence=True,
)
# Refund specialist: Handles refund requests
refund_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
# Order/shipping specialist: Resolves delivery issues
order_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
# Return specialist: Handles return requests
return_agent = Agent(
client=client,
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],
require_per_service_call_history_persistence=True,
)
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" and content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
request_function_args = WorkflowAgent.RequestInfoFunctionArgs.from_dict(content.arguments) # type: ignore
request_id = request_function_args.request_id
request_event = request_function_args.request_event
pending_requests[request_id] = request_event.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 = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
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()
)
# 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("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())
@@ -0,0 +1,119 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import (
Agent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import MagenticBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 `Agent(client=workflow, ...)` so the entire Magentic loop can be reused
like any other agent while still emitting callback telemetry.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
"""
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=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
# Create code interpreter tool using instance method
coder_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
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=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
print("\nBuilding Magentic Workflow...")
# Mark participant responses as intermediate so workflow.as_agent() maps
# them to text_reasoning content while the final answer remains normal text.
workflow = MagenticBuilder(
participants=[researcher_agent, coder_agent],
intermediate_output_from=[researcher_agent, coder_agent],
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()
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())
@@ -0,0 +1,87 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
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 `Agent(client=workflow,...)` 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:
- FOUNDRY_PROJECT_ENDPOINT must be set to the Azure Foundry project endpoint.
- FOUNDRY_MODEL must be set to the model name for the Foundry chat client.
"""
async def main() -> None:
# 1) Create agents
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
writer = Agent(
client=client,
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
name="writer",
)
reviewer = Agent(
client=client,
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()
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:
===== Conversation =====
------------------------------------------------------------
01 [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.
Note:
`workflow.as_agent()` returns ONLY the final agent's response (the "answer") — the prior agents' work
is not included in the response. To preserve earlier participant replies while running as an agent, build with
`SequentialBuilder(participants=[...], intermediate_output_from=[writer])`; intermediate workflow events become
`text_reasoning` content on the AgentResponse, while `.text` remains terminal-output only.
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,166 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Ensure local package can be imported when running as a script.
_SAMPLES_ROOT = Path(__file__).resolve().parents[3]
if str(_SAMPLES_ROOT) not in sys.path:
sys.path.insert(0, str(_SAMPLES_ROOT))
# Also add the current directory for sibling imports
_CURRENT_DIR = str(Path(__file__).resolve().parent)
if _CURRENT_DIR not in sys.path:
sys.path.insert(0, _CURRENT_DIR)
from agent_framework import ( # noqa: E402
Content,
Executor,
Message,
WorkflowAgent,
WorkflowBuilder,
WorkflowContext,
handler,
response_handler,
)
from workflow_as_agent_reflection_pattern import ( # pyrefly: ignore[missing-import] # noqa: E402
ReviewRequest,
ReviewResponse,
Worker,
)
"""
Sample: Workflow Agent with Human-in-the-Loop
Purpose:
This sample demonstrates how to build a workflow agent that escalates uncertain
decisions to a human manager. A Worker generates results, while a Reviewer
evaluates them. When the Reviewer is not confident, it escalates the decision
to a human, receives the human response, and then forwards that response back
to the Worker. The workflow completes when idle.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- 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
workflow_as_agent_reflection.py.
"""
# Load environment variables from .env file
load_dotenv()
@dataclass
class HumanReviewRequest:
"""A request message type for escalation to a human reviewer."""
agent_request: ReviewRequest | None = None
class ReviewerWithHumanInTheLoop(Executor):
"""Executor that always escalates reviews to a human manager."""
def __init__(self, worker_id: str, reviewer_id: str | None = None) -> None:
unique_id = reviewer_id or f"{worker_id}-reviewer"
super().__init__(id=unique_id)
self._worker_id = worker_id
@handler
async def review(self, request: ReviewRequest, ctx: WorkflowContext) -> None:
# In this simplified example, we always escalate to a human manager.
# See workflow_as_agent_reflection.py for an implementation
# using an automated agent to make the review decision.
print(f"Reviewer: Evaluating response for request {request.request_id[:8]}...")
print("Reviewer: Escalating to human manager...")
# Forward the request to a human manager by sending a HumanReviewRequest.
await ctx.request_info(request_data=HumanReviewRequest(agent_request=request), response_type=ReviewResponse)
@response_handler
async def accept_human_review(
self,
original_request: HumanReviewRequest,
response: ReviewResponse,
ctx: WorkflowContext[ReviewResponse],
) -> None:
# Accept the human review response and forward it back to the Worker.
print(f"Reviewer: Accepting human review for request {response.request_id[:8]}...")
print(f"Reviewer: Human feedback: {response.feedback}")
print(f"Reviewer: Human approved: {response.approved}")
print("Reviewer: Forwarding human review back to worker...")
await ctx.send_message(response, target_id=self._worker_id)
async def main() -> None:
print("Starting Workflow Agent with Human-in-the-Loop Demo")
print("=" * 50)
print("Building workflow with Worker-Reviewer cycle...")
# Build a workflow with bidirectional communication between Worker and Reviewer,
# and escalation paths for human review.
worker = Worker(
id="worker",
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
reviewer = ReviewerWithHumanInTheLoop(worker_id="worker")
agent = (
WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_agent()
)
print("Running workflow agent with user query...")
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
print("-" * 50)
# Run the agent with an initial query.
response = await agent.run(
"Write code for parallel reading 1 million Files on disk and write to a sorted output file."
)
# Locate the human review function call in the response messages.
human_review_function_call: Content | None = None
for message in response.messages:
for content in message.contents:
if content.name == WorkflowAgent.REQUEST_INFO_FUNCTION_NAME:
human_review_function_call = content
# Handle the human review if required.
if human_review_function_call:
# Parse the human review request arguments.
human_request_args = WorkflowAgent.RequestInfoFunctionArgs.from_dict(human_review_function_call.arguments) # type: ignore
request_payload = human_request_args.request_event.data
if not isinstance(request_payload, HumanReviewRequest):
raise ValueError("Human review request payload must be a HumanReviewRequest.")
if not request_payload.agent_request:
raise ValueError("Human review request must contain an agent_request.")
# Mock a human response approval for demonstration purposes.
human_response = ReviewResponse(request_id=request_payload.agent_request.request_id, feedback="", approved=True)
# Create the function call result object to send back to the agent.
human_review_function_result = Content(
"function_result",
call_id=human_review_function_call.call_id, # type: ignore
result=human_response,
)
# Send the human review result back to the agent.
response = await agent.run(Message("tool", [human_review_function_result]))
print(f"📤 Agent Response: {response.messages[-1].text}")
print("=" * 50)
print("Workflow completed!")
if __name__ == "__main__":
print("Initializing Workflow as Agent Sample...")
asyncio.run(main())
@@ -0,0 +1,156 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from typing import Annotated, Any
from agent_framework import Agent, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
Sample: Workflow as Agent with kwargs Propagation to @tool Tools
This sample demonstrates how to flow custom context (skill data, user tokens, etc.)
through a workflow exposed Agent(client=via,) to @tool functions using the **kwargs pattern.
Key Concepts:
- Build a workflow using SequentialBuilder (or any builder pattern)
- Expose the workflow as a reusable agent via Agent(client=workflow,)
- Pass custom context as kwargs when invoking workflow_agent.run()
- kwargs are stored in State and propagated to all agent invocations
- @tool functions receive kwargs via **kwargs parameter
When to use Agent(client=workflow,):
- To treat an entire workflow orchestration as a single agent
- To compose workflows into higher-level orchestrations
- To maintain a consistent agent interface for callers
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
"""
# Define tools that accept custom context via **kwargs
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and
# samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
def get_user_data(
query: Annotated[str, Field(description="What user data to retrieve")],
**kwargs: Any,
) -> str:
"""Retrieve user-specific data based on the authenticated context."""
user_token = kwargs.get("user_token", {})
user_name = user_token.get("user_name", "anonymous")
access_level = user_token.get("access_level", "none")
print(f"\n[get_user_data] Received kwargs keys: {list(kwargs.keys())}")
print(f"[get_user_data] User: {user_name}")
print(f"[get_user_data] Access level: {access_level}")
return f"Retrieved data for user {user_name} with {access_level} access: {query}"
@tool(approval_mode="never_require")
def call_api(
endpoint_name: Annotated[str, Field(description="Name of the API endpoint to call")],
**kwargs: Any,
) -> str:
"""Call an API using the configured endpoints from custom_data."""
custom_data = kwargs.get("custom_data", {})
api_config = custom_data.get("api_config", {})
base_url = api_config.get("base_url", "unknown")
endpoints = api_config.get("endpoints", {})
print(f"\n[call_api] Received kwargs keys: {list(kwargs.keys())}")
print(f"[call_api] Base URL: {base_url}")
print(f"[call_api] Available endpoints: {list(endpoints.keys())}")
if endpoint_name in endpoints:
return f"Called {base_url}{endpoints[endpoint_name]} successfully"
return f"Endpoint '{endpoint_name}' not found in configuration"
async def main() -> None:
print("=" * 70)
print("Workflow as Agent kwargs Flow Demo")
print("=" * 70)
# Create chat client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agent with tools that use kwargs
agent = Agent(
client=client,
name="assistant",
instructions=(
"You are a helpful assistant. Use the available tools to help users. "
"When asked about user data, use get_user_data. "
"When asked to call an API, use call_api."
),
tools=[get_user_data, call_api],
)
# Build a sequential workflow
workflow = SequentialBuilder(participants=[agent]).build()
# Expose the workflow as an agent Agent(client=using,)
workflow_agent = workflow.as_agent()
# Define custom context that will flow to tools via kwargs
custom_data = {
"api_config": {
"base_url": "https://api.example.com",
"endpoints": {
"users": "/v1/users",
"orders": "/v1/orders",
"products": "/v1/products",
},
},
}
user_token = {
"user_name": "bob@contoso.com",
"access_level": "admin",
}
print("\nCustom Data being passed:")
print(json.dumps(custom_data, indent=2))
print(f"\nUser: {user_token['user_name']}")
print("\n" + "-" * 70)
print("Workflow Agent Execution (watch for [tool_name] logs showing kwargs received):")
print("-" * 70)
# Run workflow agent with kwargs - these will flow through to tools
# Note: kwargs are passed to workflow.run()
print("\n===== Streaming Response =====")
async for update in workflow_agent.run(
"Please get my user data and then call the users API endpoint.",
function_invocation_kwargs={"custom_data": custom_data, "user_token": user_token},
stream=True,
):
if update.text:
print(update.text, end="", flush=True)
print()
print("\n" + "=" * 70)
print("Sample Complete")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,235 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from dataclasses import dataclass
from uuid import uuid4
from agent_framework import (
AgentResponse,
Executor,
Message,
SupportsChatGetResponse,
WorkflowBuilder,
WorkflowContext,
handler,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel
# Load environment variables from .env file
load_dotenv()
"""
Sample: Workflow as Agent with Reflection and Retry Pattern
Purpose:
This sample demonstrates how to wrap a workflow as an agent using WorkflowAgent.
It uses a reflection pattern where a Worker executor generates responses and a
Reviewer executor evaluates them. If the response is not approved, the Worker
regenerates the output based on feedback until the Reviewer approves it. Only
approved responses are emitted to the external consumer. The workflow completes when idle.
Key Concepts Demonstrated:
- WorkflowAgent: Wraps a workflow to behave like a regular agent.
- Cyclic workflow design (Worker ↔ Reviewer) for iterative improvement.
- Structured output parsing for review feedback using Pydantic.
- State management for pending requests and retry logic.
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
- Familiarity with WorkflowBuilder, Executor, WorkflowContext, and event handling.
- Understanding of how agent messages are generated, reviewed, and re-submitted.
"""
@dataclass
class ReviewRequest:
"""Structured request passed from Worker to Reviewer for evaluation."""
request_id: str
user_messages: list[Message]
agent_messages: list[Message]
@dataclass
class ReviewResponse:
"""Structured response from Reviewer back to Worker."""
request_id: str
feedback: str
approved: bool
class Reviewer(Executor):
"""Executor that reviews agent responses and provides structured feedback."""
def __init__(self, id: str, client: SupportsChatGetResponse) -> None:
super().__init__(id=id)
self._chat_client = client
@handler
async def review(self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]) -> None:
print(f"Reviewer: Evaluating response for request {request.request_id[:8]}...")
# Define structured schema for the LLM to return.
class _Response(BaseModel):
feedback: str
approved: bool
# Construct review instructions and context.
messages = [
Message(
role="system",
contents=[
(
"You are a reviewer for an AI agent. Provide feedback on the "
"exchange between a user and the agent. Indicate approval only if:\n"
"- Relevance: response addresses the query\n"
"- Accuracy: information is correct\n"
"- Clarity: response is easy to understand\n"
"- Completeness: response covers all aspects\n"
"Do not approve until all criteria are satisfied."
)
],
)
]
# Add conversation history.
messages.extend(request.user_messages)
messages.extend(request.agent_messages)
# Add explicit review instruction.
messages.append(Message("user", ["Please review the agent's responses."]))
print("Reviewer: Sending review request to LLM...")
response = await self._chat_client.get_response(messages=messages, options={"response_format": _Response})
parsed = _Response.model_validate_json(response.messages[-1].text)
print(f"Reviewer: Review complete - Approved: {parsed.approved}")
print(f"Reviewer: Feedback: {parsed.feedback}")
# Send structured review result to Worker.
await ctx.send_message(
ReviewResponse(request_id=request.request_id, feedback=parsed.feedback, approved=parsed.approved)
)
class Worker(Executor):
"""Executor that generates responses and incorporates feedback when necessary."""
def __init__(self, id: str, client: SupportsChatGetResponse) -> None:
super().__init__(id=id)
self._chat_client = client
self._pending_requests: dict[str, tuple[ReviewRequest, list[Message]]] = {}
@handler
async def handle_user_messages(self, user_messages: list[Message], ctx: WorkflowContext[ReviewRequest]) -> None:
print("Worker: Received user messages, generating response...")
# Initialize chat with system prompt.
messages = [Message("system", ["You are a helpful assistant."])]
messages.extend(user_messages)
print("Worker: Calling LLM to generate response...")
response = await self._chat_client.get_response(messages=messages)
print(f"Worker: Response generated: {response.messages[-1].text}")
# Add agent messages to context.
messages.extend(response.messages)
# Create review request and send to Reviewer.
request = ReviewRequest(request_id=str(uuid4()), user_messages=user_messages, agent_messages=response.messages)
print(f"Worker: Sending response for review (ID: {request.request_id[:8]})")
await ctx.send_message(request)
# Track request for possible retry.
self._pending_requests[request.request_id] = (request, messages)
@handler
async def handle_review_response(
self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest, AgentResponse]
) -> None:
print(f"Worker: Received review for request {review.request_id[:8]} - Approved: {review.approved}")
if review.request_id not in self._pending_requests:
raise ValueError(f"Unknown request ID in review: {review.request_id}")
request, messages = self._pending_requests.pop(review.request_id)
if review.approved:
print("Worker: Response approved. Emitting to external consumer...")
# Emit approved result to external consumer
await ctx.yield_output(AgentResponse(messages=request.agent_messages))
return
print(f"Worker: Response not approved. Feedback: {review.feedback}")
print("Worker: Regenerating response with feedback...")
# Incorporate review feedback.
messages.append(Message("system", [review.feedback]))
messages.append(Message("system", ["Please incorporate the feedback and regenerate the response."]))
messages.extend(request.user_messages)
# Retry with updated prompt.
response = await self._chat_client.get_response(messages=messages)
print(f"Worker: New response generated: {response.messages[-1].text}")
messages.extend(response.messages)
# Send updated request for re-review.
new_request = ReviewRequest(
request_id=review.request_id, user_messages=request.user_messages, agent_messages=response.messages
)
await ctx.send_message(new_request)
# Track new request for further evaluation.
self._pending_requests[new_request.request_id] = (new_request, messages)
async def main() -> None:
print("Starting Workflow Agent Demo")
print("=" * 50)
print("Building workflow with Worker ↔ Reviewer cycle...")
worker = Worker(
id="worker",
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
reviewer = Reviewer(
id="reviewer",
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
)
agent = (
WorkflowBuilder(start_executor=worker).add_edge(worker, reviewer).add_edge(reviewer, worker).build().as_agent()
)
print("Running workflow agent with user query...")
print("Query: 'Write code for parallel reading 1 million files on disk and write to a sorted output file.'")
print("-" * 50)
# Run agent in streaming mode to observe incremental updates.
response = await agent.run(
"Write code for parallel reading 1 million files on disk and write to a sorted output file."
)
print("-" * 50)
print("Final Approved Response:")
print(f"{response.agent_id}: {response.text}")
if __name__ == "__main__":
print("Initializing Workflow as Agent Sample...")
asyncio.run(main())
@@ -0,0 +1,180 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent, AgentSession, InMemoryHistoryProvider
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
"""
Sample: Workflow as Agent with Session Conversation History and Checkpointing
This sample demonstrates how to use AgentSession with a workflow wrapped as an agent
to maintain conversation history across multiple invocations. When using as_agent(),
the session's history is included in each workflow run, enabling
the workflow participants to reference prior conversation context.
It also demonstrates how to enable checkpointing for workflow execution state
persistence, allowing workflows to be paused and resumed.
Key concepts:
- Workflows can be wrapped as agents using Agent(client=workflow,)
- AgentSession preserves conversation history
- Each call to agent.run() includes session history + new message
- Participants in the workflow see the full conversation context
- checkpoint_storage parameter enables workflow state persistence
Use cases:
- Multi-turn conversations with workflow-based orchestrations
- Stateful workflows that need context from previous interactions
- Building conversational agents that leverage workflow patterns
- Long-running workflows that need pause/resume capability
Prerequisites:
- FOUNDRY_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- FOUNDRY_MODEL must be set to your Azure OpenAI model deployment name.
"""
async def main() -> None:
# Create a chat client
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
assistant = Agent(
client=client,
name="assistant",
instructions=(
"You are a helpful assistant. Answer questions based on the conversation "
"history. If the user asks about something mentioned earlier, reference it."
),
)
summarizer = Agent(
client=client,
name="summarizer",
instructions=(
"You are a summarizer. After the assistant responds, provide a brief "
"one-sentence summary of the key point from the conversation so far."
),
)
# Build a sequential workflow: assistant -> summarizer
workflow = SequentialBuilder(participants=[assistant, summarizer]).build()
# Wrap the workflow as an agent
agent = workflow.as_agent()
# Create a session to maintain history
session = agent.create_session()
print("=" * 60)
print("Workflow as Agent with Session - Multi-turn Conversation")
print("=" * 60)
# First turn: Introduce a topic
query1 = "My name is Alex and I'm learning about machine learning."
print(f"\n[Turn 1] User: {query1}")
response1 = await agent.run(query1, session=session)
if response1.messages:
for msg in response1.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Second turn: Reference the previous topic
query2 = "What was my name again, and what am I learning about?"
print(f"\n[Turn 2] User: {query2}")
response2 = await agent.run(query2, session=session)
if response2.messages:
for msg in response2.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Third turn: Ask a follow-up question
query3 = "Can you suggest a good first project for me to try?"
print(f"\n[Turn 3] User: {query3}")
response3 = await agent.run(query3, session=session)
if response3.messages:
for msg in response3.messages:
speaker = msg.author_name or msg.role
print(f"[{speaker}]: {msg.text}")
# Show the accumulated conversation history
print("\n" + "=" * 60)
print("Full Session History")
print("=" * 60)
memory_state = session.state.get(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {})
history = memory_state.get("messages", [])
for i, msg in enumerate(history, start=1):
role = msg.role if hasattr(msg.role, "value") else str(msg.role)
speaker = msg.author_name or role
text_preview = msg.text[:80] + "..." if len(msg.text) > 80 else msg.text
print(f"{i:02d}. [{speaker}]: {text_preview}")
async def demonstrate_session_serialization() -> None:
"""
Demonstrates serializing and resuming a session with a workflow agent.
This shows how conversation history can be persisted and restored,
enabling long-running conversational workflows.
"""
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
memory_assistant = Agent(
client=client,
name="memory_assistant",
instructions="You are a helpful assistant with good memory. Remember details from our conversation.",
)
workflow = SequentialBuilder(participants=[memory_assistant]).build()
agent = workflow.as_agent()
# Create initial session and have a conversation
session = agent.create_session()
print("\n" + "=" * 60)
print("Session Serialization Demo")
print("=" * 60)
# First interaction
query = "Remember this: the secret code is ALPHA-7."
print(f"\n[Session 1] User: {query}")
response = await agent.run(query, session=session)
if response.messages:
print(f"[assistant]: {response.messages[0].text}")
# Serialize session state (could be saved to database/file)
serialized_state = session.to_dict()
print("\n[Serialized session state for persistence]")
# Simulate a new session by creating a new session from serialized state
restored_session = AgentSession.from_dict(serialized_state)
# Continue conversation with restored session
query = "What was the secret code I told you?"
print(f"\n[Session 2 - Restored] User: {query}")
response = await agent.run(query, session=restored_session)
if response.messages:
print(f"[assistant]: {response.messages[0].text}")
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(demonstrate_session_serialization())