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,235 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from dataclasses import dataclass, field
from agent_framework import (
Agent,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponse,
AgentResponseUpdate,
Executor,
Message,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
handler,
response_handler,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from typing_extensions import Never
# Load environment variables from .env file
load_dotenv()
"""
Sample: Azure AI Agents in workflow with human feedback
Pipeline layout:
writer_agent -> Coordinator -> writer_agent -> Coordinator -> final_editor_agent -> Coordinator -> output
The writer agent drafts marketing copy. A custom executor emits a request_info event (type='request_info') so a
human can comment, then relays the human guidance back into the conversation before the final editor agent
produces the polished output.
Demonstrates:
- Capturing agent responses in a custom executor.
- Emitting request_info events (type='request_info') to request human input.
- Handling human feedback and routing it to the appropriate 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. Run `az login` before executing.
"""
@dataclass
class DraftFeedbackRequest:
"""Payload sent for human review."""
prompt: str = ""
conversation: list[Message] = field(default_factory=lambda: [])
class Coordinator(Executor):
"""Bridge between the writer agent, human feedback, and final editor."""
def __init__(self, id: str, writer_name: str, final_editor_name: str) -> None:
super().__init__(id)
self.writer_name = writer_name
self.final_editor_name = final_editor_name
@handler
async def on_writer_response(
self,
draft: AgentExecutorResponse,
ctx: WorkflowContext[Never, AgentResponse],
) -> None:
"""Handle responses from the writer and final editor agents."""
if draft.executor_id == self.final_editor_name:
# No further processing is needed when the final editor has responded.
return
# Writer agent response; request human feedback.
# Preserve the full conversation so that the final editor has context.
conversation = list(draft.full_conversation)
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, conversation=conversation),
response_type=str,
)
@response_handler
async def on_human_feedback(
self,
original_request: DraftFeedbackRequest,
feedback: str,
ctx: WorkflowContext[AgentExecutorRequest],
) -> None:
"""Process human feedback and forward to the appropriate agent."""
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_name,
)
return
# Human provided feedback; prompt the writer to revise.
conversation: list[Message] = list(original_request.conversation)
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."
)
conversation.append(Message("user", contents=[instruction]))
await ctx.send_message(
AgentExecutorRequest(messages=conversation, should_respond=True),
target_id=self.writer_name,
)
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, str] | None:
"""Process events from the workflow stream to capture human feedback requests."""
# Track the last author to format streaming output.
last_author: str | None = None
requests: list[tuple[str, DraftFeedbackRequest]] = []
async for event in stream:
if event.type == "request_info" and isinstance(event.data, DraftFeedbackRequest):
requests.append((event.request_id, event.data))
elif event.type == "output" and isinstance(event.data, AgentResponseUpdate):
# This workflow should only produce AgentResponseUpdate as outputs.
# Streaming updates from an agent will be consecutive, because no two agents run simultaneously
# in this workflow. So we can use last_author to format output nicely.
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)
# Handle any pending human feedback requests.
if requests:
responses: dict[str, str] = {}
for request_id, _ in requests:
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...")
return None
responses[request_id] = answer
return responses
return None
async def main() -> None:
"""Run the workflow and bridge human feedback between two agents."""
# Create the agents
writer_agent = 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."),
default_options={
"tool_choice": "required",
},
)
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=(
"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. "
),
)
# Create the executor
coordinator = Coordinator(
id="coordinator",
writer_name=writer_agent.name, # type: ignore
final_editor_name=final_editor_agent.name, # type: ignore
)
# Build the workflow.
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 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)
print("\nWorkflow complete.")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,352 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
import os
from dataclasses import dataclass
from typing import Annotated
from agent_framework import (
Agent,
AgentExecutorResponse,
Content,
Executor,
WorkflowBuilder,
WorkflowContext,
executor,
handler,
tool,
)
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from typing_extensions import Never
# Load environment variables from .env file
load_dotenv()
"""
Sample: Agents in a workflow with AI functions requiring approval
This sample creates a workflow that automatically replies to incoming emails.
If historical email data is needed, it uses an AI function to read the data,
which requires human approval before execution.
This sample works as follows:
1. An incoming email is received by the workflow.
2. The EmailPreprocessor executor preprocesses the email, adding special notes if the sender is important.
3. The preprocessed email is sent to the Email Writer agent, which generates a response.
4. If the agent needs to read historical email data, it calls the read_historical_email_data AI function,
which triggers an approval request.
5. The sample automatically approves the request for demonstration purposes.
6. Once approved, the AI function executes and returns the historical email data to the agent.
7. The agent uses the historical data to compose a comprehensive email response.
8. The response is sent to the conclude_workflow_executor, which yields the final response.
Purpose:
Show how to integrate AI functions with approval requests into a workflow.
Demonstrate:
- Creating AI functions that require approval before execution.
- Building a workflow that includes an agent and executors.
- Handling approval requests during workflow execution.
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, request_info events (type='request_info'), and streaming runs.
"""
# 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 get_current_date() -> str:
"""Get the current date in YYYY-MM-DD format."""
# For demonstration purposes, we return a fixed date.
return "2025-11-07"
@tool(approval_mode="never_require")
def get_team_members_email_addresses() -> list[dict[str, str]]:
"""Get the email addresses of team members."""
# In a real implementation, this might query a database or directory service.
return [
{
"name": "Alice",
"email": "alice@contoso.com",
"position": "Software Engineer",
"manager": "John Doe",
},
{
"name": "Bob",
"email": "bob@contoso.com",
"position": "Product Manager",
"manager": "John Doe",
},
{
"name": "Charlie",
"email": "charlie@contoso.com",
"position": "Senior Software Engineer",
"manager": "John Doe",
},
{
"name": "Mike",
"email": "mike@contoso.com",
"position": "Principal Software Engineer Manager",
"manager": "VP of Engineering",
},
]
@tool(approval_mode="never_require")
def get_my_information() -> dict[str, str]:
"""Get my personal information."""
return {
"name": "John Doe",
"email": "john@contoso.com",
"position": "Software Engineer Manager",
"manager": "Mike",
}
@tool(approval_mode="always_require")
async def read_historical_email_data(
email_address: Annotated[str, "The email address to read historical data from"],
start_date: Annotated[str, "The start date in YYYY-MM-DD format"],
end_date: Annotated[str, "The end date in YYYY-MM-DD format"],
) -> list[dict[str, str]]:
"""Read historical email data for a given email address and date range."""
historical_data = {
"alice@contoso.com": [
{
"from": "alice@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-05",
"subject": "Bug Bash Results",
"body": "We just completed the bug bash and found a few issues that need immediate attention.",
},
{
"from": "alice@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-03",
"subject": "Code Freeze",
"body": "We are entering code freeze starting tomorrow.",
},
],
"bob@contoso.com": [
{
"from": "bob@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-04",
"subject": "Team Outing",
"body": "Don't forget about the team outing this Friday!",
},
{
"from": "bob@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-02",
"subject": "Requirements Update",
"body": "The requirements for the new feature have been updated. Please review them.",
},
],
"charlie@contoso.com": [
{
"from": "charlie@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-05",
"subject": "Project Update",
"body": "The bug bash went well. A few critical bugs but should be fixed by the end of the week.",
},
{
"from": "charlie@contoso.com",
"to": "john@contoso.com",
"date": "2025-11-06",
"subject": "Code Review",
"body": "Please review my latest code changes.",
},
],
}
emails = historical_data.get(email_address, [])
return [email for email in emails if start_date <= email["date"] <= end_date]
@tool(approval_mode="always_require")
async def send_email(
to: Annotated[str, "The recipient email address"],
subject: Annotated[str, "The email subject"],
body: Annotated[str, "The email body"],
) -> str:
"""Send an email."""
await asyncio.sleep(1) # Simulate sending email
return "Email successfully sent."
@dataclass
class Email:
sender: str
subject: str
body: str
class EmailPreprocessor(Executor):
def __init__(self, special_email_addresses: set[str]) -> None:
super().__init__(id="email_preprocessor")
self.special_email_addresses = special_email_addresses
@handler
async def preprocess(self, email: Email, ctx: WorkflowContext[str]) -> None:
"""Preprocess the incoming email."""
email_payload = f"Incoming email:\nFrom: {email.sender}\nSubject: {email.subject}\nBody: {email.body}"
message = email_payload
if email.sender in self.special_email_addresses:
note = (
"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{email_payload}"
await ctx.send_message(message)
@executor(id="conclude_workflow_executor")
async def conclude_workflow(
email_response: AgentExecutorResponse,
ctx: WorkflowContext[Never, str],
) -> None:
"""Conclude the workflow by yielding the final email response."""
await ctx.yield_output(email_response.agent_response.text)
async def main() -> None:
# Create agent
email_writer_agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="EmailWriter",
instructions=("You are an excellent email assistant. You respond to incoming emails."),
# tools with `approval_mode="always_require"` will trigger approval requests
tools=[
read_historical_email_data,
send_email,
get_current_date,
get_team_members_email_addresses,
get_my_information,
],
)
# Create executor
email_processor = EmailPreprocessor(special_email_addresses={"mike@contoso.com"})
# Build the workflow
workflow = (
WorkflowBuilder(start_executor=email_processor, output_from=[conclude_workflow])
.add_edge(email_processor, email_writer_agent)
.add_edge(email_writer_agent, conclude_workflow)
.build()
)
# Simulate an incoming email
incoming_email = Email(
sender="mike@contoso.com",
subject="Important: Project Update",
body="Please provide your team's status update on the project since last week.",
)
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
events = await workflow.run(incoming_email)
request_info_events = events.get_request_info_events()
# Run until there are no more approval requests
while request_info_events:
responses: dict[str, Content] = {}
for request_info_event in request_info_events:
# We should only expect FunctionApprovalRequestContent in this sample
data = request_info_event.data
if not isinstance(data, Content) or data.type != "function_approval_request":
raise ValueError(f"Unexpected request info content type: {type(data)}")
# To make the type checker happy, we make sure function_call is not None
if data.function_call is None:
raise ValueError("Function call information is missing in the approval request.")
# Pretty print the function call details
arguments = json.dumps(data.function_call.parse_arguments(), indent=2)
print(f"Received approval request for function: {data.function_call.name} with args:\n{arguments}")
# For demo purposes, we automatically approve the request
# The expected response type of the request is `function_approval_response Content`,
# which can be created via `to_function_approval_response` method on the request content
print("Performing automatic approval for demo purposes...")
responses[request_info_event.request_id] = data.to_function_approval_response(approved=True)
events = await workflow.run(responses=responses)
request_info_events = events.get_request_info_events()
# The output should only come from conclude_workflow executor and it's a single string
print("Final email response conversation:")
print(events.get_outputs()[0])
"""
Sample Output:
Received approval request for function: read_historical_email_data with args:
{
"email_address": "alice@contoso.com",
"start_date": "2025-10-31",
"end_date": "2025-11-07"
}
Performing automatic approval for demo purposes...
Received approval request for function: read_historical_email_data with args:
{
"email_address": "bob@contoso.com",
"start_date": "2025-10-31",
"end_date": "2025-11-07"
}
Performing automatic approval for demo purposes...
Received approval request for function: read_historical_email_data with args:
{
"email_address": "charlie@contoso.com",
"start_date": "2025-10-31",
"end_date": "2025-11-07"
}
Performing automatic approval for demo purposes...
Received approval request for function: send_email with args:
{
"to": "mike@contoso.com",
"subject": "Team's Status Update on the Project",
"body": "
Hi Mike,
Here's the status update from our team:
- **Bug Bash and Code Freeze:**
- We recently completed a bug bash, during which several issues were identified. Alice and Charlie are working on fixing these critical bugs, and we anticipate resolving them by the end of this week.
- We have entered a code freeze as of November 4, 2025.
- **Requirements Update:**
- Bob has updated the requirements for a new feature, and all team members are reviewing these changes to ensure alignment.
- **Ongoing Reviews:**
- Charlie has submitted his latest code changes for review to ensure they meet our quality standards.
Please let me know if you need more detailed information or have any questions.
Best regards,
John"
}
Performing automatic approval for demo purposes...
Final email response conversation:
I've sent the status update to Mike with the relevant information from the team. Let me know if there's anything else you need
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Declaration-only tools in a workflow (issue #3425)
A declaration-only tool (func=None) represents a client-side tool that the
framework cannot execute — the LLM can call it, but the workflow must pause
so the caller can supply the result.
Flow:
1. The agent is given a declaration-only tool ("get_user_location").
2. When the LLM decides to call it, the workflow pauses and emits a
request_info event containing the FunctionCallContent.
3. The caller inspects the tool name/args, runs the tool however it wants,
and feeds the result back via workflow.run(responses={...}).
4. The workflow resumes — the agent sees the tool result and finishes.
Prerequisites:
- FOUNDRY_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 Agent, Content, FunctionTool, 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()
# A declaration-only tool: the schema is sent to the LLM, but the framework
# has no implementation to execute. The caller must supply the result.
get_user_location = FunctionTool(
name="get_user_location",
func=None,
description="Get the user's current city. Only the client application can resolve this.",
input_model={
"type": "object",
"properties": {
"reason": {"type": "string", "description": "Why the location is needed"},
},
"required": ["reason"],
},
)
async def main() -> None:
_client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
agent = Agent(
client=_client,
name="WeatherBot",
instructions=(
"You are a helpful weather assistant. "
"When the user asks about weather, call get_user_location first, "
"then make up a plausible forecast for that city."
),
tools=[get_user_location],
)
workflow = WorkflowBuilder(start_executor=agent).build()
# --- First run: the agent should call the declaration-only tool ---
print(">>> Sending: 'What's the weather like today?'")
result = await workflow.run("What's the weather like today?")
requests = result.get_request_info_events()
if not requests:
# The LLM chose not to call the tool — print whatever it said and exit
print(f"Agent replied without calling the tool: {result.get_outputs()}")
return
# --- Inspect what the agent wants ---
for req in requests:
data = req.data
args = json.loads(data.arguments) if isinstance(data.arguments, str) else data.arguments
print(f"Workflow paused — agent called: {data.name}({args})")
# --- "Execute" the tool on the client side and send results back ---
responses: dict[str, Any] = {}
for req in requests:
# In a real app this could be a GPS lookup, browser API, user prompt, etc.
client_result = "Seattle, WA"
print(f"Client provides result for {req.data.name}: {client_result!r}")
responses[req.request_id] = Content.from_function_result(
call_id=req.data.call_id,
result=client_result,
)
result = await workflow.run(responses=responses)
# --- Final answer ---
for output in result.get_outputs():
print(f"\nAgent: {output.text}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,213 @@
# 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:
- 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)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import Any
from agent_framework import (
Agent,
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Store chat client at module level for aggregator access
_chat_client: FoundryChatClient | 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",
contents=[
(
"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", contents=["\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 = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agents that analyze from different perspectives
technical_analyst = Agent(
client=_chat_client,
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 = Agent(
client=_chat_client,
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 = Agent(
client=_chat_client,
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())
@@ -0,0 +1,182 @@
# 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:
- 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)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
Agent,
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
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 = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agents for a group discussion
optimist = Agent(
client=client,
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 = Agent(
client=client,
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 = Agent(
client=client,
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 = Agent(
client=client,
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())
@@ -0,0 +1,254 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from collections.abc import AsyncIterable
from dataclasses import dataclass
from typing import Any
from agent_framework import (
Agent,
AgentExecutorRequest,
AgentExecutorResponse,
AgentResponseUpdate,
Executor,
Message,
WorkflowBuilder,
WorkflowContext,
WorkflowEvent,
handler,
response_handler,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.openai import OpenAIChatOptions
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel
# Load environment variables from .env file
load_dotenv()
"""
Sample: Human in the loop guessing game
An agent guesses a number, then a human guides it with higher, lower, or
correct. The loop continues until the human confirms correct, at which point
the workflow completes when idle with no pending work.
Purpose:
Show how to integrate a human step in the middle of an LLM workflow by using
`request_info` and `run(responses=..., stream=True)`.
Demonstrate:
- Alternating turns between an AgentExecutor and a human, driven by events.
- Using Pydantic response_format to enforce structured JSON output from the agent instead of regex parsing.
- Driving the loop in application code with run and responses parameter.
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 runs.
"""
# How human-in-the-loop is achieved via `request_info` and `run(responses=..., stream=True)`:
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
# - The workflow run pauses and emits a with the payload and the request_id.
# - The application captures the event, prompts the user, and collects replies.
# - The application calls `run(stream=True, responses=...)` with a map of request_ids to replies.
# - The workflow resumes, and the response is delivered to the executor method decorated with @response_handler.
# - The executor can then continue the workflow, e.g., by sending a new message to the agent.
@dataclass
class HumanFeedbackRequest:
"""Request sent to the human for feedback on the agent's guess."""
prompt: str
class GuessOutput(BaseModel):
"""Structured output from the agent. Enforced via response_format for reliable parsing."""
guess: int
class TurnManager(Executor):
"""Coordinates turns between the agent and the human.
Responsibilities:
- Kick off the first agent turn.
- After each agent reply, request human feedback with a HumanFeedbackRequest.
- After each human reply, either finish the game or prompt the agent again with feedback.
"""
def __init__(self, id: str | None = None):
super().__init__(id=id or "turn_manager")
@handler
async def start(self, _: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None:
"""Start the game by asking the agent for an initial guess.
Contract:
- Input is a simple starter token (ignored here).
- Output is an AgentExecutorRequest that triggers the agent to produce a guess.
"""
user = Message("user", contents=["Start by making your first guess."])
await ctx.send_message(AgentExecutorRequest(messages=[user], should_respond=True))
@handler
async def on_agent_response(
self,
result: AgentExecutorResponse,
ctx: WorkflowContext,
) -> None:
"""Handle the agent's guess and request human guidance.
Steps:
1) Use .value to access the parsed structured output directly.
2) Request info with a HumanFeedbackRequest as the payload.
"""
# Access the parsed structured model output via .value.
# Since the agent is configured with response_format=GuessOutput,
# .value returns the parsed GuessOutput instance directly.
agent_value = result.agent_response.value
if agent_value is None:
raise RuntimeError(
"AgentResponse.value is None. Ensure that the agent is invoked with "
"options={'response_format': GuessOutput} so structured output is available."
)
last_guess = agent_value.guess
# Craft a precise human prompt that defines higher and lower relative to the agent's guess.
prompt = (
f"The agent guessed: {last_guess}. "
"Type one of: higher (your number is higher than this guess), "
"lower (your number is lower than this guess), correct, or exit."
)
# Send a request with a prompt as the payload and expect a string reply.
await ctx.request_info(
request_data=HumanFeedbackRequest(prompt=prompt),
response_type=str,
)
@response_handler
async def on_human_feedback(
self,
original_request: HumanFeedbackRequest,
feedback: str,
ctx: WorkflowContext[AgentExecutorRequest, str],
) -> None:
"""Continue the game or finish based on human feedback."""
reply = feedback.strip().lower()
if reply == "correct":
await ctx.yield_output("Guessed correctly!")
return
# Provide feedback to the agent to try again.
# response_format=GuessOutput on the agent ensures JSON output, so we just need to guide the logic.
last_guess = original_request.prompt.split(": ")[1].split(".")[0]
feedback_text = (
f"Feedback: {reply}. Your last guess was {last_guess}. "
f"Use this feedback to adjust and make your next guess (1-10)."
)
user_msg = Message("user", contents=[feedback_text])
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str, str] | None:
"""Process events from the workflow stream to capture human feedback requests."""
# Track the last author to format streaming output.
last_response_id: str | None = None
requests: list[tuple[str, HumanFeedbackRequest]] = []
async for event in stream:
if event.type == "request_info" and isinstance(event.data, HumanFeedbackRequest):
requests.append((event.request_id, event.data))
elif event.type == "output":
if isinstance(event.data, AgentResponseUpdate):
update = event.data
response_id = update.response_id
if response_id != last_response_id:
if last_response_id is not None:
print() # Newline between different responses
print(f"{update.author_name}: {update.text}", end="", flush=True)
last_response_id = response_id
else:
print(update.text, end="", flush=True)
else:
print(f"\n{event.executor_id}: {event.data}")
# Handle any pending human feedback requests.
if requests:
responses: dict[str, str] = {}
for request_id, request in requests:
print(f"\nHITL: {request.prompt}")
# Instructional print already appears above. The input line below is the user entry point.
# If desired, you can add more guidance here, but keep it concise.
answer = input("Enter higher/lower/correct/exit: ").lower() # noqa: ASYNC250
if answer == "exit":
print("Exiting...")
return None
responses[request_id] = answer
return responses
return None
async def main() -> None:
"""Run the human-in-the-loop guessing game workflow."""
# Create agent and executor
guessing_agent = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
),
name="GuessingAgent",
instructions=(
"You guess a number between 1 and 10. "
"If the user says 'higher' or 'lower', adjust your next guess. "
'You MUST return ONLY a JSON object exactly matching this schema: {"guess": <integer 1..10>}. '
"No explanations or additional text."
),
# response_format enforces that the model produces JSON compatible with GuessOutput.
default_options=OpenAIChatOptions[Any](response_format=GuessOutput),
)
turn_manager = TurnManager(id="turn_manager")
# Build a simple loop: TurnManager <-> AgentExecutor.
workflow = (
WorkflowBuilder(start_executor=turn_manager)
.add_edge(turn_manager, guessing_agent) # Ask agent to make/adjust a guess
.add_edge(guessing_agent, turn_manager) # Agent's response comes back to coordinator
).build()
# Initiate the first run of the workflow.
# Runs are not isolated; state is preserved across multiple calls to run.
stream = workflow.run("start", 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:
HITL> The agent guessed: 5. Type one of: higher (your number is higher than this guess), lower (your number is lower than this guess), correct, or exit.
Enter higher/lower/correct/exit: higher
HITL> The agent guessed: 8. Type one of: higher (your number is higher than this guess), lower (your number is lower than this guess), correct, or exit.
Enter higher/lower/correct/exit: higher
HITL> The agent guessed: 10. Type one of: higher (your number is higher than this guess), lower (your number is lower than this guess), correct, or exit.
Enter higher/lower/correct/exit: lower
HITL> The agent guessed: 9. Type one of: higher (your number is higher than this guess), lower (your number is lower than this guess), correct, or exit.
Enter higher/lower/correct/exit: correct
Workflow output: Guessed correctly: 9
""" # noqa: E501
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,149 @@
# 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:
- 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)
"""
import asyncio
import os
from collections.abc import AsyncIterable
from typing import cast
from agent_framework import (
Agent,
AgentExecutorResponse,
Message,
WorkflowEvent,
)
from agent_framework.foundry import FoundryChatClient
from agent_framework.orchestrations import AgentRequestInfoResponse, SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
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 = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
# Create agents for a sequential document review workflow
drafter = Agent(
client=client,
name="drafter",
instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."),
)
editor = Agent(
client=client,
name="editor",
instructions=(
"You are an editor. Review the draft and make improvements. "
"Incorporate any human feedback that was provided."
),
)
finalizer = Agent(
client=client,
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())