test
Some checks failed
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
Python - Merge - Tests / paths-filter (push) Has been cancelled
Python - Merge - Tests / Python Tests - Core (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / Python Tests - Azure AI (integration, ubuntu-latest, 3.10) (push) Has been cancelled
Python - Merge - Tests / python-integration-tests-check (push) Has been cancelled
Python - Lab Tests / paths-filter (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (ubuntu-latest, 3.14) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.10) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.11) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.12) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.13) (push) Has been cancelled
Python - Lab Tests / Python Lab Tests (windows-latest, 3.14) (push) Has been cancelled
Check .md links / markdown-link-check (push) Has been cancelled

This commit is contained in:
2026-01-24 03:05:12 +11:00
parent f78f2388b3
commit 539852f81c
2584 changed files with 287471 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from dataclasses import dataclass
from typing import Annotated, Never
from agent_framework import (
AgentExecutorResponse,
ChatAgent,
ChatMessage,
Executor,
FunctionApprovalRequestContent,
FunctionApprovalResponseContent,
WorkflowBuilder,
WorkflowContext,
ai_function,
executor,
handler,
)
from agent_framework.openai import OpenAIChatClient
"""
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:
- Azure AI Agent Service configured, along with the required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, edges, events, RequestInfoEvent, and streaming runs.
"""
@ai_function
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"
@ai_function
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",
},
]
@ai_function
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",
}
@ai_function(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]
@ai_function(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."""
message = str(email)
if email.sender in self.special_email_addresses:
note = (
"Pay special attention to this sender. This email is very important. "
"Gather relevant information from all previous emails within my team before responding."
)
message = f"{note}\n\n{message}"
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)
def create_email_writer_agent() -> ChatAgent:
"""Create the Email Writer agent with tools that require approval."""
return OpenAIChatClient().as_agent(
name="Email Writer",
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,
],
)
async def main() -> None:
# Build the workflow
workflow = (
WorkflowBuilder()
.register_agent(create_email_writer_agent, name="email_writer")
.register_executor(
lambda: EmailPreprocessor(special_email_addresses={"mike@contoso.com"}),
name="email_preprocessor",
)
.register_executor(lambda: conclude_workflow, name="conclude_workflow")
.set_start_executor("email_preprocessor")
.add_edge("email_preprocessor", "email_writer")
.add_edge("email_writer", "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.",
)
responses: dict[str, FunctionApprovalResponseContent] = {}
output: list[ChatMessage] | None = None
while True:
if responses:
events = await workflow.send_responses(responses)
responses.clear()
else:
events = await workflow.run(incoming_email)
request_info_events = events.get_request_info_events()
for request_info_event in request_info_events:
# We should only expect FunctionApprovalRequestContent in this sample
if not isinstance(request_info_event.data, FunctionApprovalRequestContent):
raise ValueError(f"Unexpected request info content type: {type(request_info_event.data)}")
# Pretty print the function call details
arguments = json.dumps(request_info_event.data.function_call.parse_arguments(), indent=2)
print(
f"Received approval request for function: {request_info_event.data.function_call.name} "
f"with args:\n{arguments}"
)
# For demo purposes, we automatically approve the request
# The expected response type of the request is `FunctionApprovalResponseContent`,
# which can be created via `create_response` method on the request content
print("Performing automatic approval for demo purposes...")
responses[request_info_event.request_id] = request_info_event.data.create_response(approved=True)
# Once we get an output event, we can conclude the workflow
# Outputs can only be produced by the conclude_workflow_executor in this sample
if outputs := events.get_outputs():
# We expect only one output from the conclude_workflow_executor
output = outputs[0]
break
if not output:
raise RuntimeError("Workflow did not produce any output event.")
print("Final email response conversation:")
print(output)
"""
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())

View File

@@ -0,0 +1,206 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with ConcurrentBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
ConcurrentBuilder workflow for specific agents, allowing human review and
modification of individual agent outputs before aggregation.
Purpose:
Show how to use the request info API that pauses for selected concurrent agents,
allowing review and steering of their results.
Demonstrate:
- Configuring request info with `.with_request_info()` for specific agents
- Reviewing output from individual agents during concurrent execution
- Injecting human guidance for specific agents before aggregation
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from typing import Any
from agent_framework import (
AgentRequestInfoResponse,
ChatMessage,
ConcurrentBuilder,
RequestInfoEvent,
Role,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework._workflows._agent_executor import AgentExecutorResponse
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
# Store chat client at module level for aggregator access
_chat_client: AzureOpenAIChatClient | None = None
async def aggregate_with_synthesis(results: list[AgentExecutorResponse]) -> Any:
"""Custom aggregator that synthesizes concurrent agent outputs using an LLM.
This aggregator extracts the outputs from each parallel agent and uses the
chat client to create a unified summary, incorporating any human feedback
that was injected into the conversation.
Args:
results: List of responses from all concurrent agents
Returns:
The synthesized summary text
"""
if not _chat_client:
return "Error: Chat client not initialized"
# Extract each agent's final output
expert_sections: list[str] = []
human_guidance = ""
for r in results:
try:
messages = getattr(r.agent_response, "messages", [])
final_text = messages[-1].text if messages and hasattr(messages[-1], "text") else "(no content)"
expert_sections.append(f"{getattr(r, 'executor_id', 'analyst')}:\n{final_text}")
# Check for human feedback in the conversation (will be last user message if present)
if r.full_conversation:
for msg in reversed(r.full_conversation):
if msg.role == 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 = ChatMessage(
Role.SYSTEM,
text=(
"You are a synthesis expert. Consolidate the following analyst perspectives "
"into one cohesive, balanced summary (3-4 sentences). If human guidance is provided, "
"prioritize aspects as directed."
),
)
user_msg = ChatMessage(Role.USER, text="\n\n".join(expert_sections) + guidance_text)
response = await _chat_client.get_response([system_msg, user_msg])
return response.messages[-1].text if response.messages else ""
async def main() -> None:
global _chat_client
_chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents that analyze from different perspectives
technical_analyst = _chat_client.as_agent(
name="technical_analyst",
instructions=(
"You are a technical analyst. When given a topic, provide a technical "
"perspective focusing on implementation details, performance, and architecture. "
"Keep your analysis to 2-3 sentences."
),
)
business_analyst = _chat_client.as_agent(
name="business_analyst",
instructions=(
"You are a business analyst. When given a topic, provide a business "
"perspective focusing on ROI, market impact, and strategic value. "
"Keep your analysis to 2-3 sentences."
),
)
user_experience_analyst = _chat_client.as_agent(
name="ux_analyst",
instructions=(
"You are a UX analyst. When given a topic, provide a user experience "
"perspective focusing on usability, accessibility, and user satisfaction. "
"Keep your analysis to 2-3 sentences."
),
)
# Build workflow with request info enabled and custom aggregator
workflow = (
ConcurrentBuilder()
.participants([technical_analyst, business_analyst, user_experience_analyst])
.with_aggregator(aggregate_with_synthesis)
# Only enable request info for the technical analyst agent
.with_request_info(agents=["technical_analyst"])
.build()
)
# Run the workflow with human-in-the-loop
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
print("Starting multi-perspective analysis workflow...")
print("=" * 60)
while not workflow_complete:
# Run or continue the workflow
stream = (
workflow.send_responses_streaming(pending_responses)
if pending_responses
else workflow.run_stream("Analyze the impact of large language models on software development.")
)
pending_responses = None
# Process events
async for event in stream:
if isinstance(event, RequestInfoEvent):
if isinstance(event.data, AgentExecutorResponse):
# Display agent output for review and potential modification
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(
f"Agent {event.source_executor_id} just responded with: '{event.data.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if event.data.full_conversation:
print("Conversation context:")
recent = (
event.data.full_conversation[-2:]
if len(event.data.full_conversation) > 2
else event.data.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role.value
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])
pending_responses = {event.request_id: user_input}
print("(Resuming workflow...)")
elif isinstance(event, WorkflowOutputEvent):
print("\n" + "=" * 60)
print("WORKFLOW COMPLETE")
print("=" * 60)
print("Aggregated output:")
# Custom aggregator returns a string
if event.data:
print(event.data)
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
workflow_complete = True
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,177 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Sample: Request Info with GroupChatBuilder
This sample demonstrates using the `.with_request_info()` method to pause a
GroupChatBuilder workflow BEFORE specific participants speak. By using the
`agents=` filter parameter, you can target only certain participants rather
than pausing before every turn.
Purpose:
Show how to use the request info API with selective filtering to pause before
specific participants speak, allowing human input to steer their response.
Demonstrate:
- Configuring request info with `.with_request_info(agents=[...])`
- Using agent filtering to reduce interruptions
- Steering agent behavior with pre-agent human input
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from agent_framework import (
AgentExecutorResponse,
AgentRequestInfoResponse,
AgentResponse,
AgentRunUpdateEvent,
ChatMessage,
GroupChatBuilder,
RequestInfoEvent,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
async def main() -> None:
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents for a group discussion
optimist = chat_client.as_agent(
name="optimist",
instructions=(
"You are an optimistic team member. You see opportunities and potential "
"in ideas. Engage constructively with the discussion, building on others' "
"points while maintaining a positive outlook. Keep responses to 2-3 sentences."
),
)
pragmatist = chat_client.as_agent(
name="pragmatist",
instructions=(
"You are a pragmatic team member. You focus on practical implementation "
"and realistic timelines. Sometimes you disagree with overly optimistic views. "
"Keep responses to 2-3 sentences."
),
)
creative = chat_client.as_agent(
name="creative",
instructions=(
"You are a creative team member. You propose innovative solutions and "
"think outside the box. You may suggest alternatives to conventional approaches. "
"Keep responses to 2-3 sentences."
),
)
# Orchestrator coordinates the discussion
orchestrator = chat_client.as_agent(
name="orchestrator",
instructions=(
"You are a discussion manager coordinating a team conversation between participants. "
"Your job is to select who speaks next.\n\n"
"RULES:\n"
"1. Rotate through ALL participants - do not favor any single participant\n"
"2. Each participant should speak at least once before any participant speaks twice\n"
"3. Continue for at least 5 rounds before ending the discussion\n"
"4. Do NOT select the same participant twice in a row"
),
)
# Build workflow with request info enabled
# Using agents= filter to only pause before pragmatist speaks (not every turn)
workflow = (
GroupChatBuilder()
.with_agent_orchestrator(orchestrator)
.participants([optimist, pragmatist, creative])
.with_max_rounds(6)
.with_request_info(agents=[pragmatist]) # Only pause before pragmatist speaks
.build()
)
# Run the workflow with human-in-the-loop
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
current_agent: str | None = None # Track current streaming agent
print("Starting group discussion workflow...")
print("=" * 60)
while not workflow_complete:
# Run or continue the workflow
stream = (
workflow.send_responses_streaming(pending_responses)
if pending_responses
else workflow.run_stream(
"Discuss how our team should approach adopting AI tools for productivity. "
"Consider benefits, risks, and implementation strategies."
)
)
pending_responses = None
# Process events
async for event in stream:
if isinstance(event, AgentRunUpdateEvent):
# Show all agent responses as they stream
if event.data and event.data.text:
agent_name = event.data.author_name or "unknown"
# Print agent name header only when agent changes
if agent_name != current_agent:
current_agent = agent_name
print(f"\n[{agent_name}]: ", end="", flush=True)
print(event.data.text, end="", flush=True)
elif isinstance(event, RequestInfoEvent):
current_agent = None # Reset for next agent
if isinstance(event.data, AgentExecutorResponse):
# Display pre-agent context for human input
print("\n" + "-" * 40)
print("INPUT REQUESTED")
print(f"About to call agent: {event.source_executor_id}")
print("-" * 40)
print("Conversation context:")
agent_response: AgentResponse = event.data.agent_response
messages: list[ChatMessage] = agent_response.messages
recent: list[ChatMessage] = messages[-3:] if len(messages) > 3 else messages # type: ignore
for msg in recent:
name = msg.author_name or "unknown"
text = (msg.text or "")[:100]
print(f" [{name}]: {text}...")
print("-" * 40)
# Get human input to steer the agent
user_input = input(f"Feedback for {event.source_executor_id} (or 'skip' to approve): ") # noqa: ASYNC250
if user_input.lower() == "skip":
pending_responses = {event.request_id: AgentRequestInfoResponse.approve()}
else:
pending_responses = {event.request_id: AgentRequestInfoResponse.from_strings([user_input])}
print("(Resuming discussion...)")
elif isinstance(event, WorkflowOutputEvent):
print("\n" + "=" * 60)
print("DISCUSSION COMPLETE")
print("=" * 60)
print("Final conversation:")
if event.data:
messages: list[ChatMessage] = event.data
for msg in messages:
role = msg.role.value.capitalize()
name = msg.author_name or "unknown"
text = (msg.text or "")[:200]
print(f"[{role}][{name}]: {text}...")
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
workflow_complete = True
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,257 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from dataclasses import dataclass
from agent_framework import (
AgentExecutorRequest, # Message bundle sent to an AgentExecutor
AgentExecutorResponse,
ChatAgent, # Result returned by an AgentExecutor
ChatMessage, # Chat message structure
Executor, # Base class for workflow executors
RequestInfoEvent, # Event emitted when human input is requested
Role, # Enum of chat roles (user, assistant, system)
WorkflowBuilder, # Fluent builder for assembling the graph
WorkflowContext, # Per run context and event bus
WorkflowOutputEvent, # Event emitted when workflow yields output
WorkflowRunState, # Enum of workflow run states
WorkflowStatusEvent, # Event emitted on run state changes
handler,
response_handler, # Decorator to expose an Executor method as a step
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
from pydantic import BaseModel
"""
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 `send_responses_streaming`.
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_stream and responses parameter.
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Basic familiarity with WorkflowBuilder, executors, edges, events, and streaming runs.
"""
# How human-in-the-loop is achieved via `request_info` and `send_responses_streaming`:
# - An executor (TurnManager) calls `ctx.request_info` with a payload (HumanFeedbackRequest).
# - The workflow run pauses and emits a RequestInfoEvent with the payload and the request_id.
# - The application captures the event, prompts the user, and collects replies.
# - The application calls `send_responses_streaming` 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 = ChatMessage(Role.USER, text="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) Parse the agent's JSON into GuessOutput for robustness.
2) Request info with a HumanFeedbackRequest as the payload.
"""
# Parse structured model output
text = result.agent_response.text
last_guess = GuessOutput.model_validate_json(text).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."""
print(f"Feedback for prompt '{original_request.prompt}' received: {feedback}")
reply = feedback.strip().lower()
if reply == "correct":
await ctx.yield_output("Guessed correctly!")
return
# Provide feedback to the agent to try again.
# We keep the agent's output strictly JSON to ensure stable parsing on the next turn.
user_msg = ChatMessage(
Role.USER,
text=(f'Feedback: {reply}. Return ONLY a JSON object matching the schema {{"guess": <int 1..10>}}.'),
)
await ctx.send_message(AgentExecutorRequest(messages=[user_msg], should_respond=True))
def create_guessing_agent() -> ChatAgent:
"""Create the guessing agent with instructions to guess a number between 1 and 10."""
return AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
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={"response_format": GuessOutput},
)
async def main() -> None:
"""Run the human-in-the-loop guessing game workflow."""
# Build a simple loop: TurnManager <-> AgentExecutor.
workflow = (
WorkflowBuilder()
.register_agent(create_guessing_agent, name="guessing_agent")
.register_executor(lambda: TurnManager(id="turn_manager"), name="turn_manager")
.set_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()
# Human in the loop run: alternate between invoking the workflow and supplying collected responses.
pending_responses: dict[str, str] | None = None
workflow_output: str | None = None
# User guidance printing:
# If you want to instruct users up front, print a short banner before the loop.
# Example:
# print(
# "Interactive mode. When prompted, type one of: higher, lower, correct, or exit. "
# "The agent will keep guessing until you reply correct.",
# flush=True,
# )
while workflow_output is None:
# First iteration uses run_stream("start").
# Subsequent iterations use send_responses_streaming with pending_responses from the console.
stream = (
workflow.send_responses_streaming(pending_responses) if pending_responses else workflow.run_stream("start")
)
# Collect events for this turn. Among these you may see WorkflowStatusEvent
# with state IDLE_WITH_PENDING_REQUESTS when the workflow pauses for
# human input, preceded by IN_PROGRESS_PENDING_REQUESTS as requests are
# emitted.
events = [event async for event in stream]
pending_responses = None
# Collect human requests, workflow outputs, and check for completion.
requests: list[tuple[str, str]] = [] # (request_id, prompt)
for event in events:
if isinstance(event, RequestInfoEvent) and isinstance(event.data, HumanFeedbackRequest):
# RequestInfoEvent for our HumanFeedbackRequest.
requests.append((event.request_id, event.data.prompt))
elif isinstance(event, WorkflowOutputEvent):
# Capture workflow output as they're yielded
workflow_output = str(event.data)
# Detect run state transitions for a better developer experience.
pending_status = any(
isinstance(e, WorkflowStatusEvent) and e.state == WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS
for e in events
)
idle_with_requests = any(
isinstance(e, WorkflowStatusEvent) and e.state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS
for e in events
)
if pending_status:
print("State: IN_PROGRESS_PENDING_REQUESTS (requests outstanding)")
if idle_with_requests:
print("State: IDLE_WITH_PENDING_REQUESTS (awaiting human input)")
# If we have any human requests, prompt the user and prepare responses.
if requests:
responses: dict[str, str] = {}
for req_id, prompt in requests:
# Simple console prompt for the sample.
print(f"HITL> {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
responses[req_id] = answer
pending_responses = responses
# Show final result from workflow output captured during streaming.
print(f"Workflow output: {workflow_output}")
"""
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())

View File

@@ -0,0 +1,143 @@
# 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 RequestInfoEvent with AgentInputRequest data
- Injecting responses back into the workflow via send_responses_streaming
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient with required environment variables
- Authentication via azure-identity (run az login before executing)
"""
import asyncio
from agent_framework import (
AgentExecutorResponse,
AgentRequestInfoResponse,
ChatMessage,
RequestInfoEvent,
SequentialBuilder,
WorkflowOutputEvent,
WorkflowRunState,
WorkflowStatusEvent,
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
async def main() -> None:
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create agents for a sequential document review workflow
drafter = chat_client.as_agent(
name="drafter",
instructions=("You are a document drafter. When given a topic, create a brief draft (2-3 sentences)."),
)
editor = chat_client.as_agent(
name="editor",
instructions=(
"You are an editor. Review the draft and make improvements. "
"Incorporate any human feedback that was provided."
),
)
finalizer = chat_client.as_agent(
name="finalizer",
instructions=(
"You are a finalizer. Take the edited content and create a polished final version. "
"Incorporate any additional feedback provided."
),
)
# Build workflow with request info enabled (pauses after each agent responds)
workflow = (
SequentialBuilder()
.participants([drafter, editor, finalizer])
# Only enable request info for the editor agent
.with_request_info(agents=["editor"])
.build()
)
# Run the workflow with request info handling
pending_responses: dict[str, AgentRequestInfoResponse] | None = None
workflow_complete = False
print("Starting document review workflow...")
print("=" * 60)
while not workflow_complete:
# Run or continue the workflow
stream = (
workflow.send_responses_streaming(pending_responses)
if pending_responses
else workflow.run_stream("Write a brief introduction to artificial intelligence.")
)
pending_responses = None
# Process events
async for event in stream:
if isinstance(event, RequestInfoEvent):
if isinstance(event.data, AgentExecutorResponse):
# Display agent response and conversation context for review
print("\n" + "-" * 40)
print("REQUEST INFO: INPUT REQUESTED")
print(
f"Agent {event.source_executor_id} just responded with: '{event.data.agent_response.text}'. "
"Please provide your feedback."
)
print("-" * 40)
if event.data.full_conversation:
print("Conversation context:")
recent = (
event.data.full_conversation[-2:]
if len(event.data.full_conversation) > 2
else event.data.full_conversation
)
for msg in recent:
name = msg.author_name or msg.role.value
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])
pending_responses = {event.request_id: user_input}
print("(Resuming workflow...)")
elif isinstance(event, WorkflowOutputEvent):
print("\n" + "=" * 60)
print("WORKFLOW COMPLETE")
print("=" * 60)
print("Final output:")
if event.data:
messages: list[ChatMessage] = event.data[-3:]
for msg in messages:
role = msg.role.value if msg.role else "unknown"
print(f"[{role}]: {msg.text}")
workflow_complete = True
elif isinstance(event, WorkflowStatusEvent) and event.state == WorkflowRunState.IDLE:
workflow_complete = True
if __name__ == "__main__":
asyncio.run(main())