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