diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 40bb0cf1..8a1b6b90 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -824,6 +824,11 @@ "source_path": "semantic-kernel/Frameworks/agent/examples/example-agent-collaboration.md", "redirect_url": "/semantic-kernel/support/archive/agent-chat-example", "redirect_document_id": false + }, + { + "source_path": "agent-framework/tutorials/workflows/visualization.md", + "redirect_url": "/agent-framework/user-guide/workflows/visualization", + "redirect_document_id": true } ] } diff --git a/agent-framework/TOC.yml b/agent-framework/TOC.yml index 9af22a76..ddfc117c 100644 --- a/agent-framework/TOC.yml +++ b/agent-framework/TOC.yml @@ -16,6 +16,10 @@ items: href: user-guide/model-context-protocol/TOC.yml - name: Workflows href: user-guide/workflows/TOC.yml + - name: Hosting + href: user-guide/hosting/TOC.yml + - name: DevUI + href: user-guide/devui/TOC.yml - name: Integrations items: - name: AG-UI diff --git a/agent-framework/integrations/ag-ui/getting-started.md b/agent-framework/integrations/ag-ui/getting-started.md index 18c7a069..c9bc539c 100644 --- a/agent-framework/integrations/ag-ui/getting-started.md +++ b/agent-framework/integrations/ag-ui/getting-started.md @@ -387,13 +387,13 @@ The AG-UI server hosts your AI agent and exposes it via HTTP endpoints using Fas Install the necessary packages for the server: ```bash -pip install agent-framework-ag-ui +pip install agent-framework-ag-ui --pre ``` Or using uv: ```bash -uv pip install agent-framework-ag-ui +uv pip install agent-framework-ag-ui --prerelease=allow ``` This will automatically install `agent-framework-core`, `fastapi`, and `uvicorn` as dependencies. @@ -425,7 +425,7 @@ if not deployment_name: chat_client = AzureOpenAIChatClient( credential=AzureCliCredential(), endpoint=endpoint, - deployment_name=deployment_name, + deployment_name=deployment_name, ) # Create the AI agent @@ -488,7 +488,7 @@ The AG-UI package is already installed, which includes the `AGUIChatClient`: ```bash # Already installed with agent-framework-ag-ui -pip install agent-framework-ag-ui +pip install agent-framework-ag-ui --pre ``` ### Client Code @@ -513,7 +513,7 @@ async def main(): # Create AG-UI chat client chat_client = AGUIChatClient(server_url=server_url) - + # Create agent with the chat client agent = ChatAgent( name="ClientAgent", diff --git a/agent-framework/integrations/ag-ui/index.md b/agent-framework/integrations/ag-ui/index.md index d7b8a755..2fac4fcf 100644 --- a/agent-framework/integrations/ag-ui/index.md +++ b/agent-framework/integrations/ag-ui/index.md @@ -48,7 +48,7 @@ The Agent Framework AG-UI integration supports all 7 AG-UI protocol features: ## Build agent UIs with CopilotKit -[CopilotKit](https://copilotkit.ai/) provides rich UI components for building agent user interfaces based on the standard AG-UI protocol. CopilotKit supports streaming chat interfaces, frontend & backend tool calling, human-in-the-loop interactions, generative UI, shared state, and much more. You can see a examples of the various agent UI scenarios that CopilotKit supports in the [AG-UI Dojo](https://dojo.ag-ui.com/microsoft-agent-framework-dotnet) sample application. +[CopilotKit](https://copilotkit.ai/) provides rich UI components for building agent user interfaces based on the standard AG-UI protocol. CopilotKit supports streaming chat interfaces, frontend & backend tool calling, human-in-the-loop interactions, generative UI, shared state, and much more. You can see a examples of the various agent UI scenarios that CopilotKit supports in the [AG-UI Dojo](https://dojo.ag-ui.com/microsoft-agent-framework-dotnet) sample application. CopilotKit helps you focus on your agent’s capabilities while delivering a polished user experience without reinventing the wheel. To learn more about getting started with Microsoft Agent Framework and CopilotKit, see the [Microsoft Agent Framework integration for CopilotKit](https://docs.copilotkit.ai/microsoft-agent-framework) documentation. @@ -233,7 +233,7 @@ Understanding how Agent Framework concepts map to AG-UI helps you build effectiv Install the AG-UI integration package: ```bash -pip install agent-framework-ag-ui +pip install agent-framework-ag-ui --pre ``` This installs both the core agent framework and AG-UI integration components. diff --git a/agent-framework/integrations/ag-ui/state-management.md b/agent-framework/integrations/ag-ui/state-management.md index 9981717c..149abf0a 100644 --- a/agent-framework/integrations/ag-ui/state-management.md +++ b/agent-framework/integrations/ag-ui/state-management.md @@ -488,7 +488,7 @@ agent = ChatAgent( chat_client=AzureOpenAIChatClient( credential=AzureCliCredential(), endpoint=endpoint, - deployment_name=deployment_name, + deployment_name=deployment_name, ), tools=[update_recipe], ) @@ -614,7 +614,7 @@ async def main(): # Create AG-UI chat client chat_client = AGUIChatClient(server_url=server_url) - + # Wrap with ChatAgent for convenient API agent = ChatAgent( name="ClientAgent", @@ -624,7 +624,7 @@ async def main(): # Get a thread for conversation continuity thread = agent.get_new_thread() - + # Track state locally state: dict[str, Any] = {} @@ -647,7 +647,7 @@ async def main(): # Handle text content if update.text: print(update.text, end="", flush=True) - + # Handle state updates for content in update.contents: # STATE_SNAPSHOT events come as DataContent with application/json @@ -656,7 +656,7 @@ async def main(): state_data = json.loads(content.data.decode() if isinstance(content.data, bytes) else content.data) state = state_data print("\n[State Snapshot Received]") - + # STATE_DELTA events are handled similarly # Apply JSON Patch deltas to maintain state if hasattr(content, 'delta') and content.delta: @@ -672,7 +672,7 @@ async def main(): if __name__ == "__main__": - # Install dependencies: pip install agent-framework-ag-ui jsonpatch + # Install dependencies: pip install agent-framework-ag-ui jsonpatch --pre asyncio.run(main()) ``` @@ -913,7 +913,7 @@ Always write the complete state, not just deltas: def update_recipe(recipe: Recipe) -> str: """ You MUST write the complete recipe with ALL fields. - When modifying a recipe, include ALL existing ingredients and + When modifying a recipe, include ALL existing ingredients and instructions plus your changes. NEVER delete existing data. """ return "Recipe updated." diff --git a/agent-framework/media/durable-agent-chat-history-tutorial.png b/agent-framework/media/durable-agent-chat-history-tutorial.png new file mode 100644 index 00000000..5046152c Binary files /dev/null and b/agent-framework/media/durable-agent-chat-history-tutorial.png differ diff --git a/agent-framework/media/durable-agent-chat-history.png b/agent-framework/media/durable-agent-chat-history.png new file mode 100644 index 00000000..6b62be1b Binary files /dev/null and b/agent-framework/media/durable-agent-chat-history.png differ diff --git a/agent-framework/media/durable-agent-orchestration.png b/agent-framework/media/durable-agent-orchestration.png new file mode 100644 index 00000000..1df10380 Binary files /dev/null and b/agent-framework/media/durable-agent-orchestration.png differ diff --git a/agent-framework/migration-guide/from-autogen/index.md b/agent-framework/migration-guide/from-autogen/index.md index 2fef9fb1..3cbde666 100644 --- a/agent-framework/migration-guide/from-autogen/index.md +++ b/agent-framework/migration-guide/from-autogen/index.md @@ -1,9 +1,9 @@ --- title: AutoGen to Microsoft Agent Framework Migration Guide description: A comprehensive guide for migrating from AutoGen to the Microsoft Agent Framework Python SDK. -author: ekzhu +author: moonbox3 ms.topic: reference -ms.author: ekzhu +ms.author: evmattso ms.date: 09/29/2025 ms.service: agent-framework --- @@ -1195,85 +1195,134 @@ result = await team.run("Complex research and analysis task") **Agent Framework Implementation:** ```python +from typing import cast from agent_framework import ( - MagenticBuilder, MagenticCallbackMode, WorkflowOutputEvent, - MagenticCallbackEvent, MagenticOrchestratorMessageEvent, MagenticAgentDeltaEvent + MAGENTIC_EVENT_TYPE_AGENT_DELTA, + MAGENTIC_EVENT_TYPE_ORCHESTRATOR, + AgentRunUpdateEvent, + ChatAgent, + ChatMessage, + MagenticBuilder, + WorkflowOutputEvent, ) +from agent_framework.openai import OpenAIChatClient -# Assume we have researcher, coder, and coordinator_client from previous examples -async def on_event(event: MagenticCallbackEvent) -> None: - if isinstance(event, MagenticOrchestratorMessageEvent): - print(f"[ORCHESTRATOR]: {event.message.text}") - elif isinstance(event, MagenticAgentDeltaEvent): - print(f"[{event.agent_id}]: {event.text}", end="") - -workflow = (MagenticBuilder() - .participants(researcher=researcher, coder=coder) - .on_event(on_event, mode=MagenticCallbackMode.STREAMING) - .with_standard_manager( - chat_client=coordinator_client, - max_round_count=20, - max_stall_count=3, - max_reset_count=2 - ) - .build()) +# Create a manager agent for orchestration +manager_agent = ChatAgent( + name="MagenticManager", + description="Orchestrator that coordinates the workflow", + instructions="You coordinate a team to complete complex tasks efficiently.", + chat_client=OpenAIChatClient(), +) + +workflow = ( + MagenticBuilder() + .participants(researcher=researcher, coder=coder) + .with_standard_manager( + agent=manager_agent, + max_round_count=20, + max_stall_count=3, + max_reset_count=2, + ) + .build() +) # Example usage (would be in async context) async def magentic_example(): + output: str | None = None async for event in workflow.run_stream("Complex research task"): - if isinstance(event, WorkflowOutputEvent): - final_result = event.data + if isinstance(event, AgentRunUpdateEvent): + props = event.data.additional_properties if event.data else None + event_type = props.get("magentic_event_type") if props else None + + if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR: + text = event.data.text if event.data else "" + print(f"[ORCHESTRATOR]: {text}") + elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA: + agent_id = props.get("agent_id", event.executor_id) if props else event.executor_id + if event.data and event.data.text: + print(f"[{agent_id}]: {event.data.text}", end="") + + elif isinstance(event, WorkflowOutputEvent): + output_messages = cast(list[ChatMessage], event.data) + if output_messages: + output = output_messages[-1].text ``` **Agent Framework Customization Options:** The Magentic workflow provides extensive customization options: -- **Manager configuration**: Custom orchestrator models and prompts +- **Manager configuration**: Use a ChatAgent with custom instructions and model settings - **Round limits**: `max_round_count`, `max_stall_count`, `max_reset_count` -- **Event callbacks**: Real-time streaming with granular event filtering +- **Event streaming**: Use `AgentRunUpdateEvent` with `magentic_event_type` metadata - **Agent specialization**: Custom instructions and tools per agent -- **Callback modes**: `STREAMING` for real-time updates or `BATCH` for final results -- **Human-in-the-loop planning**: Custom planner functions for interactive workflows +- **Human-in-the-loop**: Plan review, tool approval, and stall intervention ```python # Advanced customization example with human-in-the-loop +from typing import cast +from agent_framework import ( + MAGENTIC_EVENT_TYPE_AGENT_DELTA, + MAGENTIC_EVENT_TYPE_ORCHESTRATOR, + AgentRunUpdateEvent, + ChatAgent, + MagenticBuilder, + MagenticHumanInterventionDecision, + MagenticHumanInterventionKind, + MagenticHumanInterventionReply, + MagenticHumanInterventionRequest, + RequestInfoEvent, + WorkflowOutputEvent, +) from agent_framework.openai import OpenAIChatClient -from agent_framework import MagenticBuilder, MagenticCallbackMode, MagenticPlannerContext - -# Assume we have researcher_agent, coder_agent, analyst_agent, detailed_event_handler -# and get_human_input function defined elsewhere - -async def custom_planner(context: MagenticPlannerContext) -> str: - """Custom planner with human input for critical decisions.""" - if context.round_count > 5: - # Request human input for complex decisions - return await get_human_input(f"Next action for: {context.current_state}") - return "Continue with automated planning" - -workflow = (MagenticBuilder() - .participants( - researcher=researcher_agent, - coder=coder_agent, - analyst=analyst_agent - ) - .with_standard_manager( - chat_client=OpenAIChatClient(model_id="gpt-5"), - max_round_count=15, # Limit total rounds - max_stall_count=2, # Prevent infinite loops - max_reset_count=1, # Allow one reset on failure - orchestrator_prompt="Custom orchestration instructions..." - ) - .with_planner(custom_planner) # Human-in-the-loop planning - .on_event(detailed_event_handler, mode=MagenticCallbackMode.STREAMING) - .build()) + +# Create manager agent with custom configuration +manager_agent = ChatAgent( + name="MagenticManager", + description="Orchestrator for complex tasks", + instructions="Custom orchestration instructions...", + chat_client=OpenAIChatClient(model_id="gpt-4o"), +) + +workflow = ( + MagenticBuilder() + .participants( + researcher=researcher_agent, + coder=coder_agent, + analyst=analyst_agent, + ) + .with_standard_manager( + agent=manager_agent, + max_round_count=15, # Limit total rounds + max_stall_count=2, # Trigger stall handling + max_reset_count=1, # Allow one reset on failure + ) + .with_plan_review() # Enable human plan review + .with_human_input_on_stall() # Enable human intervention on stalls + .build() +) + +# Handle human intervention requests during execution +async for event in workflow.run_stream("Complex task"): + if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest: + req = cast(MagenticHumanInterventionRequest, event.data) + if req.kind == MagenticHumanInterventionKind.PLAN_REVIEW: + # Review and approve the plan + reply = MagenticHumanInterventionReply( + decision=MagenticHumanInterventionDecision.APPROVE + ) + async for ev in workflow.send_responses_streaming({event.request_id: reply}): + pass # Handle continuation ``` For detailed Magentic examples, see: - [Basic Magentic Workflow](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic.py) - Standard orchestrated multi-agent workflow - [Magentic with Checkpointing](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic_checkpoint.py) - Persistent orchestrated workflows -- [Magentic Human Plan Update](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic_human_plan_update.py) - Human-in-the-loop planning +- [Magentic Human Plan Update](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic_human_plan_update.py) - Human-in-the-loop plan review +- [Magentic Agent Clarification](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic_agent_clarification.py) - Tool approval for agent clarification +- [Magentic Human Replan](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/orchestration/magentic_human_replan.py) - Human intervention on stalls #### Future Patterns diff --git a/agent-framework/migration-guide/from-semantic-kernel/index.md b/agent-framework/migration-guide/from-semantic-kernel/index.md index cab01942..d0166e81 100644 --- a/agent-framework/migration-guide/from-semantic-kernel/index.md +++ b/agent-framework/migration-guide/from-semantic-kernel/index.md @@ -321,7 +321,7 @@ from semantic_kernel.agents import ChatCompletionAgent ### Agent Framework Agent Framework package is installed as `agent-framework` and imported as `agent_framework`. -Agent Framework is built up differently, it has a core package `agent-framework-core` that contains the core functionality, and then there are multiple packages that rely on that core package, such as `agent-framework-azure-ai`, `agent-framework-mem0`, `agent-framework-copilotstudio`, etc. When you run `pip install agent-framework` it will install the core package and *all* packages, so that you can get started with all the features quickly. When you are ready to reduce the number of packages because you know what you need, you can install only the packages you need, so for instance if you only plan to use Azure AI Foundry and Mem0 you can install only those two packages: `pip install agent-framework-azure-ai agent-framework-mem0`, `agent-framework-core` is a dependency to those two, so will automatically be installed. +Agent Framework is built up differently, it has a core package `agent-framework-core` that contains the core functionality, and then there are multiple packages that rely on that core package, such as `agent-framework-azure-ai`, `agent-framework-mem0`, `agent-framework-copilotstudio`, etc. When you run `pip install agent-framework --pre` it will install the core package and *all* packages, so that you can get started with all the features quickly. When you are ready to reduce the number of packages because you know what you need, you can install only the packages you need, so for instance if you only plan to use Azure AI Foundry and Mem0 you can install only those two packages: `pip install agent-framework-azure-ai agent-framework-mem0 --pre`, `agent-framework-core` is a dependency to those two, so will automatically be installed. Even though the packages are split up, the imports are all from `agent_framework`, or it's modules. So for instance to import the client for Azure AI Foundry you would do: diff --git a/agent-framework/overview/agent-framework-overview.md b/agent-framework/overview/agent-framework-overview.md index f3d0f471..608453d8 100644 --- a/agent-framework/overview/agent-framework-overview.md +++ b/agent-framework/overview/agent-framework-overview.md @@ -1,9 +1,9 @@ --- title: Introduction to Microsoft Agent Framework description: Learn about Microsoft Agent Framework -author: ekzhu +author: markwallace-microsoft ms.topic: reference -ms.author: ekzhu +ms.author: markwallace ms.date: 10/01/2025 ms.service: agent-framework --- @@ -57,7 +57,7 @@ and the same is expected for Agent Framework. Microsoft Agent Framework welcomes Python: ```bash -pip install agent-framework +pip install agent-framework --pre ``` .NET: diff --git a/agent-framework/support/index.md b/agent-framework/support/index.md index b54b7ee4..354672f7 100644 --- a/agent-framework/support/index.md +++ b/agent-framework/support/index.md @@ -2,7 +2,7 @@ title: Support for Agent Framework description: Support for Agent Framework author: TaoChenOSU -ms.topic: conceptual +ms.topic: article ms.author: taochen ms.date: 10/30/2025 ms.service: agent-framework @@ -13,7 +13,7 @@ ms.service: agent-framework | Your preference | What's available | |---|---| -| Read the docs | [This learning site](/agent-framework/overview) is the home of the latest information for developers | +| Read the docs | [This learning site](/agent-framework/) is the home of the latest information for developers | | Visit the repo | Our open-source [GitHub repository](https://github.com/microsoft/agent-framework) is available for perusal and suggestions | | Connect with the Agent Framework Team | Visit our [GitHub Discussions](https://github.com/microsoft/agent-framework/discussions) to get supported quickly with our [CoC](https://github.com/microsoft/agent-framework/blob/main/CODE_OF_CONDUCT.md) actively enforced | -| Office Hours | We will be hosting regular office hours; the calendar invites and cadence are located here: [Community.MD](https://github.com/microsoft/agent-framework/blob/main/COMMUNITY.md) | \ No newline at end of file +| Office Hours | We will be hosting regular office hours; the calendar invites and cadence are located here: [Community.MD](https://github.com/microsoft/agent-framework/blob/main/COMMUNITY.md) | diff --git a/agent-framework/tutorials/TOC.yml b/agent-framework/tutorials/TOC.yml index e48cfea3..48a83a86 100644 --- a/agent-framework/tutorials/TOC.yml +++ b/agent-framework/tutorials/TOC.yml @@ -2,5 +2,7 @@ href: overview.md - name: Agents href: agents/TOC.yml +- name: Plugins + href: plugins/TOC.yml - name: Workflows href: workflows/TOC.yml diff --git a/agent-framework/tutorials/agents/TOC.yml b/agent-framework/tutorials/agents/TOC.yml index 12436d82..1495879c 100644 --- a/agent-framework/tutorials/agents/TOC.yml +++ b/agent-framework/tutorials/agents/TOC.yml @@ -23,4 +23,10 @@ - name: Third Party chat history storage href: third-party-chat-history-storage.md - name: Adding memory to agents - href: memory.md \ No newline at end of file + href: memory.md +- name: Durable agents + items: + - name: Create and run a durable agent + href: create-and-run-durable-agent.md + - name: Orchestrate durable agents + href: orchestrate-durable-agents.md \ No newline at end of file diff --git a/agent-framework/tutorials/agents/create-and-run-durable-agent.md b/agent-framework/tutorials/agents/create-and-run-durable-agent.md new file mode 100644 index 00000000..fcd37c95 --- /dev/null +++ b/agent-framework/tutorials/agents/create-and-run-durable-agent.md @@ -0,0 +1,622 @@ +--- +title: Create and run a durable agent +description: Learn how to create and run a durable AI agent with Azure Functions and the durable task extension for Microsoft Agent Framework +zone_pivot_groups: programming-languages +author: anthonychu +ms.topic: tutorial +ms.author: antchu +ms.date: 11/05/2025 +ms.service: agent-framework +--- + +# Create and run a durable agent + +This tutorial shows you how to create and run a [durable AI agent](../../user-guide/agents/agent-types/durable-agent/create-durable-agent.md) using the durable task extension for Microsoft Agent Framework. You'll build an Azure Functions app that hosts a stateful agent with built-in HTTP endpoints, and learn how to monitor it using the Durable Task Scheduler dashboard. + +Durable agents provide serverless hosting with automatic state management, allowing your agents to maintain conversation history across multiple interactions without managing infrastructure. + +## Prerequisites + +Before you begin, ensure you have the following prerequisites: + +::: zone pivot="programming-language-csharp" + +- [.NET 9.0 SDK or later](https://dotnet.microsoft.com/download) +- [Azure Functions Core Tools v4.x](/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) +- [Azure Developer CLI (azd)](/azure/developer/azure-developer-cli/install-azd) +- [Azure CLI installed](/cli/azure/install-azure-cli) and [authenticated](/cli/azure/authenticate-azure-cli) +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running (for local development with Azurite and the Durable Task Scheduler emulator) +- An Azure subscription with permissions to create resources + +> [!NOTE] +> Microsoft Agent Framework is supported with all actively supported versions of .NET. For the purposes of this sample, we recommend the .NET 9 SDK or a later version. + +::: zone-end + +::: zone pivot="programming-language-python" + +- [Python 3.10 or later](https://www.python.org/downloads/) +- [Azure Functions Core Tools v4.x](/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) +- [Azure Developer CLI (azd)](/azure/developer/azure-developer-cli/install-azd) +- [Azure CLI installed](/cli/azure/install-azure-cli) and [authenticated](/cli/azure/authenticate-azure-cli) +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running (for local development with Azurite and the Durable Task Scheduler emulator) +- An Azure subscription with permissions to create resources + +::: zone-end + +## Download the quickstart project + +Use Azure Developer CLI to initialize a new project from the durable agents quickstart template. + +::: zone pivot="programming-language-csharp" + +1. Create a new directory for your project and navigate to it: + + # [Bash](#tab/bash) + + ```bash + mkdir MyDurableAgent + cd MyDurableAgent + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + New-Item -ItemType Directory -Path MyDurableAgent + Set-Location MyDurableAgent + ``` + + --- + +1. Initialize the project from the template: + + ```console + azd init --template durable-agents-quickstart-dotnet + ``` + + When prompted for an environment name, enter a name like `my-durable-agent`. + +This downloads the quickstart project with all necessary files, including the Azure Functions configuration, agent code, and infrastructure as code templates. + +::: zone-end + +::: zone pivot="programming-language-python" + +1. Create a new directory for your project and navigate to it: + + # [Bash](#tab/bash) + + ```bash + mkdir MyDurableAgent + cd MyDurableAgent + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + New-Item -ItemType Directory -Path MyDurableAgent + Set-Location MyDurableAgent + ``` + + --- + +1. Initialize the project from the template: + + ```console + azd init --template durable-agents-quickstart-python + ``` + + When prompted for an environment name, enter a name like `my-durable-agent`. + +1. Create and activate a virtual environment: + + # [Bash](#tab/bash) + + ```bash + python3 -m venv .venv + source .venv/bin/activate + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + python3 -m venv .venv + .venv\Scripts\Activate.ps1 + ``` + + --- + + +1. Install the required packages: + + ```console + python -m pip install -r requirements.txt + ``` + +This downloads the quickstart project with all necessary files, including the Azure Functions configuration, agent code, and infrastructure as code templates. It also prepares a virtual environment with the required dependencies. + +::: zone-end + +## Provision Azure resources + +Use Azure Developer CLI to create the required Azure resources for your durable agent. + +1. Provision the infrastructure: + + ```console + azd provision + ``` + + This command creates: + - An Azure OpenAI service with a gpt-4o-mini deployment + - An Azure Functions app with Flex Consumption hosting plan + - An Azure Storage account for the Azure Functions runtime and durable storage + - A Durable Task Scheduler instance (Consumption plan) for managing agent state + - Necessary networking and identity configurations + +1. When prompted, select your Azure subscription and choose a location for the resources. + +The provisioning process takes a few minutes. Once complete, azd stores the created resource information in your environment. + +## Review the agent code + +Now let's examine the code that defines your durable agent. + +::: zone pivot="programming-language-csharp" + +Open `Program.cs` to see the agent configuration: + +```csharp +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AzureFunctions; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Hosting; +using OpenAI; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT environment variable is not set"); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini"; + +// Create an AI agent following the standard Microsoft Agent Framework pattern +AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent( + instructions: "You are a helpful assistant that can answer questions and provide information.", + name: "MyDurableAgent"); + +using IHost app = FunctionsApplication + .CreateBuilder(args) + .ConfigureFunctionsWebApplication() + .ConfigureDurableAgents(options => options.AddAIAgent(agent)) + .Build(); +app.Run(); +``` + +This code: +1. Retrieves your Azure OpenAI configuration from environment variables. +1. Creates an Azure OpenAI client using Azure credentials. +1. Creates an AI agent with instructions and a name. +1. Configures the Azure Functions app to host the agent with durable thread management. + +::: zone-end + +::: zone pivot="programming-language-python" + +Open `function_app.py` to see the agent configuration: + +```python +import os +from agent_framework.azure import AzureOpenAIChatClient, AgentFunctionApp +from azure.identity import DefaultAzureCredential + +endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") +if not endpoint: + raise ValueError("AZURE_OPENAI_ENDPOINT is not set.") +deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") + +# Create an AI agent following the standard Microsoft Agent Framework pattern +agent = AzureOpenAIChatClient( + endpoint=endpoint, + deployment_name=deployment_name, + credential=DefaultAzureCredential() +).create_agent( + instructions="You are a helpful assistant that can answer questions and provide information.", + name="MyDurableAgent" +) + +# Configure the function app to host the agent with durable thread management +app = AgentFunctionApp(agents=[agent]) +``` + +This code: ++ Retrieves your Azure OpenAI configuration from environment variables. ++ Creates an Azure OpenAI client using Azure credentials. ++ Creates an AI agent with instructions and a name. ++ Configures the Azure Functions app to host the agent with durable thread management. + +::: zone-end + +The agent is now ready to be hosted in Azure Functions. The durable task extension automatically creates HTTP endpoints for interacting with your agent and manages conversation state across multiple requests. + +## Configure local settings + +Create a `local.settings.json` file for local development based on the sample file included in the project. + +1. Copy the sample settings file: + + # [Bash](#tab/bash) + + ```bash + cp local.settings.sample.json local.settings.json + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + Copy-Item local.settings.sample.json local.settings.json + ``` + + --- + +1. Get your Azure OpenAI endpoint from the provisioned resources: + + ```console + azd env get-value AZURE_OPENAI_ENDPOINT + ``` + +1. Open `local.settings.json` and replace `` in the `AZURE_OPENAI_ENDPOINT` value with the endpoint from the previous command. + +Your `local.settings.json` should look like this: + +```json +{ + "IsEncrypted": false, + "Values": { + // ... other settings ... + "AZURE_OPENAI_ENDPOINT": "https://your-openai-resource.openai.azure.com", + "AZURE_OPENAI_DEPLOYMENT": "gpt-4o-mini", + "TASKHUB_NAME": "default" + } +} +``` + +> [!NOTE] +> The `local.settings.json` file is used for local development only and is not deployed to Azure. For production deployments, these settings are automatically configured in your Azure Functions app by the infrastructure templates. + +## Start local development dependencies + +To run durable agents locally, you need to start two services: +- **Azurite**: Emulates Azure Storage services (used by Azure Functions for managing triggers and internal state). +- **Durable Task Scheduler (DTS) emulator**: Manages durable state (conversation history, orchestration state) and scheduling for your agents + +### Start Azurite + +Azurite emulates Azure Storage services locally. The Azure Functions uses it for managing internal state. You'll need to run this in a new terminal window and keep it running while you develop and test your durable agent. + +1. Open a new terminal window and pull the Azurite Docker image: + + ```console + docker pull mcr.microsoft.com/azure-storage/azurite + ``` + +1. Start Azurite in a terminal window: + + ```console + docker run -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite + ``` + + Azurite will start and listen on the default ports for Blob (10000), Queue (10001), and Table (10002) services. + +Keep this terminal window open while you're developing and testing your durable agent. + +> [!TIP] +> For more information about Azurite, including alternative installation methods, see [Use Azurite emulator for local Azure Storage development](/azure/storage/common/storage-use-azurite). + +### Start the Durable Task Scheduler emulator + +The DTS emulator provides the durable backend for managing agent state and orchestrations. It stores conversation history and ensures your agent's state persists across restarts. It also triggers durable orchestrations and agents. You'll need to run this in a separate new terminal window and keep it running while you develop and test your durable agent. + +1. Open another new terminal window and pull the DTS emulator Docker image: + + ```console + docker pull mcr.microsoft.com/dts/dts-emulator:latest + ``` + +1. Run the DTS emulator: + + ```console + docker run -p 8080:8080 -p 8082:8082 mcr.microsoft.com/dts/dts-emulator:latest + ``` + + This command starts the emulator and exposes: + - Port 8080: The gRPC endpoint for the Durable Task Scheduler (used by your Functions app) + - Port 8082: The administrative dashboard + +1. The dashboard will be available at `http://localhost:8082`. + +Keep this terminal window open while you're developing and testing your durable agent. + +> [!TIP] +> To learn more about the DTS emulator, including how to configure multiple task hubs and access the dashboard, see [Develop with Durable Task Scheduler](/azure/azure-functions/durable/durable-task-scheduler/develop-with-durable-task-scheduler). + +## Run the function app + +Now you're ready to run your Azure Functions app with the durable agent. + +1. In a new terminal window (keeping both Azurite and the DTS emulator running in separate windows), navigate to your project directory. + +1. Start the Azure Functions runtime: + + ```console + func start + ``` + +1. You should see output indicating that your function app is running, including the HTTP endpoints for your agent: + + ``` + Functions: + http-MyDurableAgent: [POST] http://localhost:7071/api/agents/MyDurableAgent/run + dafx-MyDurableAgent: entityTrigger + ``` + +These endpoints manage conversation state automatically - you don't need to create or manage thread objects yourself. + +## Test the agent locally + +Now you can interact with your durable agent using HTTP requests. The agent maintains conversation state across multiple requests, enabling multi-turn conversations. + +### Start a new conversation + +Create a new thread and send your first message: + +# [Bash](#tab/bash) + +```bash +curl -i -X POST http://localhost:7071/api/agents/MyDurableAgent/run \ + -H "Content-Type: text/plain" \ + -d "What are three popular programming languages?" +``` + +# [PowerShell](#tab/powershell) + +```powershell +$response = Invoke-WebRequest -Uri "http://localhost:7071/api/agents/MyDurableAgent/run" ` + -Method POST ` + -Headers @{"Content-Type"="text/plain"} ` + -Body "What are three popular programming languages?" +$response.Headers +$response.Content +``` + +--- + +Sample response (note the `x-ms-thread-id` header contains the thread ID): + +``` +HTTP/1.1 200 OK +Content-Type: text/plain +x-ms-thread-id: @dafx-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d +Content-Length: 189 + +Three popular programming languages are Python, JavaScript, and Java. Python is known for its simplicity and readability, JavaScript powers web interactivity, and Java is widely used in enterprise applications. +``` + +Save the thread ID from the `x-ms-thread-id` header (e.g., `@dafx-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d`) for the next request. + +### Continue the conversation + +Send a follow-up message to the same thread by including the thread ID as a query parameter: + +# [Bash](#tab/bash) + +```bash +curl -X POST "http://localhost:7071/api/agents/MyDurableAgent/run?thread_id=@dafx-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d" \ + -H "Content-Type: text/plain" \ + -d "Which one is best for beginners?" +``` + +# [PowerShell](#tab/powershell) + +```powershell +$threadId = "@dafx-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d" +Invoke-RestMethod -Uri "http://localhost:7071/api/agents/MyDurableAgent/run?thread_id=$threadId" ` + -Method POST ` + -Headers @{"Content-Type"="text/plain"} ` + -Body "Which one is best for beginners?" +``` + +--- + +Replace `@dafx-mydurableagent@263fa373-fa01-4705-abf2-5a114c2bb87d` with the actual thread ID from the previous response's `x-ms-thread-id` header. + +Sample response: + +``` +Python is often considered the best choice for beginners among those three. Its clean syntax reads almost like English, making it easier to learn programming concepts without getting overwhelmed by complex syntax. It's also versatile and widely used in education. +``` + +Notice that the agent remembers the context from the previous message (the three programming languages) without you having to specify them again. Because the conversation state is stored durably by the Durable Task Scheduler, this history persists even if you restart the function app or the conversation is resumed by a different instance. + +## Monitor with the Durable Task Scheduler dashboard + +The Durable Task Scheduler provides a built-in dashboard for monitoring and debugging your durable agents. The dashboard offers deep visibility into agent operations, conversation history, and execution flow. + +### Access the dashboard + +1. Open the dashboard for your local DTS emulator at `http://localhost:8082` in your web browser. + +1. Select the **default** task hub from the list to view its details. + +1. Select the gear icon in the top-right corner to open the settings, and ensure that the **Enable Agent pages** option under *Preview Features* is selected. + +### Explore agent conversations + +1. In the dashboard, navigate to the **Agents** tab. + +1. Select your durable agent thread (e.g., `mydurableagent - 263fa373-fa01-4705-abf2-5a114c2bb87d`) from the list. + + You'll see a detailed view of the agent thread, including the complete conversation history with all messages and responses. + + :::image type="content" source="../../media/durable-agent-chat-history-tutorial.png" alt-text="Screenshot of the Durable Task Scheduler dashboard showing an agent thread's conversation history." lightbox="../../media/durable-agent-chat-history-tutorial.png"::: + +The dashboard provides a timeline view to help you understand the flow of the conversation. Key information include: + +- Timestamps and duration for each interaction +- Prompt and response content +- Number of tokens used + +> [!TIP] +> The DTS dashboard provides real-time updates, so you can watch your agent's behavior as you interact with it through the HTTP endpoints. + +## Deploy to Azure + +Now that you've tested your durable agent locally, deploy it to Azure. + +1. Deploy the application: + + ```console + azd deploy + ``` + + This command packages your application and deploys it to the Azure Functions app created during provisioning. + +1. Wait for the deployment to complete. The output will confirm when your agent is running in Azure. + +## Test the deployed agent + +After deployment, test your agent running in Azure. + +### Get the function key + +Azure Functions requires an API key for HTTP-triggered functions in production: + +# [Bash](#tab/bash) + +```bash +API_KEY=`az functionapp function keys list --name $(azd env get-value AZURE_FUNCTION_NAME) --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --function-name http-MyDurableAgent --query default -o tsv` +``` + +# [PowerShell](#tab/powershell) + +```powershell +$functionName = azd env get-value AZURE_FUNCTION_NAME +$resourceGroup = azd env get-value AZURE_RESOURCE_GROUP +$API_KEY = az functionapp function keys list --name $functionName --resource-group $resourceGroup --function-name http-MyDurableAgent --query default -o tsv +``` + +--- + +### Start a new conversation + +Create a new thread and send your first message to the deployed agent: + +# [Bash](#tab/bash) + +```bash +curl -i -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY" \ + -H "Content-Type: text/plain" \ + -d "What are three popular programming languages?" +``` + +# [PowerShell](#tab/powershell) + +```powershell +$functionName = azd env get-value AZURE_FUNCTION_NAME +$response = Invoke-WebRequest -Uri "https://$functionName.azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY" ` + -Method POST ` + -Headers @{"Content-Type"="text/plain"} ` + -Body "What are three popular programming languages?" +$response.Headers +$response.Content +``` + +--- + +Note the thread ID returned in the `x-ms-thread-id` response header. + +### Continue the conversation + +Send a follow-up message in the same thread. Replace `` with the thread ID from the previous response: + +# [Bash](#tab/bash) + +```bash +THREAD_ID="" +curl -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY&thread_id=$THREAD_ID" \ + -H "Content-Type: text/plain" \ + -d "Which is easiest to learn?" +``` + +# [PowerShell](#tab/powershell) + +```powershell +$THREAD_ID = "" +$functionName = azd env get-value AZURE_FUNCTION_NAME +Invoke-RestMethod -Uri "https://$functionName.azurewebsites.net/api/agents/MyDurableAgent/run?code=$API_KEY&thread_id=$THREAD_ID" ` + -Method POST ` + -Headers @{"Content-Type"="text/plain"} ` + -Body "Which is easiest to learn?" +``` + +--- + +The agent maintains conversation context in Azure just as it did locally, demonstrating the durability of the agent state. + +## Monitor the deployed agent + +You can monitor your deployed agent using the Durable Task Scheduler dashboard in Azure. + +1. Get the name of your Durable Task Scheduler instance: + + ```console + azd env get-value DTS_NAME + ``` + +1. Open the [Azure portal](https://portal.azure.com) and search for the Durable Task Scheduler name from the previous step. + +1. In the overview blade of the Durable Task Scheduler resource, select the **default** task hub from the list. + +1. Select **Open Dashboard** at the top of the task hub page to open the monitoring dashboard. + +1. View your agent's conversations just as you did with the local emulator. + +The Azure-hosted dashboard provides the same debugging and monitoring capabilities as the local emulator, allowing you to inspect conversation history, trace tool calls, and analyze performance in your production environment. + +## Understanding durable agent features + +The durable agent you just created provides several important features that differentiate it from standard agents: + +### Stateful conversations + +The agent automatically maintains conversation state across interactions. Each thread has its own isolated conversation history, stored durably in the Durable Task Scheduler. Unlike stateless APIs where you'd need to send the full conversation history with each request, durable agents manage this for you automatically. + +### Serverless hosting + +Your agent runs in Azure Functions with event-driven, pay-per-invocation pricing. When deployed to Azure with the [Flex Consumption plan](/azure/azure-functions/flex-consumption-plan), your agent can scale to thousands of instances during high traffic or down to zero when not in use, ensuring you only pay for actual usage. + +### Built-in HTTP endpoints + +The durable task extension automatically creates HTTP endpoints for your agent, eliminating the need to write custom HTTP handlers or API code. This includes endpoints for creating threads, sending messages, and retrieving conversation history. + +### Durable state management + +All agent state is managed by the Durable Task Scheduler, ensuring that: +- Conversations survive process crashes and restarts. +- State is distributed across multiple instances for high availability. +- Any instance can resume an agent's execution after interruptions. +- Conversation history is maintained reliably even during scaling events. + +## Next steps + +Now that you have a working durable agent, you can explore more advanced features: + +> [!div class="nextstepaction"] +> [Learn about durable agent features](../../user-guide/agents/agent-types/durable-agent/features.md) + +Additional resources: + +- [Durable Task Scheduler Overview](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Azure Functions Flex Consumption Plan](/azure/azure-functions/flex-consumption-plan) diff --git a/agent-framework/tutorials/agents/enable-observability.md b/agent-framework/tutorials/agents/enable-observability.md index 8185f081..e7368390 100644 --- a/agent-framework/tutorials/agents/enable-observability.md +++ b/agent-framework/tutorials/agents/enable-observability.md @@ -143,7 +143,7 @@ For prerequisites, see the [Create and run a simple agent](./run-agent.md) step To use Agent Framework with Azure OpenAI, you need to install the following packages. Agent Framework automatically includes all necessary OpenTelemetry dependencies: ```bash -pip install agent-framework +pip install agent-framework --pre ``` The following OpenTelemetry packages are included by default: diff --git a/agent-framework/tutorials/agents/orchestrate-durable-agents.md b/agent-framework/tutorials/agents/orchestrate-durable-agents.md new file mode 100644 index 00000000..44effea7 --- /dev/null +++ b/agent-framework/tutorials/agents/orchestrate-durable-agents.md @@ -0,0 +1,478 @@ +--- +title: Orchestrate durable agents +description: Learn how to orchestrate multiple durable AI agents with fan-out/fan-in patterns for concurrent processing +zone_pivot_groups: programming-languages +author: anthonychu +ms.topic: tutorial +ms.author: antchu +ms.date: 11/07/2025 +ms.service: agent-framework +--- + +# Orchestrate durable agents + +This tutorial shows you how to orchestrate multiple durable AI agents using the fan-out/fan-in patterns. You'll extend the durable agent from the [Create and run a durable agent](create-and-run-durable-agent.md) tutorial to create a multi-agent system that processes a user's question, then translates the response into multiple languages concurrently. + +This orchestration pattern demonstrates how to: +- Reuse the durable agent from the first tutorial. +- Create additional durable agents for language translation. +- Fan out to multiple agents for concurrent processing. +- Fan in results and return them as structured JSON. + +## Prerequisites + +Before you begin, you must complete the [Create and run a durable agent](create-and-run-durable-agent.md) tutorial. This tutorial extends the project created in that tutorial by adding orchestration capabilities. + +## Understanding the orchestration pattern + +The orchestration you'll build follows this flow: + +1. **User input** - A question or message from the user +2. **Main agent** - The `MyDurableAgent` from the first tutorial processes the question +3. **Fan-out** - The main agent's response is sent concurrently to both translation agents +4. **Translation agents** - Two specialized agents translate the response (French and Spanish) +5. **Fan-in** - Results are aggregated into a single JSON response with the original response and translations + +This pattern enables concurrent processing, reducing total response time compared to sequential translation. + +## Register agents at startup + +To properly use agents in durable orchestrations, register them at application startup. They can be used across orchestration executions. + +::: zone pivot="programming-language-csharp" + +Update your `Program.cs` to register the translation agents alongside the existing `MyDurableAgent`: + +```csharp +using System; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AzureFunctions; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Hosting; +using OpenAI; +using OpenAI.Chat; + +// Get the Azure OpenAI configuration +string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") + ?? "gpt-4o-mini"; + +// Create the Azure OpenAI client +AzureOpenAIClient client = new(new Uri(endpoint), new DefaultAzureCredential()); +ChatClient chatClient = client.GetChatClient(deploymentName); + +// Create the main agent from the first tutorial +AIAgent mainAgent = chatClient.CreateAIAgent( + instructions: "You are a helpful assistant that can answer questions and provide information.", + name: "MyDurableAgent"); + +// Create translation agents +AIAgent frenchAgent = chatClient.CreateAIAgent( + instructions: "You are a translator. Translate the following text to French. Return only the translation, no explanations.", + name: "FrenchTranslator"); + +AIAgent spanishAgent = chatClient.CreateAIAgent( + instructions: "You are a translator. Translate the following text to Spanish. Return only the translation, no explanations.", + name: "SpanishTranslator"); + +// Build and configure the Functions host +using IHost app = FunctionsApplication + .CreateBuilder(args) + .ConfigureFunctionsWebApplication() + .ConfigureDurableAgents(options => + { + // Register all agents for use in orchestrations and HTTP endpoints + options.AddAIAgent(mainAgent); + options.AddAIAgent(frenchAgent); + options.AddAIAgent(spanishAgent); + }) + .Build(); + +app.Run(); +``` + +This setup: +- Keeps the original `MyDurableAgent` from the first tutorial. +- Creates two new translation agents (French and Spanish). +- Registers all three agents with the Durable Task framework using `options.AddAIAgent()`. +- Makes agents available throughout the application lifetime for individual interactions and orchestrations. + +::: zone-end + +::: zone pivot="programming-language-python" + +Update your `function_app.py` to register the translation agents alongside the existing `MyDurableAgent`: + +```python +import os +from azure.identity import DefaultAzureCredential +from agent_framework.azure import AzureOpenAIChatClient, AgentFunctionApp + +# Get the Azure OpenAI configuration +endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") +if not endpoint: + raise ValueError("AZURE_OPENAI_ENDPOINT is not set.") +deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o-mini") + +# Create the Azure OpenAI client +chat_client = AzureOpenAIChatClient( + endpoint=endpoint, + deployment_name=deployment_name, + credential=DefaultAzureCredential() +) + +# Create the main agent from the first tutorial +main_agent = chat_client.create_agent( + instructions="You are a helpful assistant that can answer questions and provide information.", + name="MyDurableAgent" +) + +# Create translation agents +french_agent = chat_client.create_agent( + instructions="You are a translator. Translate the following text to French. Return only the translation, no explanations.", + name="FrenchTranslator" +) + +spanish_agent = chat_client.create_agent( + instructions="You are a translator. Translate the following text to Spanish. Return only the translation, no explanations.", + name="SpanishTranslator" +) + +# Create the function app and register all agents +app = AgentFunctionApp(agents=[main_agent, french_agent, spanish_agent]) +``` + +This setup: +- Keeps the original `MyDurableAgent` from the first tutorial. +- Creates two new translation agents (French and Spanish). +- Registers all three agents with the Durable Task framework using `AgentFunctionApp(agents=[...])`. +- Makes agents available throughout the application lifetime for individual interactions and orchestrations. + +::: zone-end + +## Create an orchestration function + +An orchestration function coordinates the workflow across multiple agents. It retrieves registered agents from the durable context and orchestrates their execution, first calling the main agent, then fanning out to translation agents concurrently. + +::: zone pivot="programming-language-csharp" + +Create a new file named `AgentOrchestration.cs` in your project directory: + +```csharp +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.DurableTask; +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; + +namespace MyDurableAgent; + +public static class AgentOrchestration +{ + // Define a strongly-typed response structure for agent outputs + public sealed record TextResponse(string Text); + + [Function("agent_orchestration_workflow")] + public static async Task> AgentOrchestrationWorkflow( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + var input = context.GetInput() ?? throw new ArgumentNullException(nameof(context), "Input cannot be null"); + + // Step 1: Get the main agent's response + DurableAIAgent mainAgent = context.GetAgent("MyDurableAgent"); + AgentRunResponse mainResponse = await mainAgent.RunAsync(input); + string agentResponse = mainResponse.Result.Text; + + // Step 2: Fan out - get the translation agents and run them concurrently + DurableAIAgent frenchAgent = context.GetAgent("FrenchTranslator"); + DurableAIAgent spanishAgent = context.GetAgent("SpanishTranslator"); + + Task> frenchTask = frenchAgent.RunAsync(agentResponse); + Task> spanishTask = spanishAgent.RunAsync(agentResponse); + + // Step 3: Wait for both translation tasks to complete (fan-in) + await Task.WhenAll(frenchTask, spanishTask); + + // Get the translation results + TextResponse frenchResponse = (await frenchTask).Result; + TextResponse spanishResponse = (await spanishTask).Result; + + // Step 4: Combine results into a dictionary + var result = new Dictionary + { + ["original"] = agentResponse, + ["french"] = frenchResponse.Text, + ["spanish"] = spanishResponse.Text + }; + + return result; + } +} +``` + +This orchestration demonstrates the proper durable task pattern: +- **Main agent execution**: First calls `MyDurableAgent` to process the user's input. +- **Agent retrieval**: Uses `context.GetAgent()` to get registered agents by name (agents were registered at startup). +- **Sequential then concurrent**: Main agent runs first, then translation agents run concurrently using `Task.WhenAll`. + +::: zone-end + +::: zone pivot="programming-language-python" + +Add the orchestration function to your `function_app.py` file: + +```python +import azure.durable_functions as df + +@app.orchestration_trigger(context_name="context") +def agent_orchestration_workflow(context: df.DurableOrchestrationContext): + """ + Orchestration function that coordinates multiple agents. + Returns a dictionary with the original response and translations. + """ + input_text = context.get_input() + + # Step 1: Get the main agent's response + main_agent = app.get_agent(context, "MyDurableAgent") + main_response = yield main_agent.run(input_text) + agent_response = main_response.get("response", "") + + # Step 2: Fan out - get the translation agents and run them concurrently + french_agent = app.get_agent(context, "FrenchTranslator") + spanish_agent = app.get_agent(context, "SpanishTranslator") + + parallel_tasks = [ + french_agent.run(agent_response), + spanish_agent.run(agent_response) + ] + + # Step 3: Wait for both translation tasks to complete (fan-in) + translations = yield context.task_all(parallel_tasks) + + # Step 4: Combine results into a dictionary + result = { + "original": agent_response, + "french": translations[0].get("response", ""), + "spanish": translations[1].get("response", "") + } + + return result +``` + +This orchestration demonstrates the proper durable task pattern: +- **Main agent execution**: First calls `MyDurableAgent` to process the user's input. +- **Agent retrieval**: Uses `app.get_agent(context, "AgentName")` to get registered agents by name (agents were registered at startup). +- **Sequential then concurrent**: Main agent runs first, then translation agents run concurrently using `context.task_all`. + +::: zone-end + +## Test the orchestration + +Ensure your local development dependencies from the first tutorial are still running: +- **Azurite** in one terminal window +- **Durable Task Scheduler emulator** in another terminal window + +If you've stopped them, restart them now following the instructions in the [Create and run a durable agent](create-and-run-durable-agent.md#start-local-development-dependencies) tutorial. + +With your local development dependencies running: + +1. Start your Azure Functions app in a new terminal window: + + ```console + func start + ``` + +1. The Durable Functions extension automatically creates built-in HTTP endpoints for managing orchestrations. Start the orchestration using the built-in API: + + # [Bash](#tab/bash) + + ```bash + curl -X POST http://localhost:7071/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow \ + -H "Content-Type: application/json" \ + -d '"\"What are three popular programming languages?\""' + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + $body = '"What are three popular programming languages?"' + Invoke-RestMethod -Method Post -Uri "http://localhost:7071/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow" ` + -ContentType "application/json" ` + -Body $body + ``` + + --- + +1. The response includes URLs for managing the orchestration instance: + + ```json + { + "id": "abc123def456", + "statusQueryGetUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456", + "sendEventPostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456/raiseEvent/{eventName}", + "terminatePostUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456/terminate", + "purgeHistoryDeleteUri": "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456" + } + ``` + +1. Query the orchestration status using the `statusQueryGetUri` (replace `abc123def456` with your actual instance ID): + + # [Bash](#tab/bash) + + ```bash + curl http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456 + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + Invoke-RestMethod -Uri "http://localhost:7071/runtime/webhooks/durabletask/instances/abc123def456" + ``` + + --- + +1. Initially, the orchestration will be running: + + ```json + { + "name": "agent_orchestration_workflow", + "instanceId": "abc123def456", + "runtimeStatus": "Running", + "input": "What are three popular programming languages?", + "createdTime": "2025-11-07T10:00:00Z", + "lastUpdatedTime": "2025-11-07T10:00:05Z" + } + ``` + +1. Poll the status endpoint until `runtimeStatus` is `Completed`. When complete, you'll see the orchestration output with the main agent's response and its translations: + + ```json + { + "name": "agent_orchestration_workflow", + "instanceId": "abc123def456", + "runtimeStatus": "Completed", + "output": { + "original": "Three popular programming languages are Python, JavaScript, and Java. Python is known for its simplicity...", + "french": "Trois langages de programmation populaires sont Python, JavaScript et Java. Python est connu pour sa simplicité...", + "spanish": "Tres lenguajes de programación populares son Python, JavaScript y Java. Python es conocido por su simplicidad..." + } + } + ``` + +Note that the `original` field contains the response from `MyDurableAgent`, not the original user input. This demonstrates how the orchestration flows from the main agent to the translation agents. + +## Monitor the orchestration in the dashboard + +The Durable Task Scheduler dashboard provides visibility into your orchestration: + +1. Open `http://localhost:8082` in your browser. + +1. Select the "default" task hub. + +1. Select the "Orchestrations" tab. + +1. Find your orchestration instance in the list. + +1. Select the instance to see: + - The orchestration timeline + - Main agent execution followed by concurrent translation agents + - Each agent execution (MyDurableAgent, then French and Spanish translators) + - Fan-out and fan-in patterns visualized + - Timing and duration for each step + +## Understanding the benefits + +This orchestration pattern provides several advantages: + +### Concurrent processing + +The translation agents run in parallel, significantly reducing total response time compared to sequential execution. The main agent runs first to generate a response, then both translations happen concurrently. + +- **.NET**: Uses `Task.WhenAll` to await multiple agent tasks simultaneously. +- **Python**: Uses `context.task_all` to execute multiple agent runs concurrently. + +### Durability and reliability + +The orchestration state is persisted by the Durable Task Scheduler. If an agent execution fails or times out, the orchestration can retry that specific step without restarting the entire workflow. + +### Scalability + +The Azure Functions Flex Consumption plan can scale out to hundreds of instances to handle concurrent translations across many orchestration instances. + +## Deploy to Azure + +Now that you've tested the orchestration locally, deploy the updated application to Azure. + +1. Deploy the updated application using Azure Developer CLI: + + ```console + azd deploy + ``` + + This deploys your updated code with the new orchestration function and additional agents to the Azure Functions app created in the first tutorial. + +1. Wait for the deployment to complete. + +## Test the deployed orchestration + +After deployment, test your orchestration running in Azure. + +1. Get the system key for the durable extension: + + # [Bash](#tab/bash) + + ```bash + SYSTEM_KEY=$(az functionapp keys list --name $(azd env get-value AZURE_FUNCTION_NAME) --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) --query "systemKeys.durabletask_extension" -o tsv) + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + $functionName = azd env get-value AZURE_FUNCTION_NAME + $resourceGroup = azd env get-value AZURE_RESOURCE_GROUP + $SYSTEM_KEY = (az functionapp keys list --name $functionName --resource-group $resourceGroup --query "systemKeys.durabletask_extension" -o tsv) + ``` + + --- + +1. Start the orchestration using the built-in API: + + # [Bash](#tab/bash) + + ```bash + curl -X POST "https://$(azd env get-value AZURE_FUNCTION_NAME).azurewebsites.net/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow?code=$SYSTEM_KEY" \ + -H "Content-Type: application/json" \ + -d '"\"What are three popular programming languages?\""' + ``` + + # [PowerShell](#tab/powershell) + + ```powershell + $functionName = azd env get-value AZURE_FUNCTION_NAME + $body = '"What are three popular programming languages?"' + Invoke-RestMethod -Method Post -Uri "https://$functionName.azurewebsites.net/runtime/webhooks/durabletask/orchestrators/agent_orchestration_workflow?code=$SYSTEM_KEY" ` + -ContentType "application/json" ` + -Body $body + ``` + + --- + +1. Use the `statusQueryGetUri` from the response to poll for completion and view the results with translations. + +## Next steps + +Now that you understand durable agent orchestration, you can explore more advanced patterns: + +- **Sequential orchestrations** - Chain agents where each depends on the previous output. +- **Conditional branching** - Route to different agents based on content. +- **Human-in-the-loop** - Pause orchestration for human approval. +- **External events** - Trigger orchestration steps from external systems. + +Additional resources: + +- [Durable Task Scheduler Overview](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Durable Functions patterns and concepts](/azure/azure-functions/durable/durable-functions-overview?tabs=in-process%2Cnodejs-v3%2Cv1-model&pivots=csharp) diff --git a/agent-framework/tutorials/agents/run-agent.md b/agent-framework/tutorials/agents/run-agent.md index f1c8127b..bef4ca87 100644 --- a/agent-framework/tutorials/agents/run-agent.md +++ b/agent-framework/tutorials/agents/run-agent.md @@ -186,7 +186,7 @@ Before you begin, ensure you have the following prerequisites: To use Microsoft Agent Framework with Azure OpenAI, you need to install the following Python packages: ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Create the agent diff --git a/agent-framework/tutorials/plugins/TOC.yml b/agent-framework/tutorials/plugins/TOC.yml new file mode 100644 index 00000000..b8b000c8 --- /dev/null +++ b/agent-framework/tutorials/plugins/TOC.yml @@ -0,0 +1,2 @@ +- name: Use Microsoft Purview SDK with Agent Framework + href: use-purview-with-agent-framework-sdk.md \ No newline at end of file diff --git a/agent-framework/tutorials/plugins/use-purview-with-agent-framework-sdk.md b/agent-framework/tutorials/plugins/use-purview-with-agent-framework-sdk.md new file mode 100644 index 00000000..e49aa36c --- /dev/null +++ b/agent-framework/tutorials/plugins/use-purview-with-agent-framework-sdk.md @@ -0,0 +1,136 @@ +--- +title: Use Microsoft Purview SDK with Agent Framework +description: Learn how to integrate Microsoft Purview SDK for data security and governance in your Agent Framework project +zone_pivot_groups: programming-languages +author: reezaali149 +ms.topic: article +ms.author: v-reezaali +ms.date: 10/28/2025 +ms.service: purview +--- + +# Use Microsoft Purview SDK with Agent Framework + +Microsoft Purview provides enterprise-grade data security, compliance, and governance capabilities for AI applications. By integrating Purview APIs within the Agent Framework SDK, developers can build intelligent agents that are secure by design, while ensuring sensitive data in prompts and responses are protected and compliant with organizational policies. + +## Why integrate Purview with Agent Framework? + +- **Prevent sensitive data leaks**: Inline blocking of sensitive content based on Data Loss Prevention (DLP) policies. +- **Enable governance**: Log AI interactions in Purview for Audit, Communication Compliance, Insider Risk Management, eDiscovery, and Data Lifecycle Management. +- **Accelerate adoption**: Enterprise customers require compliance for AI apps. Purview integration unblocks deployment. + +## Prerequisites + +Before you begin, ensure you have: + +- Microsoft Azure subscription with Microsoft Purview configured. +- Microsoft 365 subscription with an E5 license and pay-as-you-go billing setup. + - For testing, you can use a Microsoft 365 Developer Program tenant. For more information, see [Join the Microsoft 365 Developer Program](https://developer.microsoft.com/en-us/microsoft-365/dev-program). +- Agent Framework SDK: To install the Agent Framework SDK: + - Python: Run `pip install agent-framework --pre`. + - .NET: Install from NuGet. + +## How to integrate Microsoft Purview into your agent + +In your agent's workflow middleware pipeline, you can add Microsoft Purview policy middleware to intercept prompts and responses to determine if they meet the policies set up in Microsoft Purview. The Agent Framework SDK is capable of intercepting agent-to-agent or end-user chat client prompts and responses. + +The following code sample demonstrates how to add the Microsoft Purview policy middleware to your agent code. If you're new to Agent Framework, see [Create and run an agent with Agent Framework](/agent-framework/tutorials/agents/run-agent?pivots=programming-language-python). + +::: zone pivot="programming-language-csharp" + +```csharp + +using Azure.AI.OpenAI; +using Azure.Core; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Purview; +using Microsoft.Extensions.AI; +using OpenAI; + +string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; +string purviewClientAppId = Environment.GetEnvironmentVariable("PURVIEW_CLIENT_APP_ID") ?? throw new InvalidOperationException("PURVIEW_CLIENT_APP_ID is not set."); + +TokenCredential browserCredential = new InteractiveBrowserCredential( + new InteractiveBrowserCredentialOptions + { + ClientId = purviewClientAppId + }); + +AIAgent agent = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent("You are a secure assistant.") + .AsBuilder() + .WithPurview(browserCredential, new PurviewSettings("My Secure Agent")) + .Build(); + +AgentRunResponse response = await agent.RunAsync("Summarize zero trust in one sentence.").ConfigureAwait(false); +Console.WriteLine(response); + +``` + +::: zone-end +::: zone pivot="programming-language-python" + +```python +import asyncio +import os +from agent_framework import ChatAgent, ChatMessage, Role +from agent_framework.azure import AzureOpenAIChatClient +from agent_framework.microsoft import PurviewPolicyMiddleware, PurviewSettings +from azure.identity import AzureCliCredential, InteractiveBrowserCredential + +# Set default environment variables if not already set +os.environ.setdefault("AZURE_OPENAI_ENDPOINT", "") +os.environ.setdefault("AZURE_OPENAI_CHAT_DEPLOYMENT_NAME", "") + +async def main(): + chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) + purview_middleware = PurviewPolicyMiddleware( + credential=InteractiveBrowserCredential( + client_id="", + ), + settings=PurviewSettings(app_name="My Secure Agent") + ) + agent = ChatAgent( + chat_client=chat_client, + instructions="You are a secure assistant.", + middleware=[purview_middleware] + ) + response = await agent.run(ChatMessage(role=Role.USER, text="Summarize zero trust in one sentence.")) + print(response) + + if __name__ == "__main__": + asyncio.run(main()) +``` + +::: zone-end + +--- + +## Next steps + +Now that you added the above code to your agent, perform the following steps to test the integration of Microsoft Purview into your code: + +1. **Entra registration**: Register your agent and add the required Microsoft Graph permissions ([ProtectionScopes.Compute.All](/graph/api/userprotectionscopecontainer-compute), [ContentActivity.Write](/graph/api/activitiescontainer-post-contentactivities), [Content.Process.All](/graph/api/userdatasecurityandgovernance-processcontent)) to the Service Principal. For more information, see [Register an application in Microsoft Entra ID](/entra/identity-platform/quickstart-register-app) and [dataSecurityAndGovernance resource type](/graph/api/resources/datasecurityandgovernance). You'll need the Microsoft Entra app ID in the next step. +1. **Purview policies**: Configure Purview policies using the Microsoft Entra app ID to enable agent communications data to flow into Purview. For more information, see [Configure Microsoft Purview](/purview/developer/configurepurview). + +## Resources + +::: zone pivot="programming-language-csharp" + +- Nuget: [Microsoft.Agents.AI.Purview](https://www.nuget.org/packages/Microsoft.Agents.AI.Purview/) +- Github: [Microsoft.Agents.AI.Purview](https://github.com/microsoft/agent-framework/tree/main/dotnet/src/Microsoft.Agents.AI.Purview) +- Sample: [AgentWithPurview](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/Purview/AgentWithPurview) + +::: zone-end +::: zone pivot="programming-language-python" + +- [PyPI Package: Microsoft Agent Framework - Purview Integration (Python)](https://pypi.org/project/agent-framework-purview/). +- [GitHub: Microsoft Agent Framework – Purview Integration (Python) source code](https://github.com/microsoft/agent-framework/tree/main/python/packages/purview). +- [Code Sample: Purview Policy Enforcement Sample (Python)](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/purview_agent). + +::: zone-end diff --git a/agent-framework/tutorials/quick-start.md b/agent-framework/tutorials/quick-start.md index 67df7eaa..b3a0d393 100644 --- a/agent-framework/tutorials/quick-start.md +++ b/agent-framework/tutorials/quick-start.md @@ -144,9 +144,19 @@ Before you begin, ensure you have the following: - [Python 3.10 or later](https://www.python.org/downloads/) - An [Azure AI](/azure/ai-foundry/) project with a deployed model (for example, `gpt-4o-mini`) - [Azure CLI](/cli/azure/install-azure-cli) installed and authenticated (`az login`) +- Install the Agent Framework Package: + +```bash +pip install -U agent-framework --pre +``` > [!NOTE] -> This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure AI project. For more information, see the [Azure CLI documentation](/cli/azure/authenticate-azure-cli-interactively). +> Installing `agent-framework` will install `agent-framework-core` and all other official packages. If you want to install only the Azure AI package, you can run: `pip install agent-framework-azure-ai --pre` +> All of the official packages, including `agent-framework-azure-ai` have a dependency on `agent-framework-core`, so in most cases, you wouldn't have to specify that. +> The full list of official packages can be found in the [Agent Framework GitHub repository](https://github.com/microsoft/agent-framework/blob/main/python/pyproject.toml#L80). + +> [!NOTE] +> This sample uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure AI project. For more information, see the [Azure CLI documentation](/cli/azure/authenticate-azure-cli-interactively). ## Running a Basic Agent Sample @@ -156,20 +166,17 @@ Make sure to set the following environment variables: - `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint - `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment - ### Sample Code ```python import asyncio -from agent_framework import ChatAgent -from agent_framework.azure import AzureAIAgentClient +from agent_framework.azure import AzureAIClient from azure.identity.aio import AzureCliCredential async def main(): async with ( AzureCliCredential() as credential, - ChatAgent( - chat_client=AzureAIAgentClient(async_credential=credential), + AzureAIClient(async_credential=credential).create_agent( instructions="You are good at telling jokes." ) as agent, ): @@ -182,7 +189,7 @@ if __name__ == "__main__": ## More Examples -For more detailed examples and advanced scenarios, see the [Azure AI Agent Examples](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/azure_ai/README.md). +For more detailed examples and advanced scenarios, see the [Azure AI Examples](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/agents/azure_ai/README.md). ::: zone-end diff --git a/agent-framework/tutorials/workflows/TOC.yml b/agent-framework/tutorials/workflows/TOC.yml index 62b94792..f19fbef8 100644 --- a/agent-framework/tutorials/workflows/TOC.yml +++ b/agent-framework/tutorials/workflows/TOC.yml @@ -10,5 +10,5 @@ href: requests-and-responses.md - name: Checkpointing and resuming workflows href: checkpointing-and-resuming.md -- name: Visualizing workflows - href: visualization.md \ No newline at end of file +- name: Register factories to workflow builder + href: workflow-builder-with-factories.md \ No newline at end of file diff --git a/agent-framework/tutorials/workflows/agents-in-workflows.md b/agent-framework/tutorials/workflows/agents-in-workflows.md index b4169c73..6250c634 100644 --- a/agent-framework/tutorials/workflows/agents-in-workflows.md +++ b/agent-framework/tutorials/workflows/agents-in-workflows.md @@ -1,6 +1,6 @@ --- title: Agents in Workflows -description: Learn how to integrate agents into workflows using Agent Framework. +description: Learn how to integrate agents into workflows. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -27,6 +27,12 @@ You'll create a workflow that: - Streams real-time updates as agents process requests - Demonstrates proper resource cleanup for Azure Foundry agents +### Concepts Covered + +- [Agents in Workflows](../../user-guide/workflows/using-agents.md) +- [Direct Edges](../../user-guide/workflows/core-concepts/edges.md#direct-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) + ## Prerequisites - [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download) @@ -68,18 +74,7 @@ public static class Program var persistentAgentsClient = new PersistentAgentsClient(endpoint, new AzureCliCredential()); ``` -## Step 3: Create Specialized Azure Foundry Agents - -Create three translation agents using the helper method: - -```csharp - // Create agents - AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, model); - AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, model); - AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model); -``` - -## Step 4: Create Agent Factory Method +## Step 3: Create Agent Factory Method Implement a helper method to create Azure Foundry agents with specific instructions: @@ -106,6 +101,17 @@ Implement a helper method to create Azure Foundry agents with specific instructi } ``` +## Step 4: Create Specialized Azure Foundry Agents + +Create three translation agents using the helper method: + +```csharp + // Create agents + AIAgent frenchAgent = await GetTranslationAgentAsync("French", persistentAgentsClient, model); + AIAgent spanishAgent = await GetTranslationAgentAsync("Spanish", persistentAgentsClient, model); + AIAgent englishAgent = await GetTranslationAgentAsync("English", persistentAgentsClient, model); +``` + ## Step 5: Build the Workflow Connect the agents in a sequential workflow using the WorkflowBuilder: @@ -187,10 +193,16 @@ You'll create a workflow that: - Streams real-time updates as agents process requests - Demonstrates proper async context management for Azure AI clients +### Concepts Covered + +- [Agents in Workflows](../../user-guide/workflows/using-agents.md) +- [Direct Edges](../../user-guide/workflows/core-concepts/edges.md#direct-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) + ## Prerequisites - Python 3.10 or later -- Agent Framework installed: `pip install agent-framework-azure-ai` +- Agent Framework installed: `pip install agent-framework-azure-ai --pre` - Azure AI Agent Service configured with proper environment variables - Azure CLI authentication: `az login` diff --git a/agent-framework/tutorials/workflows/checkpointing-and-resuming.md b/agent-framework/tutorials/workflows/checkpointing-and-resuming.md index 07c79871..34d9a3b7 100644 --- a/agent-framework/tutorials/workflows/checkpointing-and-resuming.md +++ b/agent-framework/tutorials/workflows/checkpointing-and-resuming.md @@ -1,6 +1,6 @@ --- title: Checkpointing and Resuming Workflows -description: Learn how to implement checkpointing and resuming in workflows using Agent Framework. +description: Learn how to implement checkpointing and resuming in workflows. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -13,6 +13,10 @@ ms.service: agent-framework Checkpointing allows workflows to save their state at specific points and resume execution later, even after process restarts. This is crucial for long-running workflows, error recovery, and human-in-the-loop scenarios. +### Concepts Covered + +- [Checkpoints](../../user-guide/workflows/checkpoints.md) + ::: zone pivot="programming-language-csharp" ## Prerequisites @@ -67,14 +71,14 @@ await using Checkpointed checkpointedRun = await InProcessExecutio Executors can persist local state that survives checkpoints using the `Executor` base class: ```csharp -internal sealed class GuessNumberExecutor : Executor +internal sealed class GuessNumberExecutor : Executor("Guess") { private const string StateKey = "GuessNumberExecutor.State"; public int LowerBound { get; private set; } public int UpperBound { get; private set; } - public GuessNumberExecutor() : base("GuessNumber") + public GuessNumberExecutor() : this() { } @@ -446,37 +450,45 @@ Executors can persist local state that survives checkpoints: ```python from agent_framework import Executor, WorkflowContext, handler -class UpperCaseExecutor(Executor): - @handler - async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None: - result = text.upper() - - # Persist executor-local state for checkpoints - prev = await ctx.get_executor_state() or {} - count = int(prev.get("count", 0)) + 1 - await ctx.set_executor_state({ - "count": count, - "last_input": text, - "last_output": result, - }) - - # Send result to next executor - await ctx.send_message(result) -``` +class WorkerExecutor(Executor): + """Processes numbers to compute their factor pairs and manages executor state for checkpointing.""" -### Shared State + def __init__(self, id: str) -> None: + super().__init__(id=id) + self._composite_number_pairs: dict[int, list[tuple[int, int]]] = {} -Use shared state for data that multiple executors need to access: - -```python -class ProcessorExecutor(Executor): @handler - async def process(self, text: str, ctx: WorkflowContext[str]) -> None: - # Write to shared state for cross-executor visibility - await ctx.set_shared_state("original_input", text) - await ctx.set_shared_state("processed_output", text.upper()) - - await ctx.send_message(text.upper()) + async def compute( + self, + task: ComputeTask, + ctx: WorkflowContext[ComputeTask, dict[int, list[tuple[int, int]]]], + ) -> None: + """Process the next number in the task, computing its factor pairs.""" + next_number = task.remaining_numbers.pop(0) + + print(f"WorkerExecutor: Computing factor pairs for {next_number}") + pairs: list[tuple[int, int]] = [] + for i in range(1, next_number): + if next_number % i == 0: + pairs.append((i, next_number // i)) + self._composite_number_pairs[next_number] = pairs + + if not task.remaining_numbers: + # All numbers processed - output the results + await ctx.yield_output(self._composite_number_pairs) + else: + # More numbers to process - continue with remaining task + await ctx.send_message(task) + + @override + async def on_checkpoint_save(self) -> dict[str, Any]: + """Save the executor's internal state for checkpointing.""" + return {"composite_number_pairs": self._composite_number_pairs} + + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + """Restore the executor's internal state from a checkpoint.""" + self._composite_number_pairs = state.get("composite_number_pairs", {}) ``` ## Working with Checkpoints @@ -496,24 +508,6 @@ workflow_checkpoints = await checkpoint_storage.list_checkpoints(workflow_id="my sorted_checkpoints = sorted(all_checkpoints, key=lambda cp: cp.timestamp) ``` -### Checkpoint Information - -Access checkpoint metadata and state: - -```python -from agent_framework import get_checkpoint_summary - -for checkpoint in checkpoints: - # Get human-readable summary - summary = get_checkpoint_summary(checkpoint) - - print(f"Checkpoint: {summary.checkpoint_id}") - print(f"Iteration: {summary.iteration_count}") - print(f"Status: {summary.status}") - print(f"Messages: {len(checkpoint.messages)}") - print(f"Shared State: {checkpoint.shared_state}") -``` - ## Resuming from Checkpoints ### Streaming Resume @@ -686,4 +680,4 @@ For the complete working implementation, see the [Checkpoint with Resume sample] ## Next Steps > [!div class="nextstepaction"] -> [Learn about Workflow Visualization](visualization.md) +> [Learn about using factories in workflow builders](./workflow-builder-with-factories.md) diff --git a/agent-framework/tutorials/workflows/requests-and-responses.md b/agent-framework/tutorials/workflows/requests-and-responses.md index b644a945..156c17db 100644 --- a/agent-framework/tutorials/workflows/requests-and-responses.md +++ b/agent-framework/tutorials/workflows/requests-and-responses.md @@ -1,6 +1,6 @@ --- title: Handle Requests and Responses in Workflows -description: Learn how to handle requests and responses in workflows using Agent Framework. +description: Learn how to handle requests and responses in workflows. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -13,6 +13,10 @@ ms.service: agent-framework This tutorial demonstrates how to handle requests and responses in workflows using Agent Framework Workflows. You'll learn how to create interactive workflows that can pause execution to request input from external sources (like humans or other systems) and then resume once a response is provided. +## Concepts Covered + +- [Requests and Responses](../../user-guide/workflows/requests-and-responses.md) + ::: zone pivot="programming-language-csharp" In .NET, human-in-the-loop workflows use `RequestPort` and external request handling to pause execution and gather user input. This pattern enables interactive workflows where the system can request information from external sources during execution. @@ -68,12 +72,12 @@ Create executors that process user input and provide feedback: /// /// Executor that judges the guess and provides feedback. /// -internal sealed class JudgeExecutor : Executor, IMessageHandler +internal sealed class JudgeExecutor : Executor("Judge") { private readonly int _targetNumber; private int _tries; - public JudgeExecutor(int targetNumber) : base("Judge") + public JudgeExecutor(int targetNumber) : this() { _targetNumber = targetNumber; } @@ -279,9 +283,8 @@ Executors can send requests directly using `ctx.request_info()` and handle respo First, install the required packages: ```bash -pip install agent-framework-core +pip install agent-framework-core --pre pip install azure-identity -pip install pydantic ``` ## Define Request and Response Models diff --git a/agent-framework/tutorials/workflows/simple-concurrent-workflow.md b/agent-framework/tutorials/workflows/simple-concurrent-workflow.md index 787e85b1..01d0fee3 100644 --- a/agent-framework/tutorials/workflows/simple-concurrent-workflow.md +++ b/agent-framework/tutorials/workflows/simple-concurrent-workflow.md @@ -1,6 +1,6 @@ --- title: Create a Simple Concurrent Workflow -description: Learn how to create a simple concurrent workflow using Agent Framework. +description: Learn how to create a simple concurrent workflow. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -24,6 +24,14 @@ You'll create a workflow that: - Collects and combines responses from both agents into a single output - Demonstrates concurrent execution with AI agents using fan-out/fan-in patterns +### Concepts Covered + +- [Executors](../../user-guide/workflows/core-concepts/executors.md) +- [Fan-out Edges](../../user-guide/workflows/core-concepts/edges.md#fan-out-edges) +- [Fan-in Edges](../../user-guide/workflows/core-concepts/edges.md#fan-in-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) +- [Events](../../user-guide/workflows/core-concepts/events.md) + ## Prerequisites - [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download) @@ -101,8 +109,7 @@ The `ConcurrentStartExecutor` implementation: /// /// Executor that starts the concurrent processing by sending messages to the agents. /// -internal sealed class ConcurrentStartExecutor() : - Executor("ConcurrentStartExecutor") +internal sealed class ConcurrentStartExecutor() : Executor("ConcurrentStartExecutor") { /// /// Starts the concurrent processing by sending messages to the agents. @@ -117,7 +124,7 @@ internal sealed class ConcurrentStartExecutor() : // Broadcast the message to all connected agents. Receiving agents will queue // the message but will not start processing until they receive a turn token. await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken); - + // Broadcast the turn token to kick off the agents. await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken); } @@ -139,7 +146,7 @@ The `ConcurrentAggregationExecutor` implementation: /// Executor that aggregates the results from the concurrent agents. /// internal sealed class ConcurrentAggregationExecutor() : - Executor("ConcurrentAggregationExecutor") + Executor>("ConcurrentAggregationExecutor") { private readonly List _messages = []; @@ -151,9 +158,9 @@ internal sealed class ConcurrentAggregationExecutor() : /// The to monitor for cancellation requests. /// The default is . /// A task representing the asynchronous operation - public override async ValueTask HandleAsync(ChatMessage message, IWorkflowContext context, CancellationToken cancellationToken = default) + public override async ValueTask HandleAsync(List message, IWorkflowContext context, CancellationToken cancellationToken = default) { - this._messages.Add(message); + this._messages.AddRange(message); if (this._messages.Count == 2) { @@ -231,10 +238,18 @@ You'll create a workflow that: - Aggregates the different result types (float and int) into a final output - Demonstrates how the framework handles different result types from concurrent executors +### Concepts Covered + +- [Executors](../../user-guide/workflows/core-concepts/executors.md) +- [Fan-out Edges](../../user-guide/workflows/core-concepts/edges.md#fan-out-edges) +- [Fan-in Edges](../../user-guide/workflows/core-concepts/edges.md#fan-in-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) +- [Events](../../user-guide/workflows/core-concepts/events.md) + ## Prerequisites - Python 3.10 or later -- Agent Framework Core installed: `pip install agent-framework-core` +- Agent Framework Core installed: `pip install agent-framework-core --pre` ## Step 1: Import Required Dependencies diff --git a/agent-framework/tutorials/workflows/simple-sequential-workflow.md b/agent-framework/tutorials/workflows/simple-sequential-workflow.md index f96a0e2a..bf42fe75 100644 --- a/agent-framework/tutorials/workflows/simple-sequential-workflow.md +++ b/agent-framework/tutorials/workflows/simple-sequential-workflow.md @@ -1,6 +1,6 @@ --- title: Create a Simple Sequential Workflow -description: Learn how to create a simple sequential workflow using Agent Framework. +description: Learn how to create a simple sequential workflow. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -26,11 +26,19 @@ In this tutorial, you'll create a workflow with two executors: The workflow demonstrates core concepts like: -- Creating custom executors that implement `IMessageHandler` +- Creating a custom executor with one handler +- Creating a custom executor from a function - Using `WorkflowBuilder` to connect executors with edges - Processing data through sequential steps - Observing workflow execution through events +### Concepts Covered + +- [Executors](../../user-guide/workflows/core-concepts/executors.md) +- [Direct Edges](../../user-guide/workflows/core-concepts/edges.md#direct-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) +- [Events](../../user-guide/workflows/core-concepts/events.md) + ## Prerequisites - [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download) @@ -62,22 +70,14 @@ using Microsoft.Agents.AI.Workflows; /// /// First executor: converts input text to uppercase. /// -internal sealed class UppercaseExecutor() : ReflectingExecutor("UppercaseExecutor"), - IMessageHandler -{ - public ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken = default) - { - // Convert input to uppercase and pass to next executor - return ValueTask.FromResult(input.ToUpper()); - } -} +Func uppercaseFunc = s => s.ToUpperInvariant(); +var uppercase = uppercaseFunc.BindExecutor("UppercaseExecutor"); ``` **Key Points:** -- Inherits from `ReflectingExecutor` for basic executor functionality -- Implements `IMessageHandler` - takes string input, produces string output -- The `HandleAsync` method processes the input and returns the result +- Create a function that takes a string and returns the uppercase version +- Use `BindExecutor()` to create an executor from the function ### Step 3: Define the Reverse Text Executor @@ -87,32 +87,28 @@ Define an executor that reverses the text: /// /// Second executor: reverses the input text and completes the workflow. /// -internal sealed class ReverseTextExecutor() : ReflectingExecutor("ReverseTextExecutor"), - IMessageHandler +internal sealed class ReverseTextExecutor() : Executor("ReverseTextExecutor") { - public ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken = default) + public override ValueTask HandleAsync(string input, IWorkflowContext context, CancellationToken cancellationToken = default) { // Reverse the input text return ValueTask.FromResult(new string(input.Reverse().ToArray())); } } + +ReverseTextExecutor reverse = new(); ``` **Key Points:** -- Same pattern as the first executor. -- Reverses the string using LINQ's `Reverse()` method. -- This will be the final executor in the workflow. +- Create a class that inherits from `Executor` +- Implement `HandleAsync()` to process the input and return the output ### Step 4: Build and Connect the Workflow Connect the executors using `WorkflowBuilder`: ```csharp -// Create the executors -UppercaseExecutor uppercase = new(); -ReverseTextExecutor reverse = new(); - // Build the workflow by connecting executors sequentially WorkflowBuilder builder = new(uppercase); builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse); @@ -140,9 +136,6 @@ foreach (WorkflowEvent evt in run.NewEvents) case ExecutorCompletedEvent executorComplete: Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}"); break; - case WorkflowOutputEvent workflowOutput: - Console.WriteLine($"Workflow '{workflowOutput.SourceId}' outputs: {workflowOutput.Data}"); - break; } } ``` @@ -162,7 +155,11 @@ The input "Hello, World!" is first converted to uppercase ("HELLO, WORLD!"), the ### Executor Interface -Executors implement `IMessageHandler`: +Executors from functions: + +- Use `BindExecutor()` to create an executor from a function + +Executors implement `Executor`: - **TInput**: The type of data this executor accepts - **TOutput**: The type of data this executor produces @@ -182,16 +179,6 @@ The `WorkflowBuilder` provides a fluent API for constructing workflows: During execution, you can observe these event types: - `ExecutorCompletedEvent` - When an executor finishes processing -- `WorkflowOutputEvent` - Contains the final workflow result (for streaming execution) - -## Running the .NET Example - -1. Create a new console application -2. Install the `Microsoft.Agents.AI.Workflows` NuGet package -3. Combine all the code snippets from the steps above into your `Program.cs` -4. Run the application - -The workflow will process your input through both executors and display the results. ## Complete .NET Example @@ -222,10 +209,17 @@ The workflow demonstrates core concepts like: - Yielding final output with `ctx.yield_output()` - Streaming events for real-time observability +### Concepts Covered + +- [Executors](../../user-guide/workflows/core-concepts/executors.md) +- [Direct Edges](../../user-guide/workflows/core-concepts/edges.md#direct-edges) +- [Workflow Builder](../../user-guide/workflows/core-concepts/workflows.md) +- [Events](../../user-guide/workflows/core-concepts/events.md) + ## Prerequisites - Python 3.10 or later -- Agent Framework Core Python package installed: `pip install agent-framework-core` +- Agent Framework Core Python package installed: `pip install agent-framework-core --pre` - No external AI services required for this basic example ## Step-by-Step Implementation @@ -244,27 +238,35 @@ from agent_framework import WorkflowBuilder, WorkflowContext, WorkflowOutputEven ### Step 2: Create the First Executor -Create an executor that converts text to uppercase using the `@executor` decorator: +Create an executor that converts text to uppercase by implementing an executor with a handler method: ```python -@executor(id="upper_case_executor") -async def to_upper_case(text: str, ctx: WorkflowContext[str]) -> None: - """Transform the input to uppercase and forward it to the next step.""" - result = text.upper() +class UpperCase(Executor): + def __init__(self, id: str): + super().__init__(id=id) - # Send the intermediate result to the next executor - await ctx.send_message(result) + @handler + async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None: + """Convert the input to uppercase and forward it to the next node. + + Note: The WorkflowContext is parameterized with the type this handler will + emit. Here WorkflowContext[str] means downstream nodes should expect str. + """ + result = text.upper() + + # Send the result to the next executor in the workflow. + await ctx.send_message(result) ``` **Key Points:** - The `@executor` decorator registers this function as a workflow node -- `WorkflowContext[str]` indicates this executor sends a string downstream +- `WorkflowContext[str]` indicates this executor sends a string downstream by specifying the first type parameter - `ctx.send_message()` passes data to the next step ### Step 3: Create the Second Executor -Create an executor that reverses the text and yields the final output: +Create an executor that reverses the text and yields the final output from a method decorated with `@executor`: ```python @executor(id="reverse_text_executor") @@ -278,7 +280,7 @@ async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None: **Key Points:** -- `WorkflowContext[Never, str]` indicates this is a terminal executor +- `WorkflowContext[Never, str]` indicates this is a terminal executor that does not send any messages by specifying `Never` as the first type parameter but produce workflow outputs by specifying `str` as the second parameter - `ctx.yield_output()` provides the final workflow result - The workflow completes when it becomes idle @@ -287,10 +289,12 @@ async def reverse_text(text: str, ctx: WorkflowContext[Never, str]) -> None: Connect the executors using `WorkflowBuilder`: ```python +upper_case = UpperCase(id="upper_case_executor") + workflow = ( WorkflowBuilder() - .add_edge(to_upper_case, reverse_text) - .set_start_executor(to_upper_case) + .add_edge(upper_case, reverse_text) + .set_start_executor(upper_case) .build() ) ``` @@ -355,17 +359,9 @@ The `WorkflowBuilder` provides a fluent API for constructing workflows: - **set_start_executor()**: Defines the workflow entry point - **build()**: Finalizes and returns an immutable workflow object -## Running the Example - -1. Combine all the code snippets from the steps above into a single Python file -2. Save it as `sequential_workflow.py` -3. Run with: `python sequential_workflow.py` - -The workflow will process the input "hello world" through both executors and display the streaming events. - ## Complete Example -For the complete, ready-to-run implementation, see the [sequential_streaming.py sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/control-flow/sequential_streaming.py) in the Agent Framework repository. +For the complete, ready-to-run implementation, see the [sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/_start-here/step1_executors_and_edges.py) in the Agent Framework repository. This sample includes: diff --git a/agent-framework/tutorials/workflows/visualization.md b/agent-framework/tutorials/workflows/visualization.md deleted file mode 100644 index ab886900..00000000 --- a/agent-framework/tutorials/workflows/visualization.md +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: Workflow Visualization -description: Learn how to visualize workflows using Agent Framework. -author: TaoChenOSU -ms.topic: tutorial -ms.author: taochen -ms.date: 09/29/2025 -ms.service: agent-framework ---- - -# Visualizing Workflows - - -## Overview - -The Agent Framework provides powerful visualization capabilities for workflows through the `WorkflowViz` class. This allows you to generate visual diagrams of your workflow structure in multiple formats including Mermaid flowcharts, GraphViz DOT diagrams, and exported image files (SVG, PNG, PDF). - -## Getting Started with WorkflowViz - -### Basic Setup - -```python -from agent_framework import WorkflowBuilder, WorkflowViz - -# Create your workflow -workflow = ( - WorkflowBuilder() - .set_start_executor(start_executor) - .add_edge(start_executor, end_executor) - .build() -) - -# Create visualization -viz = WorkflowViz(workflow) -``` - -### Installation Requirements - -For basic text output (Mermaid and DOT), no additional dependencies are needed. For image export: - -```bash -# Install the viz extra -pip install agent-framework graphviz - -# Install GraphViz binaries (required for image export) -# On Ubuntu/Debian: -sudo apt-get install graphviz - -# On macOS: -brew install graphviz - -# On Windows: Download from https://graphviz.org/download/ -``` - -## Visualization Formats - -### Mermaid Flowcharts - -Generate Mermaid syntax for modern, web-friendly diagrams: - -```python -# Generate Mermaid flowchart -mermaid_content = viz.to_mermaid() -print("Mermaid flowchart:") -print(mermaid_content) - -# Example output: -# flowchart TD -# dispatcher["dispatcher (Start)"]; -# researcher["researcher"]; -# marketer["marketer"]; -# legal["legal"]; -# aggregator["aggregator"]; -# dispatcher --> researcher; -# dispatcher --> marketer; -# dispatcher --> legal; -# researcher --> aggregator; -# marketer --> aggregator; -# legal --> aggregator; -``` - -### GraphViz DOT Format - -Generate DOT format for detailed graph representations: - -```python -# Generate DOT diagram -dot_content = viz.to_digraph() -print("DOT diagram:") -print(dot_content) - -# Example output: -# digraph Workflow { -# rankdir=TD; -# node [shape=box, style=filled, fillcolor=lightblue]; -# "dispatcher" [fillcolor=lightgreen, label="dispatcher\n(Start)"]; -# "researcher" [label="researcher"]; -# "marketer" [label="marketer"]; -# ... -# } -``` - -## Image Export - -### Supported Formats - -Export workflows as high-quality images: - -```python -try: - # Export as SVG (vector format, recommended) - svg_file = viz.export(format="svg") - print(f"SVG exported to: {svg_file}") - - # Export as PNG (raster format) - png_file = viz.export(format="png") - print(f"PNG exported to: {png_file}") - - # Export as PDF (vector format) - pdf_file = viz.export(format="pdf") - print(f"PDF exported to: {pdf_file}") - - # Export raw DOT file - dot_file = viz.export(format="dot") - print(f"DOT file exported to: {dot_file}") - -except ImportError: - print("Install 'viz' extra and GraphViz for image export:") - print("pip install agent-framework[viz]") - print("Also install GraphViz binaries for your platform") -``` - -### Custom Filenames - -Specify custom output filenames: - -```python -# Export with custom filename -svg_path = viz.export(format="svg", filename="my_workflow.svg") -png_path = viz.export(format="png", filename="workflow_diagram.png") - -# Convenience methods -svg_path = viz.save_svg("workflow.svg") -png_path = viz.save_png("workflow.png") -pdf_path = viz.save_pdf("workflow.pdf") -``` - -## Workflow Pattern Visualizations - -### Fan-out/Fan-in Patterns - -Visualizations automatically handle complex routing patterns: - -```python -from agent_framework import ( - WorkflowBuilder, WorkflowViz, AgentExecutor, - AgentExecutorRequest, AgentExecutorResponse -) - -# Create agents -researcher = AgentExecutor(chat_client.create_agent(...), id="researcher") -marketer = AgentExecutor(chat_client.create_agent(...), id="marketer") -legal = AgentExecutor(chat_client.create_agent(...), id="legal") - -# Build fan-out/fan-in workflow -workflow = ( - WorkflowBuilder() - .set_start_executor(dispatcher) - .add_fan_out_edges(dispatcher, [researcher, marketer, legal]) # Fan-out - .add_fan_in_edges([researcher, marketer, legal], aggregator) # Fan-in - .build() -) - -# Visualize -viz = WorkflowViz(workflow) -print(viz.to_mermaid()) -``` - -Fan-in nodes are automatically rendered with special styling: - -- **DOT format**: Ellipse shape with light golden background and "fan-in" label -- **Mermaid format**: Double circle nodes `((fan-in))` for clear identification - -### Conditional Edges - -Conditional routing is visualized with distinct styling: - -```python -def spam_condition(content: str) -> bool: - return "spam" in content.lower() - -workflow = ( - WorkflowBuilder() - .add_edge(classifier, spam_handler, condition=spam_condition) - .add_edge(classifier, normal_processor) # Unconditional edge - .build() -) - -viz = WorkflowViz(workflow) -print(viz.to_digraph()) -``` - -Conditional edges appear as: - -- **DOT format**: Dashed lines with "conditional" labels -- **Mermaid format**: Dotted arrows (`-.->`) with "conditional" labels - -### Sub-workflows - -Nested workflows are visualized as clustered subgraphs: - -```python -from agent_framework import WorkflowExecutor - -# Create sub-workflow -sub_workflow = WorkflowBuilder().add_edge(sub_exec1, sub_exec2).build() -sub_workflow_executor = WorkflowExecutor(sub_workflow, id="sub_workflow") - -# Main workflow containing sub-workflow -main_workflow = ( - WorkflowBuilder() - .add_edge(main_executor, sub_workflow_executor) - .add_edge(sub_workflow_executor, final_executor) - .build() -) - -viz = WorkflowViz(main_workflow) -dot_content = viz.to_digraph() # Shows nested clusters -mermaid_content = viz.to_mermaid() # Shows subgraph structures -``` - -## Complete Example - -For a comprehensive example showing workflow visualization with fan-out/fan-in patterns, custom executors, and multiple export formats, see the [Concurrent with Visualization sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py). - -The sample demonstrates: - -- Expert agent workflow with researcher, marketer, and legal agents -- Custom dispatcher and aggregator executors -- Mermaid and DOT visualization generation -- SVG, PNG, and PDF export capabilities -- Integration with Azure OpenAI agents - -## Visualization Features - -### Node Styling - -- **Start executors**: Green background with "(Start)" label -- **Regular executors**: Blue background with executor ID -- **Fan-in nodes**: Golden background with ellipse shape (DOT) or double circles (Mermaid) - -### Edge Styling - -- **Normal edges**: Solid arrows -- **Conditional edges**: Dashed/dotted arrows with "conditional" labels -- **Fan-out/Fan-in**: Automatic routing through intermediate nodes - -### Layout Options - -- **Top-down layout**: Clear hierarchical flow visualization -- **Subgraph clustering**: Nested workflows shown as grouped clusters -- **Automatic positioning**: GraphViz handles optimal node placement - -## Integration with Development Workflow - -### Documentation Generation - -```python -# Generate documentation diagrams -workflow_viz = WorkflowViz(my_workflow) -doc_diagram = workflow_viz.save_svg("docs/workflow_architecture.svg") -``` - -### Debugging and Analysis - -```python -# Analyze workflow structure -print("Workflow complexity analysis:") -dot_content = viz.to_digraph() -edge_count = dot_content.count(" -> ") -node_count = dot_content.count('[label=') -print(f"Nodes: {node_count}, Edges: {edge_count}") -``` - -### CI/CD Integration - -```python -# Export diagrams for automated documentation -import os -if os.getenv("CI"): - # Export for docs during CI build - viz.save_svg("build/artifacts/workflow.svg") - viz.export(format="dot", filename="build/artifacts/workflow.dot") -``` - -## Best Practices - -1. **Use descriptive executor IDs** - They become node labels in visualizations -2. **Export SVG for documentation** - Vector format scales well in docs -3. **Use Mermaid for web integration** - Copy-paste into Markdown/wiki systems -4. **Leverage fan-in/fan-out visualization** - Clearly shows parallelism patterns -5. **Include visualization in testing** - Verify workflow structure matches expectations - -### Running the Example - -For the complete working implementation with visualization, see the [Concurrent with Visualization sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py). diff --git a/agent-framework/tutorials/workflows/workflow-builder-with-factories.md b/agent-framework/tutorials/workflows/workflow-builder-with-factories.md new file mode 100644 index 00000000..610a887c --- /dev/null +++ b/agent-framework/tutorials/workflows/workflow-builder-with-factories.md @@ -0,0 +1,141 @@ +--- +title: Register Factories to Workflow Builder +description: Learn how to register factories to the workflow builder. +zone_pivot_groups: programming-languages +author: TaoChenOSU +ms.topic: tutorial +ms.author: taochen +ms.date: 09/29/2025 +ms.service: agent-framework +--- + +# Register Factories to Workflow Builder + +Up to this point, we've been creating executor instances and passing them directly to the `WorkflowBuilder`. This approach works well for simple scenarios where you only need a single workflow instance. However, in more complex cases you may want to create multiple, isolated instances of the same workflow. To support this, each workflow instance must receive its own set of executor instances. Reusing the same executors would cause their internal state to be shared across workflows, resulting in unintended side effects. To avoid this, you can register executor factories with the `WorkflowBuilder`, ensuring that new executor instances are created for each workflow instance. + +## Registering Factories to Workflow Builder + +::: zone pivot="programming-language-csharp" + +Coming soon... + +::: zone-end + +::: zone pivot="programming-language-python" + +To register an executor factory to the `WorkflowBuilder`, you can use the `register_executor` method. This method takes two parameters: the factory function that creates instances of the executor (of type `Executor` or derivation of `Executor`) and the name of the factory to be used in the workflow configuration. + +```python +class UpperCase(Executor): + def __init__(self, id: str): + super().__init__(id=id) + + @handler + async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None: + """Convert the input to uppercase and forward it to the next node.""" + result = text.upper() + + # Send the result to the next executor in the workflow. + await ctx.send_message(result) + +class Accumulate(Executor): + def __init__(self, id: str): + super().__init__(id=id) + # Executor internal state that should not be shared among different workflow instances. + self._text_length = 0 + + @handler + async def accumulate(self, text: str, ctx: WorkflowContext) -> None: + """Accumulate the length of the input text and log it.""" + self._text_length += len(text) + print(f"Accumulated text length: {self._text_length}") + +@executor(id="reverse_text_executor") +async def reverse_text(text: str, ctx: WorkflowContext[str]) -> None: + """Reverse the input string and send it downstream.""" + result = text[::-1] + + # Send the result to the next executor in the workflow. + await ctx.yield_output(result) + +workflow_builder = ( + WorkflowBuilder() + .register_executor( + factory_func=lambda: UpperCase(id="UpperCaseExecutor"), + name="UpperCase", + ) + .register_executor( + factory_func=lambda: Accumulate(id="AccumulateExecutor"), + name="Accumulate", + ) + .register_executor( + factory_func=lambda: reverse_text, + name="ReverseText", + ) + # Use the factory name to configure the workflow + .add_fan_out_edges("UpperCase", ["Accumulate", "ReverseText"]) + .set_start_executor("UpperCase") +) +``` + +Build a workflow using the builder + +```python +# Build the workflow using the builder +workflow_a = workflow_builder.build() +await workflow_a.run("hello world") +await workflow_a.run("hello world") +``` + +Expected output: + +```plaintext +Accumulated text length: 22 +``` + +Now let's create another workflow instance and run it. The `Accumulate` executor should have its own internal state and not share the state with the first workflow instance. + +```python +# Build another workflow using the builder +# This workflow will have its own set of executors, including a new instance of the Accumulate executor. +workflow_b = workflow_builder.build() +await workflow_b.run("hello world") +``` + +Expected output: + +```plaintext +Accumulated text length: 11 +``` + +To register an agent factory to the `WorkflowBuilder`, you can use the `register_agent` method. This method takes two parameters: the factory function that creates instances of the agent (of types that implement `AgentProtocol`) and the name of the factory to be used in the workflow configuration. + +```python +def create_agent() -> ChatAgent: + """Factory function to create a Writer agent.""" + return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( + instructions=("You are a helpful assistant.",), + name="assistant", + ) + +workflow_builder = ( + WorkflowBuilder() + .register_agent( + factory_func=create_agent, + name="Assistant", + ) + # Register other executors or agents as needed and configure the workflow + ... +) + +# Build the workflow using the builder +workflow = workflow_builder.build() +``` + +Each time a new workflow instance is created, the agent in the workflow will be a new instance created by the factory function, and will get a new thread instance. + +::: zone-end + +## Workflow State Isolation + +To learn more about workflow state isolation, refer to the [Workflow State Isolation](../../user-guide/workflows/state-isolation.md) documentation. diff --git a/agent-framework/tutorials/workflows/workflow-with-branching-logic.md b/agent-framework/tutorials/workflows/workflow-with-branching-logic.md index 9153573e..09c8eae0 100644 --- a/agent-framework/tutorials/workflows/workflow-with-branching-logic.md +++ b/agent-framework/tutorials/workflows/workflow-with-branching-logic.md @@ -1,6 +1,6 @@ --- title: Create a Workflow with Branching Logic -description: Learn how to create a workflow with branching logic using Agent Framework. +description: Learn how to create a workflow with branching logic. zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial @@ -29,6 +29,10 @@ You'll create an email processing workflow that demonstrates conditional routing - A spam handler that marks suspicious emails. - Shared state management to persist email data between workflow steps. +### Concepts Covered + +- [Conditional Edges](../../user-guide/workflows/core-concepts/edges.md#conditional-edges) + ### Prerequisites - [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download). @@ -375,10 +379,14 @@ You'll create an email processing workflow that demonstrates conditional routing - A legitimate email handler that drafts professional responses - A spam handler that marks suspicious emails +### Concepts Covered + +- [Conditional Edges](../../user-guide/workflows/core-concepts/edges.md#conditional-edges) + ### Prerequisites - Python 3.10 or later -- Agent Framework installed: `pip install agent-framework-core` +- Agent Framework installed: `pip install agent-framework-core --pre` - Azure OpenAI service configured with proper environment variables - Azure CLI authentication: `az login` @@ -630,6 +638,10 @@ You'll extend the email processing workflow to handle three decision paths: The key improvement is using the `SwitchBuilder` pattern instead of multiple individual conditional edges, making the workflow easier to understand and maintain as decision complexity grows. +### Concepts Covered + +- [Switch-Case Edges](../../user-guide/workflows/core-concepts/edges.md#switch-case-edges) + ### Data Models for Switch-Case Update your data models to support the three-way classification: @@ -995,6 +1007,10 @@ You'll extend the email processing workflow to handle three decision paths: The key improvement is using a single switch-case edge group instead of multiple individual conditional edges, making the workflow easier to understand and maintain as decision complexity grows. +### Concepts Covered + +- [Switch-Case Edges](../../user-guide/workflows/core-concepts/edges.md#switch-case-edges) + ### Enhanced Data Models Update your data models to support the three-way classification: @@ -1270,6 +1286,10 @@ Building on the switch-case example, you'll create an enhanced email processing This pattern enables parallel processing pipelines that adapt to content characteristics. +### Concepts Covered + +- [Fan-out Edges](../../user-guide/workflows/core-concepts/edges.md#fan-out-edges) + ### Data Models for Multi-Selection Extend the data models to support email length analysis and summarization: @@ -1330,16 +1350,16 @@ public static class EmailProcessingConstants } ``` -### Partitioner Function: The Heart of Multi-Selection +### Target Assigner Function: The Heart of Multi-Selection -The partitioner function determines which executors should receive each message: +The target assigner function determines which executors should receive each message: ```csharp /// -/// Creates a partitioner for routing messages based on the analysis result. +/// Creates a target assigner for routing messages based on the analysis result. /// /// A function that takes an analysis result and returns the target partitions. -private static Func> GetPartitioner() +private static Func> GetTargetAssigner() { return (analysisResult, targetCount) => { @@ -1372,7 +1392,7 @@ private static Func> GetPartitioner() } ``` -### Key Features of the Partitioner Function +### Key Features of the Target Assigner Function 1. **Dynamic Target Selection**: Returns a list of executor indices to activate 2. **Content-Aware Routing**: Makes decisions based on message properties like email length @@ -1630,7 +1650,7 @@ public static class Program emailSummaryExecutor, // Index 2: Summarizer (conditionally for long NotSpam) handleUncertainExecutor, // Index 3: Uncertain handler ], - partitioner: GetPartitioner() + targetSelector: GetTargetAssigner() ) // Email assistant branch .AddEdge(emailAssistantExecutor, sendEmailExecutor) @@ -1689,7 +1709,7 @@ builder.AddSwitch(spamDetectionExecutor, switchBuilder => builder.AddFanOutEdge( emailAnalysisExecutor, targets: [handleSpamExecutor, emailAssistantExecutor, emailSummaryExecutor, handleUncertainExecutor], - partitioner: GetPartitioner() // Returns list of target indices + targetSelector: GetTargetAssigner() // Returns list of target indices ) ``` @@ -1747,6 +1767,10 @@ Building on the switch-case example, you'll create an enhanced email processing This pattern enables parallel processing pipelines that adapt to content characteristics. +### Concepts Covered + +- [Fan-Out Edges](../../user-guide/workflows/core-concepts/edges.md#fan-out-edges) + ### Enhanced Data Models for Multi-Selection Extend the data models to support email length analysis and summarization: diff --git a/agent-framework/user-guide/agents/TOC.yml b/agent-framework/user-guide/agents/TOC.yml index 8cfc9fd7..73a61862 100644 --- a/agent-framework/user-guide/agents/TOC.yml +++ b/agent-framework/user-guide/agents/TOC.yml @@ -10,7 +10,7 @@ href: agent-middleware.md - name: Agent Retrieval Augmented Generation (RAG) href: agent-rag.md -- name: Agent Memory +- name: Agent Chat History and Memory href: agent-memory.md - name: Agent Observability href: agent-observability.md diff --git a/agent-framework/user-guide/agents/agent-memory.md b/agent-framework/user-guide/agents/agent-memory.md index 259e2214..7c7b3bee 100644 --- a/agent-framework/user-guide/agents/agent-memory.md +++ b/agent-framework/user-guide/agents/agent-memory.md @@ -1,6 +1,6 @@ --- -title: Agent Memory -description: Learn how to use memory with Agent Framework +title: Agent Chat History and Memory +description: Learn how to use chat history and memory with Agent Framework zone_pivot_groups: programming-languages author: markwallace ms.topic: reference @@ -9,15 +9,13 @@ ms.date: 09/24/2025 ms.service: agent-framework --- -# Agent Memory +# Agent Chat History and Memory -Agent memory is a crucial capability that allows agents to maintain context across conversations, remember user preferences, and provide personalized experiences. The Agent Framework provides multiple memory mechanisms to suit different use cases, from simple in-memory storage to persistent databases and specialized memory services. +Agent chat history and memory are crucial capabilities that allow agents to maintain context across conversations, remember user preferences, and provide personalized experiences. The Agent Framework provides multiple features to suit different use cases, from simple in-memory chat message storage to persistent databases and specialized memory services. ::: zone pivot="programming-language-csharp" -The Agent Framework supports several types of memory to accommodate different use cases, including managing chat history as part of short term memory and providing extension points for extracting, storing and injecting long term memories into agents. - -## Chat History (short term memory) +## Chat History Various chat history storage options are supported by the Agent Framework. The available options vary by agent type and the underlying service(s) used to build the agent. diff --git a/agent-framework/user-guide/agents/agent-middleware.md b/agent-framework/user-guide/agents/agent-middleware.md index 412e193c..2e8463b3 100644 --- a/agent-framework/user-guide/agents/agent-middleware.md +++ b/agent-framework/user-guide/agents/agent-middleware.md @@ -29,11 +29,17 @@ Agent run and function calling middleware types can be registered on an agent, b ```csharp var middlewareEnabledAgent = originalAgent .AsBuilder() - .Use(CustomAgentRunMiddleware) + .Use(runFunc: CustomAgentRunMiddleware, runStreamingFunc: CustomAgentRunStreamingMiddleware) .Use(CustomFunctionCallingMiddleware) .Build(); ``` +> [!IMPORTANT] +> Ideally both `runFunc` and `runStreamingFunc` should be provided, when providing just the non-streaming middleware, the agent will use it for both streaming and non-streaming invocations and this will block the streaming to run in non-streaming mode to suffice the middleware expectations. + +> [!NOTE] +> There's an additional overload `Use(sharedFunc: ...)` that allows you to provide the same middleware for non-streaming and streaming without blocking the streaming, however, the shared middleware won't be able intercept or override the output, make this the best option only for scenarios where you only need to inspect/modify the input before it reaches the agent. + `IChatClient` middleware can be registered on an `IChatClient` before it is used with a `ChatClientAgent`, by using the chat client builder pattern. ```csharp @@ -80,6 +86,30 @@ async Task CustomAgentRunMiddleware( } ``` +## Agent Run Streaming Middleware + +Here is an example of agent run streaming middleware, that can inspect and/or modify the input and output from the agent streaming run. + +```csharp +async IAsyncEnumerable CustomAgentRunStreamingMiddleware( + IEnumerable messages, + AgentThread? thread, + AgentRunOptions? options, + AIAgent innerAgent, + [EnumeratorCancellation] CancellationToken cancellationToken) +{ + Console.WriteLine(messages.Count()); + List updates = []; + await foreach (var update in innerAgent.RunStreamingAsync(messages, thread, options, cancellationToken)) + { + updates.Add(update); + yield return update; + } + + Console.WriteLine(updates.ToAgentRunResponse().Messages.Count); +} +``` + ## Function calling middleware > [!NOTE] diff --git a/agent-framework/user-guide/agents/agent-observability.md b/agent-framework/user-guide/agents/agent-observability.md index ed8b3205..ca25e1c0 100644 --- a/agent-framework/user-guide/agents/agent-observability.md +++ b/agent-framework/user-guide/agents/agent-observability.md @@ -21,7 +21,7 @@ Agent Framework integrates with [OpenTelemetry](https://opentelemetry.io/), and ::: zone pivot="programming-language-csharp" -## Enable Observability +## Enable Observability (C#) To enable observability for your chat client, you need to build the chat client as follows: @@ -165,15 +165,13 @@ See a full example of an agent with OpenTelemetry enabled in the [Agent Framewor ::: zone pivot="programming-language-python" -## Enable Observability +## Enable Observability (Python) -To enable observability in your python application, you do not need to install anything extra, by default the following package are installed: +To enable observability in your python application, in most cases you do not need to install anything extra, by default the following package are installed: ```text "opentelemetry-api", "opentelemetry-sdk", -"azure-monitor-opentelemetry", -"azure-monitor-opentelemetry-exporter", "opentelemetry-exporter-otlp-proto-grpc", "opentelemetry-semantic-conventions-ai", ``` @@ -220,7 +218,7 @@ The easiest way to enable observability for your application is to set the follo This can be used for any compliant OTLP endpoint, such as [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), [Aspire Dashboard](/dotnet/aspire/fundamentals/dashboard/overview?tabs=bash) or any other OTLP compliant endpoint. - APPLICATIONINSIGHTS_CONNECTION_STRING Default is `None`, set to your Application Insights connection string to export to Azure Monitor. - You can find the connection string in the Azure portal, in the "Overview" section of your Application Insights resource. + You can find the connection string in the Azure portal, in the "Overview" section of your Application Insights resource. This will require the `azure-monitor-opentelemetry-exporter` package to be installed. - VS_CODE_EXTENSION_PORT Default is `4317`, set to the port the AI Toolkit or AzureAI Foundry VS Code extension is running on. @@ -260,16 +258,45 @@ Azure AI Foundry has built-in support for tracing, with a really great visualiza When you have a Azure AI Foundry project setup with a Application Insights resource, you can do the following: +1) Install the `azure-monitor-opentelemetry-exporter` package: + +```bash +pip install azure-monitor-opentelemetry-exporter>=1.0.0b41 --pre +``` + +2) Then you can setup observability for your Azure AI Foundry project as follows: + ```python from agent_framework.azure import AzureAIAgentClient +from agent_framework.observability import setup_observability +from azure.ai.projects.aio import AIProjectClient from azure.identity import AzureCliCredential -agent_client = AzureAIAgentClient(credential=AzureCliCredential(), project_endpoint="https://.foundry.azure.com") +async def main(): + async with AIProjectClient(credential=AzureCliCredential(), project_endpoint="https://.foundry.azure.com") as project_client: + try: + conn_string = await project_client.telemetry.get_application_insights_connection_string() + setup_observability(applicationinsights_connection_string=conn_string, enable_sensitive_data=True) + except ResourceNotFoundError: + print("No Application Insights connection string found for the Azure AI Project.") +``` + +This is a convenience method, that will use the project client, to get the Application Insights connection string, and then call `setup_observability` with that connection string, overriding any existing connection string set via environment variable. + +### Zero-code instrumentation -await agent_client.setup_azure_ai_observability() +Because we use the standard OpenTelemetry SDK, you can also use zero-code instrumentation to instrument your application, run you code like this: + +```bash +opentelemetry-instrument \ + --traces_exporter console,otlp \ + --metrics_exporter console \ + --service_name your-service-name \ + --exporter_otlp_endpoint 0.0.0.0:4317 \ + python agent_framework_app.py ``` -This is a convenience method, that will use the project client, to get the Application Insights connection string, and then call `setup_observability` with that connection string. +See the [OpenTelemetry Zero-code Python documentation](https://opentelemetry.io/docs/zero-code/python/) for more information and details of the environment variables used. ## Spans and metrics @@ -288,7 +315,7 @@ The metrics that are created are: - For function invocation during the `execute_tool` operations: - `agent_framework.function.invocation.duration` (histogram): This metric measures the duration of each function execution, in seconds. -## Example trace output +### Example trace output When you run an agent with observability enabled, you'll see trace data similar to the following console output: @@ -328,7 +355,7 @@ This trace shows: - **Model information**: The AI system used (OpenAI) and response ID - **Token usage**: Input and output token counts for cost tracking -## Getting started +## Samples We have a number of samples in our repository that demonstrate these capabilities, see the [observability samples folder](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/observability) on Github. That includes samples for using zero-code telemetry as well. diff --git a/agent-framework/user-guide/agents/agent-rag.md b/agent-framework/user-guide/agents/agent-rag.md index 48992377..46104e9a 100644 --- a/agent-framework/user-guide/agents/agent-rag.md +++ b/agent-framework/user-guide/agents/agent-rag.md @@ -297,4 +297,4 @@ Each connector provides the same `create_search_function` method that can be bri ## Next steps > [!div class="nextstepaction"] -> [Agent Memory](./agent-memory.md) +> [Agent Chat History and Memory](./agent-memory.md) diff --git a/agent-framework/user-guide/agents/agent-types/TOC.yml b/agent-framework/user-guide/agents/agent-types/TOC.yml index 9c6b5468..3a1f6a21 100644 --- a/agent-framework/user-guide/agents/agent-types/TOC.yml +++ b/agent-framework/user-guide/agents/agent-types/TOC.yml @@ -20,6 +20,12 @@ href: openai-assistants-agent.md - name: Agent based on any IChatClient href: chat-client-agent.md +- name: Durable Agents + items: + - name: Create a Durable Agent + href: durable-agent/create-durable-agent.md + - name: Durable Agent Features + href: durable-agent/features.md - name: A2A Agents href: a2a-agent.md - name: Custom Agents diff --git a/agent-framework/user-guide/agents/agent-types/a2a-agent.md b/agent-framework/user-guide/agents/agent-types/a2a-agent.md index 01b7a2fa..1dab8b8a 100644 --- a/agent-framework/user-guide/agents/agent-types/a2a-agent.md +++ b/agent-framework/user-guide/agents/agent-types/a2a-agent.md @@ -76,7 +76,7 @@ See the [Agent getting started tutorials](../../../tutorials/overview.md) for mo Add the required Python packages to your project. ```bash -pip install agent-framework-a2a +pip install agent-framework-a2a --pre ``` ## Creating an A2A Agent diff --git a/agent-framework/user-guide/agents/agent-types/anthropic-agent.md b/agent-framework/user-guide/agents/agent-types/anthropic-agent.md index 41b3c404..36e6ef1b 100644 --- a/agent-framework/user-guide/agents/agent-types/anthropic-agent.md +++ b/agent-framework/user-guide/agents/agent-types/anthropic-agent.md @@ -94,6 +94,35 @@ async def explicit_config_example(): print(result.text) ``` +### Using Anthropic on Foundry + +After you've setup Anthropic on Foundry, ensure you have the following environment variables set: + +```bash +ANTHROPIC_FOUNDRY_API_KEY="your-foundry-api-key" +ANTHROPIC_FOUNDRY_RESOURCE="your-foundry-resource-name" +``` +Then create the agent as follows: + +```python +from agent_framework.anthropic import AnthropicClient +from anthropic import AsyncAnthropicFoundry + +async def foundry_example(): + agent = AnthropicClient( + anthropic_client=AsyncAnthropicFoundry() + ).create_agent( + name="FoundryAgent", + instructions="You are a helpful assistant using Anthropic on Foundry.", + ) + + result = await agent.run("How do I use Anthropic on Foundry?") + print(result.text) +``` + +> Note: +> This requires `anthropic>=0.74.0` to be installed. + ## Agent Features ### Function Tools diff --git a/agent-framework/user-guide/agents/agent-types/azure-ai-foundry-agent.md b/agent-framework/user-guide/agents/agent-types/azure-ai-foundry-agent.md index 6854ae8c..49daed0e 100644 --- a/agent-framework/user-guide/agents/agent-types/azure-ai-foundry-agent.md +++ b/agent-framework/user-guide/agents/agent-types/azure-ai-foundry-agent.md @@ -21,7 +21,7 @@ Add the required NuGet packages to your project. ```powershell dotnet add package Azure.Identity -dotnet add package Microsoft.Agents.AI.AzureAI --prerelease +dotnet add package Microsoft.Agents.AI.AzureAI.Persistent --prerelease ``` ## Creating Azure AI Foundry Agents @@ -106,7 +106,7 @@ Alternatively, you can provide these values directly in your code. Add the Agent Framework Azure AI package to your project: ```bash -pip install agent-framework-azure-ai +pip install agent-framework-azure-ai --pre ``` ## Getting Started diff --git a/agent-framework/user-guide/agents/agent-types/azure-openai-chat-completion-agent.md b/agent-framework/user-guide/agents/agent-types/azure-openai-chat-completion-agent.md index 4a4dc89a..8928df88 100644 --- a/agent-framework/user-guide/agents/agent-types/azure-openai-chat-completion-agent.md +++ b/agent-framework/user-guide/agents/agent-types/azure-openai-chat-completion-agent.md @@ -139,7 +139,7 @@ export AZURE_OPENAI_API_KEY="" # If not using Azure CLI authentic Add the Agent Framework package to your project: ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Getting Started @@ -170,7 +170,7 @@ async def main(): instructions="You are good at telling jokes.", name="Joker" ) - + result = await agent.run("Tell me a joke about a pirate.") print(result.text) @@ -195,7 +195,7 @@ async def main(): instructions="You are good at telling jokes.", name="Joker" ) - + result = await agent.run("Tell me a joke about a pirate.") print(result.text) @@ -226,7 +226,7 @@ async def main(): instructions="You are a helpful weather assistant.", tools=get_weather ) - + result = await agent.run("What's the weather like in Seattle?") print(result.text) @@ -246,7 +246,7 @@ async def main(): agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( instructions="You are a helpful assistant." ) - + print("Agent: ", end="", flush=True) async for chunk in agent.run_stream("Tell me a short story about a robot"): if chunk.text: diff --git a/agent-framework/user-guide/agents/agent-types/azure-openai-responses-agent.md b/agent-framework/user-guide/agents/agent-types/azure-openai-responses-agent.md index 79a8bd94..f72762cc 100644 --- a/agent-framework/user-guide/agents/agent-types/azure-openai-responses-agent.md +++ b/agent-framework/user-guide/agents/agent-types/azure-openai-responses-agent.md @@ -93,7 +93,7 @@ export AZURE_OPENAI_API_KEY="" # If not using Azure CLI authentic Add the Agent Framework package to your project: ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Getting Started @@ -124,7 +124,7 @@ async def main(): instructions="You are good at telling jokes.", name="Joker" ) - + result = await agent.run("Tell me a joke about a pirate.") print(result.text) @@ -150,7 +150,7 @@ async def main(): instructions="You are good at telling jokes.", name="Joker" ) - + result = await agent.run("Tell me a joke about a pirate.") print(result.text) @@ -181,7 +181,7 @@ async def main(): instructions="You are a helpful weather assistant.", tools=get_weather ) - + result = await agent.run("What's the weather like in Seattle?") print(result.text) @@ -223,7 +223,7 @@ async def main(): agent = AzureOpenAIResponsesClient(credential=AzureCliCredential()).create_agent( instructions="You are a helpful assistant." ) - + print("Agent: ", end="", flush=True) async for chunk in agent.run_stream("Tell me a short story about a robot"): if chunk.text: diff --git a/agent-framework/user-guide/agents/agent-types/chat-client-agent.md b/agent-framework/user-guide/agents/agent-types/chat-client-agent.md index 5b6f0bd5..bbc68ba7 100644 --- a/agent-framework/user-guide/agents/agent-types/chat-client-agent.md +++ b/agent-framework/user-guide/agents/agent-types/chat-client-agent.md @@ -77,14 +77,14 @@ The Microsoft Agent Framework supports creating agents for any inference service Add the required Python packages to your project. ```bash -pip install agent-framework +pip install agent-framework --pre ``` You may also need to add packages for specific chat client implementations you want to use: ```bash # For Azure AI -pip install agent-framework-azure-ai +pip install agent-framework-azure-ai --pre # For custom implementations # Install any required dependencies for your custom client diff --git a/agent-framework/user-guide/agents/agent-types/custom-agent.md b/agent-framework/user-guide/agents/agent-types/custom-agent.md index 404c3ba2..0c935092 100644 --- a/agent-framework/user-guide/agents/agent-types/custom-agent.md +++ b/agent-framework/user-guide/agents/agent-types/custom-agent.md @@ -160,7 +160,7 @@ In most cases building your own agent will involve more complex logic and integr Add the required Python packages to your project. ```bash -pip install agent-framework-core +pip install agent-framework-core --pre ``` ## Creating a Custom Agent @@ -176,7 +176,7 @@ from typing import Any class MyCustomAgent(AgentProtocol): """A custom agent that implements the AgentProtocol directly.""" - + @property def id(self) -> str: """Returns the ID of the agent.""" diff --git a/agent-framework/user-guide/agents/agent-types/durable-agent/create-durable-agent.md b/agent-framework/user-guide/agents/agent-types/durable-agent/create-durable-agent.md new file mode 100644 index 00000000..2997120c --- /dev/null +++ b/agent-framework/user-guide/agents/agent-types/durable-agent/create-durable-agent.md @@ -0,0 +1,187 @@ +--- +title: Durable Agents +description: Learn how to use the durable task extension for Microsoft Agent Framework to build stateful AI agents with serverless hosting. +zone_pivot_groups: programming-languages +author: anthonychu +ms.topic: tutorial +ms.author: antchu +ms.date: 11/05/2025 +ms.service: agent-framework +--- + +# Durable Agents + +The durable task extension for Microsoft Agent Framework enables you to build stateful AI agents and multi-agent deterministic orchestrations in a serverless environment on Azure. + +[Azure Functions](/azure/azure-functions/functions-overview) is a serverless compute service that lets you run code on-demand without managing infrastructure. The durable task extension for Microsoft Agent Framework builds on this foundation to provide durable state management, meaning your agent's conversation history and execution state are reliably persisted and survive failures, restarts, and long-running operations. + +The extension manages agent thread state and orchestration coordination, allowing you to focus on your agent logic instead of infrastructure concerns for reliability. + +## Key Features + +The durable task extension provides the following key features: + +- **Serverless hosting**: Deploy and host agents in Azure Functions with automatically generated HTTP endpoints for agent interactions +- **Stateful agent threads**: Maintain persistent threads with conversation history that survive across multiple interactions +- **Deterministic orchestrations**: Coordinate multiple agents reliably with fault-tolerant workflows that can run for days or weeks, supporting sequential, parallel, and human-in-the-loop patterns +- **Observability and debugging**: Visualize agent conversations, orchestration flows, and execution history through the built-in Durable Task Scheduler dashboard + +## Getting Started + +::: zone pivot="programming-language-csharp" + +In a .NET Azure Functions project, add the required NuGet packages. + +```bash +dotnet add package Azure.AI.OpenAI --prerelease +dotnet add package Azure.Identity +dotnet add package Microsoft.Agents.AI.OpenAI --prerelease +dotnet add package Microsoft.Agents.AI.Hosting.AzureFunctions --prerelease +``` + +> [!NOTE] +> In addition to these packages, ensure your project uses version 2.2.0 or later of the [Microsoft.Azure.Functions.Worker](https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker/) package. + +::: zone-end + +::: zone pivot="programming-language-python" + +In a Python Azure Functions project, install the required Python packages. + +```bash +pip install azure-identity +pip install agent-framework-azurefunctions --pre +``` + +::: zone-end + +## Serverless Hosting + +With the durable task extension, you can deploy and host Microsoft Agent Framework agents in Azure Functions with built-in HTTP endpoints and orchestration-based invocation. Azure Functions provides event-driven, pay-per-invocation pricing with automatic scaling and minimal infrastructure management. + +When you configure a durable agent, the durable task extension automatically creates HTTP endpoints for your agent and manages all the underlying infrastructure for storing conversation state, handling concurrent requests, and coordinating multi-agent workflows. + +::: zone pivot="programming-language-csharp" + +```csharp +using System; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Hosting.AzureFunctions; +using Microsoft.Azure.Functions.Worker.Builder; +using Microsoft.Extensions.Hosting; + +var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); +var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o-mini"; + +// Create an AI agent following the standard Microsoft Agent Framework pattern +AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .CreateAIAgent( + instructions: "You are good at telling jokes.", + name: "Joker"); + +// Configure the function app to host the agent with durable thread management +// This automatically creates HTTP endpoints and manages state persistence +using IHost app = FunctionsApplication + .CreateBuilder(args) + .ConfigureFunctionsWebApplication() + .ConfigureDurableAgents(options => + options.AddAIAgent(agent) + ) + .Build(); +app.Run(); +``` + +::: zone-end + +::: zone pivot="programming-language-python" + +```python +import os +from agent_framework.azure import AzureOpenAIChatClient, AgentFunctionApp +from azure.identity import DefaultAzureCredential + +endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") +deployment_name = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4o-mini") + +# Create an AI agent following the standard Microsoft Agent Framework pattern +agent = AzureOpenAIChatClient( + endpoint=endpoint, + deployment_name=deployment_name, + credential=DefaultAzureCredential() +).create_agent( + instructions="You are good at telling jokes.", + name="Joker" +) + +# Configure the function app to host the agent with durable thread management +# This automatically creates HTTP endpoints and manages state persistence +app = AgentFunctionApp(agents=[agent]) +``` + +::: zone-end + +### When to Use Durable Agents + +Choose durable agents when you need: + +- **Full code control**: Deploy and manage your own compute environment while maintaining serverless benefits +- **Complex orchestrations**: Coordinate multiple agents with deterministic, reliable workflows that can run for days or weeks +- **Event-driven orchestration**: Integrate with Azure Functions triggers (HTTP, timers, queues, etc.) and bindings for event-driven agent workflows +- **Automatic conversation state**: Agent conversation history is automatically managed and persisted without requiring explicit state handling in your code + +This serverless hosting approach differs from managed service-based agent hosting (such as Azure AI Foundry Agent Service), which provides fully managed infrastructure without requiring you to deploy or manage Azure Functions apps. Durable agents are ideal when you need the flexibility of code-first deployment combined with the reliability of durable state management. + +When hosted in the [Azure Functions Flex Consumption](/azure/azure-functions/flex-consumption-plan) hosting plan, agents can scale to thousands of instances or to zero instances when not in use, allowing you to pay only for the compute you need. + +## Stateful Agent Threads with Conversation History + +Agents maintain persistent threads that survive across multiple interactions. Each thread is identified by a unique thread ID and stores the complete conversation history in durable storage managed by the [Durable Task Scheduler](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler). + +This pattern enables conversational continuity where agent state is preserved through process crashes and restarts, allowing full conversation history to be maintained across user threads. The durable storage ensures that even if your Azure Functions instance restarts or scales to a different instance, the conversation seamlessly continues from where it left off. + +The following example demonstrates multiple HTTP requests to the same thread, showing how conversation context persists: + +```bash +# First interaction - start a new thread +curl -X POST https://your-function-app.azurewebsites.net/api/agents/Joker/run \ + -H "Content-Type: text/plain" \ + -d "Tell me a joke about pirates" + +# Response includes thread ID in x-ms-thread-id header and joke as plain text +# HTTP/1.1 200 OK +# Content-Type: text/plain +# x-ms-thread-id: @dafx-joker@263fa373-fa01-4705-abf2-5a114c2bb87d +# +# Why don't pirates shower before they walk the plank? Because they'll just wash up on shore later! + +# Second interaction - continue the same thread with context +curl -X POST "https://your-function-app.azurewebsites.net/api/agents/Joker/run?thread_id=@dafx-joker@263fa373-fa01-4705-abf2-5a114c2bb87d" \ + -H "Content-Type: text/plain" \ + -d "Tell me another one about the same topic" + +# Agent remembers the pirate context from the first message and responds with plain text +# What's a pirate's favorite letter? You'd think it's R, but it's actually the C! +``` + +Agent state is maintained in durable storage, enabling distributed execution across multiple instances. Any instance can resume an agent's execution after interruptions or failures, ensuring continuous operation. + +## Next Steps + +Learn about advanced capabilities of the durable task extension: + +> [!div class="nextstepaction"] +> [Durable Agent Features](features.md) + +For a step-by-step tutorial on building and running a durable agent: + +> [!div class="nextstepaction"] +> [Create and run a durable agent](../../../../tutorials/agents/create-and-run-durable-agent.md) + +## Related Content + +- [Durable Task Scheduler Overview](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Azure Functions Flex Consumption Plan](/azure/azure-functions/flex-consumption-plan) +- [Microsoft Agent Framework Overview](../../../../overview/agent-framework-overview.md) diff --git a/agent-framework/user-guide/agents/agent-types/durable-agent/features.md b/agent-framework/user-guide/agents/agent-types/durable-agent/features.md new file mode 100644 index 00000000..92b7425a --- /dev/null +++ b/agent-framework/user-guide/agents/agent-types/durable-agent/features.md @@ -0,0 +1,378 @@ +--- +title: Durable Agent Features +description: Learn about advanced features of the durable task extension for Microsoft Agent Framework including orchestrations, tool calls, and human-in-the-loop workflows. +zone_pivot_groups: programming-languages +author: anthonychu +ms.topic: tutorial +ms.author: antchu +ms.date: 11/05/2025 +ms.service: agent-framework +--- + +# Durable Agent Features + +When you build AI agents with Microsoft Agent Framework, the durable task extension for Microsoft Agent Framework adds advanced capabilities to your standard agents including automatic conversation state management, deterministic orchestrations, and human-in-the-loop patterns. The extension also makes it easy to host your agents on serverless compute provided by Azure Functions, delivering dynamic scaling and a cost-efficient per-request billing model. + +## Deterministic Multi-Agent Orchestrations + +The durable task extension supports building deterministic workflows that coordinate multiple agents using [Azure Durable Functions](/azure/azure-functions/durable/durable-functions-overview) orchestrations. + +**[Orchestrations](/azure/azure-functions/durable/durable-functions-orchestrations)** are code-based workflows that coordinate multiple operations (like agent calls, external API calls, or timers) in a reliable way. **Deterministic** means the orchestration code executes the same way when replayed after a failure, making workflows reliable and debuggable—when you replay an orchestration's history, you can see exactly what happened at each step. + +Orchestrations execute reliably, surviving failures between agent calls, and provide predictable and repeatable processes. This makes them ideal for complex multi-agent scenarios where you need guaranteed execution order and fault tolerance. + +### Sequential Orchestrations + +In the sequential multi-agent pattern, specialized agents execute in a specific order, where each agent's output can influence the next agent's execution. This pattern supports conditional logic and branching based on agent responses. + +::: zone pivot="programming-language-csharp" + +When using agents in orchestrations, you must use the `context.GetAgent()` API to get a `DurableAIAgent` instance, which is a special subclass of the standard `AIAgent` type that wraps one of your registered agents. The `DurableAIAgent` wrapper ensures that agent calls are properly tracked and checkpointed by the durable orchestration framework. + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.Agents.AI.DurableTask; + +[Function(nameof(SpamDetectionOrchestration))] +public static async Task SpamDetectionOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + Email email = context.GetInput(); + + // Check if the email is spam + DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent"); + AgentThread spamThread = spamDetectionAgent.GetNewThread(); + + AgentRunResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( + message: $"Analyze this email for spam: {email.EmailContent}", + thread: spamThread); + DetectionResult result = spamDetectionResponse.Result; + + if (result.IsSpam) + { + return await context.CallActivityAsync(nameof(HandleSpamEmail), result.Reason); + } + + // Generate response for legitimate email + DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent"); + AgentThread emailThread = emailAssistantAgent.GetNewThread(); + + AgentRunResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( + message: $"Draft a professional response to: {email.EmailContent}", + thread: emailThread); + + return await context.CallActivityAsync(nameof(SendEmail), emailAssistantResponse.Result.Response); +} +``` + +::: zone-end + +::: zone pivot="programming-language-python" + +When using agents in orchestrations, you must use the `app.get_agent()` method to get a durable agent instance, which is a special wrapper around one of your registered agents. The durable agent wrapper ensures that agent calls are properly tracked and checkpointed by the durable orchestration framework. + +```python +import azure.durable_functions as df +from typing import cast +from agent_framework.azure import AgentFunctionApp +from pydantic import BaseModel + +class SpamDetectionResult(BaseModel): + is_spam: bool + reason: str + +class EmailResponse(BaseModel): + response: str + +app = AgentFunctionApp(agents=[spam_detection_agent, email_assistant_agent]) + +@app.orchestration_trigger(context_name="context") +def spam_detection_orchestration(context: df.DurableOrchestrationContext): + email = context.get_input() + + # Check if the email is spam + spam_agent = app.get_agent(context, "SpamDetectionAgent") + spam_thread = spam_agent.get_new_thread() + + spam_result_raw = yield spam_agent.run( + messages=f"Analyze this email for spam: {email['content']}", + thread=spam_thread, + response_format=SpamDetectionResult + ) + spam_result = cast(SpamDetectionResult, spam_result_raw.get("structured_response")) + + if spam_result.is_spam: + result = yield context.call_activity("handle_spam_email", spam_result.reason) + return result + + # Generate response for legitimate email + email_agent = app.get_agent(context, "EmailAssistantAgent") + email_thread = email_agent.get_new_thread() + + email_response_raw = yield email_agent.run( + messages=f"Draft a professional response to: {email['content']}", + thread=email_thread, + response_format=EmailResponse + ) + email_response = cast(EmailResponse, email_response_raw.get("structured_response")) + + result = yield context.call_activity("send_email", email_response.response) + return result +``` + +::: zone-end + +Orchestrations coordinate work across multiple agents, surviving failures between agent calls. The orchestration context provides methods to retrieve and interact with hosted agents within orchestrations. + +### Parallel Orchestrations + +In the parallel multi-agent pattern, you execute multiple agents concurrently and then aggregate their results. This pattern is useful for gathering diverse perspectives or processing independent subtasks simultaneously. + +::: zone pivot="programming-language-csharp" + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.Agents.AI.DurableTask; + +[Function(nameof(ResearchOrchestration))] +public static async Task ResearchOrchestration( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + string topic = context.GetInput(); + + // Execute multiple research agents in parallel + DurableAIAgent technicalAgent = context.GetAgent("TechnicalResearchAgent"); + DurableAIAgent marketAgent = context.GetAgent("MarketResearchAgent"); + DurableAIAgent competitorAgent = context.GetAgent("CompetitorResearchAgent"); + + // Start all agent runs concurrently + Task> technicalTask = + technicalAgent.RunAsync($"Research technical aspects of {topic}"); + Task> marketTask = + marketAgent.RunAsync($"Research market trends for {topic}"); + Task> competitorTask = + competitorAgent.RunAsync($"Research competitors in {topic}"); + + // Wait for all tasks to complete + await Task.WhenAll(technicalTask, marketTask, competitorTask); + + // Aggregate results + string allResearch = string.Join("\n\n", + technicalTask.Result.Result.Text, + marketTask.Result.Result.Text, + competitorTask.Result.Result.Text); + + DurableAIAgent summaryAgent = context.GetAgent("SummaryAgent"); + AgentRunResponse summaryResponse = + await summaryAgent.RunAsync($"Summarize this research:\n{allResearch}"); + + return summaryResponse.Result.Text; +} +``` + +::: zone-end + +::: zone pivot="programming-language-python" + +```python +import azure.durable_functions as df +from agent_framework.azure import AgentFunctionApp + +app = AgentFunctionApp(agents=[technical_agent, market_agent, competitor_agent, summary_agent]) + +@app.orchestration_trigger(context_name="context") +def research_orchestration(context: df.DurableOrchestrationContext): + topic = context.get_input() + + # Execute multiple research agents in parallel + technical_agent = app.get_agent(context, "TechnicalResearchAgent") + market_agent = app.get_agent(context, "MarketResearchAgent") + competitor_agent = app.get_agent(context, "CompetitorResearchAgent") + + technical_task = technical_agent.run(messages=f"Research technical aspects of {topic}") + market_task = market_agent.run(messages=f"Research market trends for {topic}") + competitor_task = competitor_agent.run(messages=f"Research competitors in {topic}") + + # Wait for all tasks to complete + results = yield context.task_all([technical_task, market_task, competitor_task]) + + # Aggregate results + all_research = "\n\n".join([r.get('response', '') for r in results]) + + summary_agent = app.get_agent(context, "SummaryAgent") + summary = yield summary_agent.run(messages=f"Summarize this research:\n{all_research}") + + return summary.get('response', '') +``` + +::: zone-end + +The parallel execution is tracked using a list of tasks. Automatic checkpointing ensures that completed agent executions are not repeated or lost if a failure occurs during aggregation. + +### Human-in-the-Loop Orchestrations + +Deterministic agent orchestrations can pause for human input, approval, or review without consuming compute resources. Durable execution enables orchestrations to wait for days or even weeks while waiting for human responses. When combined with serverless hosting, all compute resources are spun down during the wait period, eliminating compute costs until the human provides their input. + +::: zone pivot="programming-language-csharp" + +```csharp +using Microsoft.Azure.Functions.Worker; +using Microsoft.DurableTask; +using Microsoft.Agents.AI.DurableTask; + +[Function(nameof(ContentApprovalWorkflow))] +public static async Task ContentApprovalWorkflow( + [OrchestrationTrigger] TaskOrchestrationContext context) +{ + string topic = context.GetInput(); + + // Generate content using an agent + DurableAIAgent contentAgent = context.GetAgent("ContentGenerationAgent"); + AgentRunResponse contentResponse = + await contentAgent.RunAsync($"Write an article about {topic}"); + GeneratedContent draftContent = contentResponse.Result; + + // Send for human review + await context.CallActivityAsync(nameof(NotifyReviewer), draftContent); + + // Wait for approval with timeout + HumanApprovalResponse approvalResponse; + try + { + approvalResponse = await context.WaitForExternalEvent( + eventName: "ApprovalDecision", + timeout: TimeSpan.FromHours(24)); + } + catch (OperationCanceledException) + { + // Timeout occurred - escalate for review + return await context.CallActivityAsync(nameof(EscalateForReview), draftContent); + } + + if (approvalResponse.Approved) + { + return await context.CallActivityAsync(nameof(PublishContent), draftContent); + } + + return "Content rejected"; +} +``` + +::: zone-end + +::: zone pivot="programming-language-python" + +```python +import azure.durable_functions as df +from datetime import timedelta +from agent_framework.azure import AgentFunctionApp + +app = AgentFunctionApp(agents=[content_agent]) + +@app.orchestration_trigger(context_name="context") +def content_approval_workflow(context: df.DurableOrchestrationContext): + topic = context.get_input() + + # Generate content using an agent + content_agent = app.get_agent(context, "ContentGenerationAgent") + draft_content = yield content_agent.run( + messages=f"Write an article about {topic}" + ) + + # Send for human review + yield context.call_activity("notify_reviewer", draft_content) + + # Wait for approval with timeout + approval_task = context.wait_for_external_event("ApprovalDecision") + timeout_task = context.create_timer( + context.current_utc_datetime + timedelta(hours=24) + ) + + winner = yield context.task_any([approval_task, timeout_task]) + + if winner == approval_task: + timeout_task.cancel() + approval_data = approval_task.result + if approval_data.get("approved"): + result = yield context.call_activity("publish_content", draft_content) + return result + return "Content rejected" + + # Timeout occurred - escalate for review + result = yield context.call_activity("escalate_for_review", draft_content) + return result +``` + +::: zone-end + +Deterministic agent orchestrations can wait for external events, durably persisting their state while waiting for human feedback, surviving failures, restarts, and extended waiting periods. When the human response arrives, the orchestration automatically resumes with full conversation context and execution state intact. + +### Providing Human Input + +To send approval or input to a waiting orchestration, you'll need to raise an external event to the orchestration instance using the Durable Functions client SDK. For example, a reviewer might approve content through a web form that calls: + +::: zone pivot="programming-language-csharp" + +```csharp +await client.RaiseEventAsync(instanceId, "ApprovalDecision", new HumanApprovalResponse +{ + Approved = true, + Feedback = "Looks great!" +}); +``` + +::: zone-end + +::: zone pivot="programming-language-python" + +```python +approval_data = { + "approved": True, + "feedback": "Looks great!" +} +await client.raise_event(instance_id, "ApprovalDecision", approval_data) +``` + +::: zone-end + +### Cost Efficiency + +Human-in-the-loop workflows with durable agents are extremely cost-effective when hosted on the [Azure Functions Flex Consumption plan](/azure/azure-functions/flex-consumption-plan). For a workflow waiting 24 hours for approval, you only pay for a few seconds of execution time (the time to generate content, send notification, and process the response)—not the 24 hours of waiting. During the wait period, no compute resources are consumed. + +## Observability with Durable Task Scheduler + +The [Durable Task Scheduler](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) (DTS) is the recommended durable backend for your durable agents, offering the best performance, fully managed infrastructure, and built-in observability through a UI dashboard. While Azure Functions can use other storage backends (like Azure Storage), DTS is optimized specifically for durable workloads and provides superior performance and monitoring capabilities. + +### Agent Thread Insights + +- **Conversation history**: View complete conversation threads for each agent thread, including all messages, tool calls, and conversation context at any point in time +- **Task timing**: Monitor how long specific tasks and agent interactions take to complete + +:::image type="content" source="../../../../media/durable-agent-chat-history.png" alt-text="Screenshot of the Durable Task Scheduler dashboard showing agent chat history with conversation threads and messages."::: + +### Orchestration Insights + +- **Multi-agent visualization**: See the execution flow when calling multiple specialized agents with visual representation of parallel executions and conditional branching +- **Execution history**: Access detailed execution logs +- **Real-time monitoring**: Track active orchestrations, queued work items, and agent states across your deployment +- **Performance metrics**: Monitor agent response times, token usage, and orchestration duration + +:::image type="content" source="../../../../media/durable-agent-orchestration.png" alt-text="Screenshot of the Durable Task Scheduler dashboard showing orchestration visualization with multiple agent interactions and workflow execution."::: + +### Debugging Capabilities + +- View structured agent outputs and tool call results +- Trace tool invocations and their outcomes +- Monitor external event handling for human-in-the-loop scenarios + +The dashboard enables you to understand exactly what your agents are doing, diagnose issues quickly, and optimize performance based on real execution data. + +## Related Content + +- [User guide: create a Durable Agent](create-durable-agent.md) +- [Tutorial: Create and run a durable agent](../../../../tutorials/agents/create-and-run-durable-agent.md) +- [Durable Task Scheduler Overview](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler) +- [Durable Task Scheduler Dashboard](/azure/azure-functions/durable/durable-task-scheduler/durable-task-scheduler-dashboard) +- [Azure Functions Overview](/azure/azure-functions/functions-overview) diff --git a/agent-framework/user-guide/agents/agent-types/openai-assistants-agent.md b/agent-framework/user-guide/agents/agent-types/openai-assistants-agent.md index a6801b78..e744f351 100644 --- a/agent-framework/user-guide/agents/agent-types/openai-assistants-agent.md +++ b/agent-framework/user-guide/agents/agent-types/openai-assistants-agent.md @@ -100,7 +100,7 @@ See the [Agent getting started tutorials](../../../tutorials/overview.md) for mo Install the Microsoft Agent Framework package. ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Configuration @@ -175,14 +175,14 @@ from openai import AsyncOpenAI async def existing_assistant_example(): # Create OpenAI client directly client = AsyncOpenAI() - + # Create or get an existing assistant assistant = await client.beta.assistants.create( model="gpt-4o-mini", name="WeatherAssistant", instructions="You are a weather forecasting assistant." ) - + try: # Use the existing assistant with Agent Framework async with ChatAgent( diff --git a/agent-framework/user-guide/agents/agent-types/openai-chat-completion-agent.md b/agent-framework/user-guide/agents/agent-types/openai-chat-completion-agent.md index 46d0700e..6f2e9f3f 100644 --- a/agent-framework/user-guide/agents/agent-types/openai-chat-completion-agent.md +++ b/agent-framework/user-guide/agents/agent-types/openai-chat-completion-agent.md @@ -67,7 +67,7 @@ See the [Agent getting started tutorials](../../../tutorials/overview.md) for mo Install the Microsoft Agent Framework package. ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Configuration diff --git a/agent-framework/user-guide/agents/agent-types/openai-responses-agent.md b/agent-framework/user-guide/agents/agent-types/openai-responses-agent.md index 2599ef75..c3bcf484 100644 --- a/agent-framework/user-guide/agents/agent-types/openai-responses-agent.md +++ b/agent-framework/user-guide/agents/agent-types/openai-responses-agent.md @@ -69,7 +69,7 @@ See the [Agent getting started tutorials](../../../tutorials/overview.md) for mo Install the Microsoft Agent Framework package. ```bash -pip install agent-framework +pip install agent-framework --pre ``` ## Configuration @@ -205,7 +205,7 @@ async def structured_output_example(): # Non-streaming structured output result = await agent.run("Tell me about Paris, France", response_format=CityInfo) - + if result.value: city_data = result.value print(f"City: {city_data.city}") @@ -216,7 +216,7 @@ async def structured_output_example(): agent.run_stream("Tell me about Tokyo, Japan", response_format=CityInfo), output_format_type=CityInfo, ) - + if structured_result.value: tokyo_data = structured_result.value print(f"City: {tokyo_data.city}") @@ -266,7 +266,7 @@ async def image_generation_example(): ) result = await agent.run("Generate an image of a sunset over the ocean.") - + # Check for generated images in the response for content in result.contents: if isinstance(content, (DataContent, UriContent)): diff --git a/agent-framework/user-guide/agents/multi-turn-conversation.md b/agent-framework/user-guide/agents/multi-turn-conversation.md index e1ef546d..cf0dfc1c 100644 --- a/agent-framework/user-guide/agents/multi-turn-conversation.md +++ b/agent-framework/user-guide/agents/multi-turn-conversation.md @@ -17,7 +17,14 @@ The Microsoft Agent Framework provides built-in support for managing multi-turn For example, when using a ChatClientAgent based on a foundry agent, the conversation history is persisted in the service. While, when using a ChatClientAgent based on chat completion with gpt-4.1 the conversation history is in-memory and managed by the agent. -The differences between the underlying threading models are abstracted away via the `AgentThread` type. +The `AgentThread` type is the abstraction that represents a conversation thread with an agent. +`AIAgent` instances are stateless and the same agent instance can be used with multiple `AgentThread` instances. All state is therefore preserved in the `AgentThread`. +An `AgentThread` can both represent chat history plus any other state that the agent needs to preserve across multiple interactions. +The chat history may be stored in the thread itself, or remotely, with the `AgentThread` only containing a reference to the remote chat history. +The `AgentThread` state may also include memories or references to memories stored remotely. + +> [!TIP] +> To learn more about Chat History and Memory in the Agent Framework, see [Agent Chat History and Memory](./agent-memory.md). ### AgentThread Creation @@ -42,7 +49,6 @@ response = await agent.RunAsync("Hello, how are you?"); ::: zone-end - ### AgentThread Storage `AgentThread` instances can be serialized and stored for later use. This allows for the preservation of conversation context across different sessions or service calls. @@ -215,7 +221,6 @@ async def multi_turn_example(): print(f"Agent: {response3.text}") # Should remember previous context ``` - ::: zone-end ## Next steps diff --git a/agent-framework/user-guide/devui/TOC.yml b/agent-framework/user-guide/devui/TOC.yml new file mode 100644 index 00000000..3bbfb2b8 --- /dev/null +++ b/agent-framework/user-guide/devui/TOC.yml @@ -0,0 +1,12 @@ +- name: Overview + href: index.md +- name: Directory Discovery + href: directory-discovery.md +- name: API Reference + href: api-reference.md +- name: Tracing & Observability + href: tracing.md +- name: Security & Deployment + href: security.md +- name: Samples + href: samples.md diff --git a/agent-framework/user-guide/devui/api-reference.md b/agent-framework/user-guide/devui/api-reference.md new file mode 100644 index 00000000..3077fd70 --- /dev/null +++ b/agent-framework/user-guide/devui/api-reference.md @@ -0,0 +1,224 @@ +--- +title: DevUI API Reference +description: Learn about the OpenAI-compatible API endpoints provided by DevUI. +author: moonbox3 +ms.topic: reference +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# API Reference + +DevUI provides an OpenAI-compatible Responses API, allowing you to use the OpenAI SDK or any HTTP client to interact with your agents and workflows. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI documentation for C# is coming soon. Please check back later or refer to the Python documentation for conceptual guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +## Base URL + +``` +http://localhost:8080/v1 +``` + +The port can be configured with the `--port` CLI option. + +## Authentication + +By default, DevUI does not require authentication for local development. When running with `--auth`, Bearer token authentication is required. + +## Using the OpenAI SDK + +### Basic Request + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8080/v1", + api_key="not-needed" # API key not required for local DevUI +) + +response = client.responses.create( + metadata={"entity_id": "weather_agent"}, # Your agent/workflow name + input="What's the weather in Seattle?" +) + +# Extract text from response +print(response.output[0].content[0].text) +``` + +### Streaming + +```python +response = client.responses.create( + metadata={"entity_id": "weather_agent"}, + input="What's the weather in Seattle?", + stream=True +) + +for event in response: + # Process streaming events + print(event) +``` + +### Multi-turn Conversations + +Use the standard OpenAI `conversation` parameter for multi-turn conversations: + +```python +# Create a conversation +conversation = client.conversations.create( + metadata={"agent_id": "weather_agent"} +) + +# First turn +response1 = client.responses.create( + metadata={"entity_id": "weather_agent"}, + input="What's the weather in Seattle?", + conversation=conversation.id +) + +# Follow-up turn (continues the conversation) +response2 = client.responses.create( + metadata={"entity_id": "weather_agent"}, + input="How about tomorrow?", + conversation=conversation.id +) +``` + +DevUI automatically retrieves the conversation's message history and passes it to the agent. + +## REST API Endpoints + +### Responses API (OpenAI Standard) + +Execute an agent or workflow: + +```bash +curl -X POST http://localhost:8080/v1/responses \ + -H "Content-Type: application/json" \ + -d '{ + "metadata": {"entity_id": "weather_agent"}, + "input": "What is the weather in Seattle?" + }' +``` + +### Conversations API (OpenAI Standard) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/conversations` | POST | Create a conversation | +| `/v1/conversations/{id}` | GET | Get conversation details | +| `/v1/conversations/{id}` | POST | Update conversation metadata | +| `/v1/conversations/{id}` | DELETE | Delete a conversation | +| `/v1/conversations?agent_id={id}` | GET | List conversations (DevUI extension) | +| `/v1/conversations/{id}/items` | POST | Add items to conversation | +| `/v1/conversations/{id}/items` | GET | List conversation items | +| `/v1/conversations/{id}/items/{item_id}` | GET | Get a conversation item | + +### Entity Management (DevUI Extension) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/v1/entities` | GET | List discovered agents/workflows | +| `/v1/entities/{entity_id}/info` | GET | Get detailed entity information | +| `/v1/entities/{entity_id}/reload` | POST | Hot reload entity (developer mode) | + +### Health Check + +```bash +curl http://localhost:8080/health +``` + +### Server Metadata + +Get server configuration and capabilities: + +```bash +curl http://localhost:8080/meta +``` + +Returns: +- `ui_mode` - Current mode (`developer` or `user`) +- `version` - DevUI version +- `framework` - Framework name (`agent_framework`) +- `runtime` - Backend runtime (`python`) +- `capabilities` - Feature flags (tracing, OpenAI proxy, deployment) +- `auth_required` - Whether authentication is enabled + +## Event Mapping + +DevUI maps Agent Framework events to OpenAI Responses API events. The table below shows the mapping: + +### Lifecycle Events + +| OpenAI Event | Agent Framework Event | +|--------------|----------------------| +| `response.created` + `response.in_progress` | `AgentStartedEvent` | +| `response.completed` | `AgentCompletedEvent` | +| `response.failed` | `AgentFailedEvent` | +| `response.created` + `response.in_progress` | `WorkflowStartedEvent` | +| `response.completed` | `WorkflowCompletedEvent` | +| `response.failed` | `WorkflowFailedEvent` | + +### Content Types + +| OpenAI Event | Agent Framework Content | +|--------------|------------------------| +| `response.content_part.added` + `response.output_text.delta` | `TextContent` | +| `response.reasoning_text.delta` | `TextReasoningContent` | +| `response.output_item.added` | `FunctionCallContent` (initial) | +| `response.function_call_arguments.delta` | `FunctionCallContent` (args) | +| `response.function_result.complete` | `FunctionResultContent` | +| `response.output_item.added` (image) | `DataContent` (images) | +| `response.output_item.added` (file) | `DataContent` (files) | +| `error` | `ErrorContent` | + +### Workflow Events + +| OpenAI Event | Agent Framework Event | +|--------------|----------------------| +| `response.output_item.added` (ExecutorActionItem) | `ExecutorInvokedEvent` | +| `response.output_item.done` (ExecutorActionItem) | `ExecutorCompletedEvent` | +| `response.output_item.added` (ResponseOutputMessage) | `WorkflowOutputEvent` | + +### DevUI Custom Extensions + +DevUI adds custom event types for Agent Framework-specific functionality: + +- `response.function_approval.requested` - Function approval requests +- `response.function_approval.responded` - Function approval responses +- `response.function_result.complete` - Server-side function execution results +- `response.workflow_event.complete` - Workflow events +- `response.trace.complete` - Execution traces + +These custom extensions are namespaced and can be safely ignored by standard OpenAI clients. + +## OpenAI Proxy Mode + +DevUI provides an **OpenAI Proxy** feature for testing OpenAI models directly through the interface without creating custom agents. Enable via Settings in the UI. + +```bash +curl -X POST http://localhost:8080/v1/responses \ + -H "X-Proxy-Backend: openai" \ + -d '{"model": "gpt-4.1-mini", "input": "Hello"}' +``` + +> [!NOTE] +> Proxy mode requires `OPENAI_API_KEY` environment variable configured on the backend. + +::: zone-end + +## Next Steps + +- [Tracing & Observability](./tracing.md) - View traces for debugging +- [Security & Deployment](./security.md) - Secure your DevUI deployment diff --git a/agent-framework/user-guide/devui/directory-discovery.md b/agent-framework/user-guide/devui/directory-discovery.md new file mode 100644 index 00000000..8c9c6b3d --- /dev/null +++ b/agent-framework/user-guide/devui/directory-discovery.md @@ -0,0 +1,141 @@ +--- +title: DevUI Directory Discovery +description: Learn how to structure your agents and workflows for automatic discovery by DevUI. +author: moonbox3 +ms.topic: how-to +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# Directory Discovery + +DevUI can automatically discover agents and workflows from a directory structure. This enables you to organize multiple entities and launch them all with a single command. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI documentation for C# is coming soon. Please check back later or refer to the Python documentation for conceptual guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +## Directory Structure + +For your agents and workflows to be discovered by DevUI, they must be organized in a specific directory structure. Each entity must have an `__init__.py` file that exports the required variable (`agent` or `workflow`). + +``` +entities/ + weather_agent/ + __init__.py # Must export: agent = ChatAgent(...) + agent.py # Agent implementation (optional, can be in __init__.py) + .env # Optional: API keys, config vars + my_workflow/ + __init__.py # Must export: workflow = WorkflowBuilder()... + workflow.py # Workflow implementation (optional) + .env # Optional: environment variables + .env # Optional: shared environment variables +``` + +## Agent Example + +Create a directory for your agent with the required `__init__.py`: + +**`weather_agent/__init__.py`**: + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient + +def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}: 72F and sunny" + +agent = ChatAgent( + name="weather_agent", + chat_client=OpenAIChatClient(), + tools=[get_weather], + instructions="You are a helpful weather assistant." +) +``` + +The key requirement is that the `__init__.py` file must export a variable named `agent` (for agents) or `workflow` (for workflows). + +## Workflow Example + +**`my_workflow/__init__.py`**: + +```python +from agent_framework.workflows import WorkflowBuilder + +workflow = ( + WorkflowBuilder() + .add_executor(...) + .add_edge(...) + .build() +) +``` + +## Environment Variables + +DevUI automatically loads `.env` files if present: + +1. **Entity-level `.env`**: Placed in the agent/workflow directory, loaded only for that entity +2. **Parent-level `.env`**: Placed in the entities root directory, loaded for all entities + +Example `.env` file: + +```bash +OPENAI_API_KEY=sk-... +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ +``` + +> [!TIP] +> Create a `.env.example` file to document required environment variables without exposing actual values. Never commit `.env` files with real credentials to source control. + +## Launching with Directory Discovery + +Once your directory structure is set up, launch DevUI: + +```bash +# Discover all entities in ./entities directory +devui ./entities + +# With custom port +devui ./entities --port 9000 + +# With auto-reload for development +devui ./entities --reload +``` + +## Sample Gallery + +When DevUI starts with no discovered entities, it displays a **sample gallery** with curated examples from the Agent Framework repository. You can: + +- Browse available sample agents and workflows +- Download samples to review and customize +- Run samples locally to get started quickly + +## Troubleshooting + +### Entity not discovered + +- Ensure the `__init__.py` file exports `agent` or `workflow` variable +- Check for syntax errors in your Python files +- Verify the directory is directly under the path passed to `devui` + +### Environment variables not loaded + +- Ensure the `.env` file is in the correct location +- Check file permissions +- Use `--reload` flag to pick up changes during development + +::: zone-end + +## Next Steps + +- [API Reference](./api-reference.md) - Learn about the OpenAI-compatible API +- [Tracing & Observability](./tracing.md) - Debug your agents with traces diff --git a/agent-framework/user-guide/devui/index.md b/agent-framework/user-guide/devui/index.md new file mode 100644 index 00000000..109eef21 --- /dev/null +++ b/agent-framework/user-guide/devui/index.md @@ -0,0 +1,148 @@ +--- +title: DevUI Overview +description: Learn how to use DevUI, a sample app for running and testing agents and workflows in the Microsoft Agent Framework. +author: moonbox3 +ms.topic: overview +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# DevUI - A Sample App for Running Agents and Workflows + +DevUI is a lightweight, standalone sample application for running agents and workflows in the Microsoft Agent Framework. It provides a web interface for interactive testing along with an OpenAI-compatible API backend, allowing you to visually debug, test, and iterate on agents and workflows you build before integrating them into your applications. + +> [!IMPORTANT] +> DevUI is a **sample app** to help you visualize and debug your agents and workflows during development. It is **not** intended for production use. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI documentation for C# is coming soon. Please check back later or refer to the Python documentation for conceptual guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +

+ DevUI +

+ +## Features + +- **Web Interface**: Interactive UI for testing agents and workflows +- **Flexible Input Types**: Support for text, file uploads, and custom input types based on your workflow's first executor +- **Directory-Based Discovery**: Automatically discover agents and workflows from a directory structure +- **In-Memory Registration**: Register entities programmatically without file system setup +- **OpenAI-Compatible API**: Use the OpenAI Python SDK to interact with your agents +- **Sample Gallery**: Browse and download curated examples when no entities are discovered +- **Tracing**: View OpenTelemetry traces for debugging and observability + +## Input Types + +DevUI adapts its input interface based on the entity type: + +- **Agents**: Support text input and file attachments (images, documents, etc.) for multimodal interactions +- **Workflows**: The input interface is automatically generated based on the first executor's input type. DevUI introspects the workflow and reflects the expected input schema, making it easy to test workflows with structured or custom input types. + +This dynamic input handling allows you to test your agents and workflows exactly as they would receive input in your application. + +## Installation + +Install DevUI from PyPI: + +```bash +pip install agent-framework-devui --pre +``` + +## Quick Start + +### Option 1: Programmatic Registration + +Launch DevUI with agents registered in-memory: + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient +from agent_framework.devui import serve + +def get_weather(location: str) -> str: + """Get weather for a location.""" + return f"Weather in {location}: 72F and sunny" + +# Create your agent +agent = ChatAgent( + name="WeatherAgent", + chat_client=OpenAIChatClient(), + tools=[get_weather] +) + +# Launch DevUI +serve(entities=[agent], auto_open=True) +# Opens browser to http://localhost:8080 +``` + +### Option 2: Directory Discovery (CLI) + +If you have agents and workflows organized in a directory structure, launch DevUI from the command line: + +```bash +# Launch web UI + API server +devui ./agents --port 8080 +# Web UI: http://localhost:8080 +# API: http://localhost:8080/v1/* +``` + +See [Directory Discovery](./directory-discovery.md) for details on the required directory structure. + +## Using the OpenAI SDK + +DevUI provides an OpenAI-compatible Responses API. You can use the OpenAI Python SDK to interact with your agents: + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8080/v1", + api_key="not-needed" # API key not required for local DevUI +) + +response = client.responses.create( + metadata={"entity_id": "weather_agent"}, # Your agent/workflow name + input="What's the weather in Seattle?" +) + +# Extract text from response +print(response.output[0].content[0].text) +``` + +For more details on the API, see [API Reference](./api-reference.md). + +## CLI Options + +```bash +devui [directory] [options] + +Options: + --port, -p Port (default: 8080) + --host Host (default: 127.0.0.1) + --headless API only, no UI + --no-open Don't automatically open browser + --tracing Enable OpenTelemetry tracing + --reload Enable auto-reload + --mode developer|user (default: developer) + --auth Enable Bearer token authentication + --auth-token Custom authentication token +``` + +::: zone-end + +## Next Steps + +- [Directory Discovery](./directory-discovery.md) - Learn how to structure your agents for automatic discovery +- [API Reference](./api-reference.md) - Explore the OpenAI-compatible API endpoints +- [Tracing & Observability](./tracing.md) - View OpenTelemetry traces in DevUI +- [Security & Deployment](./security.md) - Best practices for securing DevUI +- [Samples](./samples.md) - Browse sample agents and workflows diff --git a/agent-framework/user-guide/devui/resources/images/devui.png b/agent-framework/user-guide/devui/resources/images/devui.png new file mode 100644 index 00000000..0478f9fe Binary files /dev/null and b/agent-framework/user-guide/devui/resources/images/devui.png differ diff --git a/agent-framework/user-guide/devui/samples.md b/agent-framework/user-guide/devui/samples.md new file mode 100644 index 00000000..60b1d5bc --- /dev/null +++ b/agent-framework/user-guide/devui/samples.md @@ -0,0 +1,131 @@ +--- +title: DevUI Samples +description: Browse sample agents and workflows for use with DevUI. +author: moonbox3 +ms.topic: reference +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# Samples + +This page provides links to sample agents and workflows designed for use with DevUI. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI samples for C# are coming soon. Please check back later or refer to the Python samples for guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +## Getting Started Samples + +The Agent Framework repository includes sample agents and workflows in the `python/samples/getting_started/devui/` directory: + +| Sample | Description | +|--------|-------------| +| [weather_agent_azure](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/weather_agent_azure) | A weather agent using Azure OpenAI | +| [foundry_agent](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/foundry_agent) | Agent using Azure AI Foundry | +| [azure_responses_agent](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/azure_responses_agent) | Agent using Azure Responses API | +| [fanout_workflow](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/fanout_workflow) | Workflow demonstrating fan-out pattern | +| [spam_workflow](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/spam_workflow) | Workflow for spam detection | +| [workflow_agents](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/devui/workflow_agents) | Multiple agents in a workflow | + +## Running the Samples + +### Clone and Navigate + +```bash +git clone https://github.com/microsoft/agent-framework.git +cd agent-framework/python/samples/getting_started/devui +``` + +### Set Up Environment + +Each sample may require environment variables. Check for `.env.example` files: + +```bash +# Copy and edit the example file +cp weather_agent_azure/.env.example weather_agent_azure/.env +# Edit .env with your credentials +``` + +### Launch DevUI + +```bash +# Discover all samples +devui . + +# Or run a specific sample +devui ./weather_agent_azure +``` + +## In-Memory Mode + +The `in_memory_mode.py` script demonstrates running agents without directory discovery: + +```bash +python in_memory_mode.py +``` + +This opens the browser with pre-configured agents and a basic workflow, showing how to use `serve()` programmatically. + +## Sample Gallery + +When DevUI starts with no discovered entities, it displays a **sample gallery** with curated examples. From the gallery, you can: + +1. Browse available samples +2. View sample descriptions and requirements +3. Download samples to your local machine +4. Run samples directly + +## Creating Your Own Samples + +Follow the [Directory Discovery](./directory-discovery.md) guide to create your own agents and workflows compatible with DevUI. + +### Minimal Agent Template + +```python +# my_agent/__init__.py +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient + +agent = ChatAgent( + name="my_agent", + chat_client=OpenAIChatClient(), + instructions="You are a helpful assistant." +) +``` + +### Minimal Workflow Template + +```python +# my_workflow/__init__.py +from agent_framework.workflows import WorkflowBuilder + +# Define your workflow +workflow = ( + WorkflowBuilder() + # Add executors and edges + .build() +) +``` + +## Related Resources + +- [DevUI Package README](https://github.com/microsoft/agent-framework/tree/main/python/packages/devui) - Full package documentation +- [Agent Framework Samples](https://github.com/microsoft/agent-framework/tree/main/python/samples) - All Python samples +- [Workflow Samples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/workflows) - Workflow-specific samples + +::: zone-end + +## Next Steps + +- [Overview](./index.md) - Return to DevUI overview +- [Directory Discovery](./directory-discovery.md) - Learn about directory structure +- [API Reference](./api-reference.md) - Explore the API diff --git a/agent-framework/user-guide/devui/security.md b/agent-framework/user-guide/devui/security.md new file mode 100644 index 00000000..b634645b --- /dev/null +++ b/agent-framework/user-guide/devui/security.md @@ -0,0 +1,185 @@ +--- +title: DevUI Security & Deployment +description: Learn about security best practices and deployment options for DevUI. +author: moonbox3 +ms.topic: how-to +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# Security & Deployment + +DevUI is designed as a **sample application for local development**. This page covers security considerations and best practices if you need to expose DevUI beyond localhost. + +> [!WARNING] +> DevUI is not intended for production use. For production deployments, build your own custom interface using the Agent Framework SDK with appropriate security measures. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI documentation for C# is coming soon. Please check back later or refer to the Python documentation for conceptual guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +## UI Modes + +DevUI offers two modes that control access to features: + +### Developer Mode (Default) + +Full access to all features: + +- Debug panel with trace information +- Hot reload for rapid development (`/v1/entities/{id}/reload`) +- Deployment tools (`/v1/deployments`) +- Verbose error messages for debugging + +```bash +devui ./agents # Developer mode is the default +``` + +### User Mode + +Simplified, restricted interface: + +- Chat interface and conversation management +- Entity listing and basic info +- Developer APIs disabled (hot reload, deployment) +- Generic error messages (details logged server-side) + +```bash +devui ./agents --mode user +``` + +## Authentication + +Enable Bearer token authentication with the `--auth` flag: + +```bash +devui ./agents --auth +``` + +When authentication is enabled: +- For **localhost**: A token is auto-generated and displayed in the console +- For **network-exposed** deployments: You must provide a token via `DEVUI_AUTH_TOKEN` environment variable or `--auth-token` flag + +```bash +# Auto-generated token (localhost only) +devui ./agents --auth + +# Custom token via CLI +devui ./agents --auth --auth-token "your-secure-token" + +# Custom token via environment variable +export DEVUI_AUTH_TOKEN="your-secure-token" +devui ./agents --auth --host 0.0.0.0 +``` + +All API requests must include a valid Bearer token in the `Authorization` header: + +```bash +curl http://localhost:8080/v1/entities \ + -H "Authorization: Bearer your-token-here" +``` + +## Recommended Deployment Configuration + +If you need to expose DevUI to end users (not recommended for production): + +```bash +devui ./agents --mode user --auth --host 0.0.0.0 +``` + +This configuration: + +- Restricts developer-facing APIs +- Requires authentication +- Binds to all network interfaces + +## Security Features + +DevUI includes several security measures: + +| Feature | Description | +|---------|-------------| +| Localhost binding | Binds to 127.0.0.1 by default | +| User mode | Restricts developer APIs | +| Bearer authentication | Optional token-based auth | +| Local entity loading | Only loads entities from local directories or in-memory | +| No remote execution | No remote code execution capabilities | + +## Best Practices + +### Credentials Management + +- Store API keys and secrets in `.env` files +- Never commit `.env` files to source control +- Use `.env.example` files to document required variables + +```bash +# .env.example (safe to commit) +OPENAI_API_KEY=your-api-key-here +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ + +# .env (never commit) +OPENAI_API_KEY=sk-actual-key +AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com/ +``` + +### Network Security + +- Keep DevUI bound to localhost for development +- Use a reverse proxy (nginx, Caddy) if external access is needed +- Enable HTTPS through the reverse proxy +- Implement proper authentication at the proxy level + +### Entity Security + +- Review all agent/workflow code before running +- Only load entities from trusted sources +- Be cautious with tools that have side effects (file access, network calls) + +## Resource Cleanup + +Register cleanup hooks to properly close credentials and resources on shutdown: + +```python +from azure.identity.aio import DefaultAzureCredential +from agent_framework import ChatAgent +from agent_framework.azure import AzureOpenAIChatClient +from agent_framework_devui import register_cleanup, serve + +credential = DefaultAzureCredential() +client = AzureOpenAIChatClient() +agent = ChatAgent(name="MyAgent", chat_client=client) + +# Register cleanup hook - credential will be closed on shutdown +register_cleanup(agent, credential.close) +serve(entities=[agent]) +``` + +## MCP Tools Considerations + +When using MCP (Model Context Protocol) tools with DevUI: + +```python +# Correct - DevUI handles cleanup automatically +mcp_tool = MCPStreamableHTTPTool(url="http://localhost:8011/mcp", chat_client=chat_client) +agent = ChatAgent(tools=mcp_tool) +serve(entities=[agent]) +``` + +> [!IMPORTANT] +> Don't use `async with` context managers when creating agents with MCP tools for DevUI. Connections will close before execution. MCP tools use lazy initialization and connect automatically on first use. + +::: zone-end + +## Next Steps + +- [Samples](./samples.md) - Browse sample agents and workflows +- [API Reference](./api-reference.md) - Learn about the API endpoints diff --git a/agent-framework/user-guide/devui/tracing.md b/agent-framework/user-guide/devui/tracing.md new file mode 100644 index 00000000..d1164522 --- /dev/null +++ b/agent-framework/user-guide/devui/tracing.md @@ -0,0 +1,120 @@ +--- +title: DevUI Tracing & Observability +description: Learn how to view OpenTelemetry traces in DevUI for debugging and monitoring your agents. +author: moonbox3 +ms.topic: how-to +ms.author: evmattso +ms.date: 12/10/2025 +ms.service: agent-framework +zone_pivot_groups: programming-languages +--- + +# Tracing & Observability + +DevUI provides built-in support for capturing and displaying OpenTelemetry (OTel) traces emitted by the Agent Framework. DevUI does not create its own spans - it collects the spans that Agent Framework emits during agent and workflow execution, then displays them in the debug panel. This helps you debug agent behavior, understand execution flow, and identify performance issues. + +::: zone pivot="programming-language-csharp" + +## Coming Soon + +DevUI documentation for C# is coming soon. Please check back later or refer to the Python documentation for conceptual guidance. + +::: zone-end + +::: zone pivot="programming-language-python" + +## Enabling Tracing + +Enable tracing when starting DevUI with the `--tracing` flag: + +```bash +devui ./agents --tracing +``` + +This enables OpenTelemetry tracing for Agent Framework operations. + +## Viewing Traces in DevUI + +When tracing is enabled, the DevUI web interface displays trace information: + +1. Run an agent or workflow through the UI +2. Open the debug panel (available in developer mode) +3. View the trace timeline showing: + - Span hierarchy + - Timing information + - Agent/workflow events + - Tool calls and results + +## Trace Structure + +Agent Framework emits traces following OpenTelemetry semantic conventions for GenAI. A typical trace includes: + +``` +Agent Execution + LLM Call + Prompt + Response + Tool Call + Tool Execution + Tool Result + LLM Call + Prompt + Response +``` + +For workflows, traces show the execution path through executors: + +``` +Workflow Execution + Executor A + Agent Execution + ... + Executor B + Agent Execution + ... +``` + +## Programmatic Tracing + +When using DevUI programmatically with `serve()`, tracing can be enabled: + +```python +from agent_framework.devui import serve + +serve( + entities=[agent], + tracing_enabled=True +) +``` + +## Integration with External Tools + +DevUI captures and displays traces emitted by the Agent Framework - it does not create its own spans. These are standard OpenTelemetry traces that can also be exported to external observability tools like: + +- Jaeger +- Zipkin +- Azure Monitor +- Datadog + +To export traces to an external collector, set the `OTLP_ENDPOINT` environment variable: + +```bash +export OTLP_ENDPOINT="http://localhost:4317" +devui ./agents --tracing +``` + +Without an OTLP endpoint, traces are captured locally and displayed only in the DevUI debug panel. + +::: zone-end + +## Related Documentation + +For more details on Agent Framework observability: + +- [Agent Observability](../agents/agent-observability.md) - Comprehensive guide to agent tracing +- [Workflow Observability](../workflows/observability.md) - Workflow-specific tracing + +## Next Steps + +- [Security & Deployment](./security.md) - Secure your DevUI deployment +- [Samples](./samples.md) - Browse sample agents and workflows diff --git a/agent-framework/user-guide/hosting/TOC.yml b/agent-framework/user-guide/hosting/TOC.yml new file mode 100644 index 00000000..993772e3 --- /dev/null +++ b/agent-framework/user-guide/hosting/TOC.yml @@ -0,0 +1,6 @@ +- name: Overview + href: index.md +- name: A2A Integration + href: agent-to-agent-integration.md +- name: OpenAI Integration + href: openai-integration.md \ No newline at end of file diff --git a/agent-framework/user-guide/hosting/agent-to-agent-integration.md b/agent-framework/user-guide/hosting/agent-to-agent-integration.md new file mode 100644 index 00000000..d2a10044 --- /dev/null +++ b/agent-framework/user-guide/hosting/agent-to-agent-integration.md @@ -0,0 +1,256 @@ +--- +title: A2A Integration +description: Learn how to expose Microsoft Agent Framework agents using the Agent-to-Agent (A2A) protocol for inter-agent communication. +author: dmkorolev +ms.service: agent-framework +ms.topic: tutorial +ms.date: 11/11/2025 +ms.author: dmkorolev +--- + +# A2A Integration + +> [!NOTE] +> This tutorial describes A2A integration in .NET apps; Python integration is in the works... + +The Agent-to-Agent (A2A) protocol enables standardized communication between agents, allowing agents built with different frameworks and technologies to communicate seamlessly. The `Microsoft.Agents.AI.Hosting.A2A.AspNetCore` library provides ASP.NET Core integration for exposing your agents via the A2A protocol. + +**NuGet Packages:** +- [Microsoft.Agents.AI.Hosting.A2A](https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.A2A) +- [Microsoft.Agents.AI.Hosting.A2A.AspNetCore](https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.A2A.AspNetCore) + +## What is A2A? + +A2A is a standardized protocol that supports: + +- **Agent discovery** through agent cards +- **Message-based communication** between agents +- **Long-running agentic processes** via tasks +- **Cross-platform interoperability** between different agent frameworks + +For more information, see the [A2A protocol specification](https://a2a-protocol.org/latest/). + +## Example + +This minimal example shows how to expose an agent via A2A. The sample includes OpenAPI and Swagger dependencies to simplify testing. + +#### 1. Create an ASP.NET Core Web API project + +Create a new ASP.NET Core Web API project or use an existing one. + +#### 2. Install required dependencies + +Install the following packages: + + ## [.NET CLI](#tab/dotnet-cli) + + Run the following commands in your project directory to install the required NuGet packages: + + ```bash + # Hosting.A2A.AspNetCore for A2A protocol integration + dotnet add package Microsoft.Agents.AI.Hosting.A2A.AspNetCore --prerelease + + # Libraries to connect to Azure OpenAI + dotnet add package Azure.AI.OpenAI --prerelease + dotnet add package Azure.Identity + dotnet add package Microsoft.Extensions.AI + dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease + + # Swagger to test app + dotnet add package Microsoft.AspNetCore.OpenApi + dotnet add package Swashbuckle.AspNetCore + ``` + ## [Package Reference](#tab/package-reference) + + Add the following `` elements to your `.csproj` file within an ``: + + ```xml + + + + + + + + + + + + + + + ``` + + --- + + +#### 3. Configure Azure OpenAI connection + +The application requires an Azure OpenAI connection. Configure the endpoint and deployment name using `dotnet user-secrets` or environment variables. +You can also simply edit the `appsettings.json`, but that's not recommended for the apps deployed in production since some of the data can be considered to be secret. + + ## [User-Secrets](#tab/user-secrets) + ```bash + dotnet user-secrets set "AZURE_OPENAI_ENDPOINT" "https://.openai.azure.com/" + dotnet user-secrets set "AZURE_OPENAI_DEPLOYMENT_NAME" "gpt-4o-mini" + ``` + ## [ENV Windows](#tab/env-windows) + ```powershell + $env:AZURE_OPENAI_ENDPOINT = "https://.openai.azure.com/" + $env:AZURE_OPENAI_DEPLOYMENT_NAME = "gpt-4o-mini" + ``` + ## [ENV unix](#tab/env-unix) + ```bash + export AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/" + export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" + ``` + ## [appsettings](#tab/appsettings) + ```json + "AZURE_OPENAI_ENDPOINT": "https://.openai.azure.com/", + "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4o-mini" + ``` + + --- + + +#### 4. Add the code to Program.cs + +Replace the contents of `Program.cs` with the following code and run the application: +```csharp +using A2A.AspNetCore; +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Extensions.AI; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(); +builder.Services.AddSwaggerGen(); + +string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] + ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); + +// Register the chat client +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); +builder.Services.AddSingleton(chatClient); + +// Register an agent +var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate."); + +var app = builder.Build(); + +app.MapOpenApi(); +app.UseSwagger(); +app.UseSwaggerUI(); + +// Expose the agent via A2A protocol. You can also customize the agentCard +app.MapA2A(pirateAgent, path: "/a2a/pirate", agentCard: new() +{ + Name = "Pirate Agent", + Description = "An agent that speaks like a pirate.", + Version = "1.0" +}); + +app.Run(); +``` + +### Testing the Agent + +Once the application is running, you can test the A2A agent using the following `.http` file or through Swagger UI. + +The input format complies with the A2A specification. You can provide values for: +- `messageId` - A unique identifier for this specific message. You can create your own ID (e.g., a GUID) or set it to `null` to let the agent generate one automatically. +- `contextId` - The conversation identifier. Provide your own ID to start a new conversation or continue an existing one by reusing a previous `contextId`. The agent will maintain conversation history for the same `contextId`. Agent will generate one for you as well, if none is provided. + +```http +# Send A2A request to the pirate agent +POST {{baseAddress}}/a2a/pirate/v1/message:stream +Content-Type: application/json +{ + "message": { + "kind": "message", + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Hey pirate! Tell me where have you been", + "metadata": {} + } + ], + "messageId": null, + "contextId": "foo" + } +} +``` +_Note: Replace `{{baseAddress}}` with your server endpoint._ + +This request returns the following JSON response: +```json +{ + "kind": "message", + "role": "agent", + "parts": [ + { + "kind": "text", + "text": "Arrr, ye scallywag! Ye’ll have to tell me what yer after, or be I walkin’ the plank? 🏴‍☠️" + } + ], + "messageId": "chatcmpl-CXtJbisgIJCg36Z44U16etngjAKRk", + "contextId": "foo" +} +``` + +The response includes the `contextId` (conversation identifier), `messageId` (message identifier), and the actual content from the pirate agent. + +## AgentCard Configuration + +The `AgentCard` provides metadata about your agent for discovery and integration: +```csharp +app.MapA2A(agent, "/a2a/my-agent", agentCard: new() +{ + Name = "My Agent", + Description = "A helpful agent that assists with tasks.", + Version = "1.0", +}); +``` + +You can access the agent card by sending this request: +```http +# Send A2A request to the pirate agent +GET {{baseAddress}}/a2a/pirate/v1/card +``` +_Note: Replace `{{baseAddress}}` with your server endpoint._ + +### AgentCard Properties + +- **Name**: Display name of the agent +- **Description**: Brief description of the agent +- **Version**: Version string for the agent +- **Url**: Endpoint URL (automatically assigned if not specified) +- **Capabilities**: Optional metadata about streaming, push notifications, and other features + +## Exposing Multiple Agents + +You can expose multiple agents in a single application, as long as their endpoints don't collide. Here's an example: + +```csharp +var mathAgent = builder.AddAIAgent("math", instructions: "You are a math expert."); +var scienceAgent = builder.AddAIAgent("science", instructions: "You are a science expert."); + +app.MapA2A(mathAgent, "/a2a/math"); +app.MapA2A(scienceAgent, "/a2a/science"); +``` + +## See Also + +- [Hosting Overview](index.md) +- [OpenAI Integration](openai-integration.md) +- [A2A Protocol Specification](https://a2a-protocol.org/latest/) +- [Agent Discovery](https://github.com/a2aproject/A2A/blob/main/docs/topics/agent-discovery.md) diff --git a/agent-framework/user-guide/hosting/index.md b/agent-framework/user-guide/hosting/index.md new file mode 100644 index 00000000..0f1b68c9 --- /dev/null +++ b/agent-framework/user-guide/hosting/index.md @@ -0,0 +1,116 @@ +--- +title: Hosting Overview +description: Learn how to host AI agents in ASP.NET Core applications using the Agent Framework hosting libraries. +author: dmkorolev +ms.service: agent-framework +ms.topic: overview +ms.date: 11/11/2025 +ms.author: dmkorolev +--- + +# Hosting AI Agents in ASP.NET Core + +The Agent Framework provides a comprehensive set of hosting libraries that enable you to seamlessly integrate AI agents into ASP.NET Core applications. These libraries simplify the process of registering, configuring, and exposing agents through various protocols and interfaces. + +## Overview +As you may already know from the [AI Agents Overview](../../overview/agent-framework-overview.md#ai-agents), `AIAgent` is the fundamental concept of the Agent Framework. It defines an "LLM wrapper" that processes user inputs, makes decisions, calls tools, and performs additional work to execute actions and generate responses. + +However, exposing AI agents from your ASP.NET Core application is not trivial. The Agent Framework hosting libraries solve this by registering AI agents in a dependency injection container, allowing you to resolve and use them in your application services. Additionally, the hosting libraries enable you to manage agent dependencies, such as tools and thread storage, from the same dependency injection container. + +Agents can be hosted alongside your application infrastructure, independent of the protocols they use. Similarly, workflows can be hosted and leverage your application's common infrastructure. + +## Core Hosting Library + +The `Microsoft.Agents.AI.Hosting` library is the foundation for hosting AI agents in ASP.NET Core. It provides the primary APIs for agent registration and configuration. + +In the context of ASP.NET Core applications, `IHostApplicationBuilder` is the fundamental type that represents the builder for hosted applications and services. It manages configuration, logging, lifetime, and more. The Agent Framework hosting libraries provide extensions for `IHostApplicationBuilder` to register and configure AI agents and workflows. + +### Key APIs + +Before configuring agents or workflows, developer needs the `IChatClient` registered in the dependency injection container. +In the examples below, it is registered as keyed singleton under name `chat-model`. This is an example of `IChatClient` registration: +```csharp +// endpoint is of 'https://.openai.azure.com/' format +// deploymentName is `gpt-4o-mini` for example + +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); +builder.Services.AddSingleton(chatClient); +``` + +#### AddAIAgent + +Register an AI agent with dependency injection: + +```csharp +var pirateAgent = builder.AddAIAgent( + "pirate", + instructions: "You are a pirate. Speak like a pirate", + description: "An agent that speaks like a pirate.", + chatClientServiceKey: "chat-model"); +``` + +The `AddAIAgent()` method returns an `IHostedAgentBuilder`, which provides a set of extension methods for configuring the `AIAgent`. +For example, you can add tools to the agent: +```csharp +var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate") + .WithAITool(new MyTool()); // MyTool is a custom type derived from `AITool` +``` + +You can also configure the thread store (storage for conversation data): +```csharp +var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate") + .WithInMemoryThreadStore(); +``` + +#### AddWorkflow + +Register workflows that coordinate multiple agents. A workflow is essentially a "graph" where each node is an `AIAgent`, and the agents communicate with each other. + +In this example, we register two agents that work sequentially. The user input is first sent to `agent-1`, which produces a response and sends it to `agent-2`. The workflow then outputs the final response. There is also a `BuildConcurrent` method that creates a concurrent agent workflow. + +```csharp +builder.AddAIAgent("agent-1", instructions: "you are agent 1!"); +builder.AddAIAgent("agent-2", instructions: "you are agent 2!"); + +var workflow = builder.AddWorkflow("my-workflow", (sp, key) => +{ + var agent1 = sp.GetRequiredKeyedService("agent-1"); + var agent2 = sp.GetRequiredKeyedService("agent-2"); + return AgentWorkflowBuilder.BuildSequential(key, [agent1, agent2]); +}); +``` + +#### Expose Workflow as AIAgent + +`AIAgent`s benefit from integration APIs that expose them via well-known protocols (such as A2A, OpenAI, and others): +- [OpenAI Integration](openai-integration.md) - Expose agents via OpenAI-compatible APIs +- [A2A Integration](agent-to-agent-integration.md) - Enable agent-to-agent communication + +Currently, workflows do not provide similar integration capabilities. To use these integrations with a workflow, you can convert the workflow into a standalone agent that can be used like any other agent: + +```csharp +var workflowAsAgent = builder + .AddWorkflow("science-workflow", (sp, key) => { ... }) + .AddAsAIAgent(); // Now the workflow can be used as an agent +``` + +## Implementation Details + +The hosting libraries act as protocol adapters that bridge the gap between external communication protocols and the Agent Framework's internal `AIAgent` implementation. When you use a hosting integration library (such as OpenAI Responses or A2A), the library retrieves the registered `AIAgent` from dependency injection and wraps it with protocol-specific middleware. This middleware handles the translation of incoming requests from the external protocol format into Agent Framework models, invokes the `AIAgent` to process the request, and then translates the agent's response back into the protocol's expected output format. This architecture allows you to use public communication protocols seamlessly with `AIAgent` while keeping your agent implementation protocol-agnostic and focused on business logic. + +## Hosting Integration Libraries + +The Agent Framework includes specialized hosting libraries for different integration scenarios: + +- [OpenAI Integration](openai-integration.md) - Expose agents via OpenAI-compatible APIs +- [A2A Integration](agent-to-agent-integration.md) - Enable agent-to-agent communication + +## See Also + +- [AI Agents Overview](../../overview/agent-framework-overview.md) +- [Workflows](../../user-guide/workflows/overview.md) +- [Tools and Capabilities](../../tutorials/agents/function-tools.md) \ No newline at end of file diff --git a/agent-framework/user-guide/hosting/openai-integration.md b/agent-framework/user-guide/hosting/openai-integration.md new file mode 100644 index 00000000..8762bd92 --- /dev/null +++ b/agent-framework/user-guide/hosting/openai-integration.md @@ -0,0 +1,520 @@ +--- +title: OpenAI Integration +description: Learn how to expose Microsoft Agent Framework agents using OpenAI-compatible protocols including Chat Completions and Responses APIs. +author: dmkorolev +ms.service: agent-framework +ms.topic: tutorial +ms.date: 11/11/2025 +ms.author: dmkorolev +--- + +# OpenAI Integration + +> [!NOTE] +> This tutorial describes OpenAI integration in .NET apps; Integration for Python apps is in the works... + +The `Microsoft.Agents.AI.Hosting.OpenAI` library enables you to expose AI agents through OpenAI-compatible HTTP endpoints, supporting both the Chat Completions and Responses APIs. This allows you to integrate your agents with any OpenAI-compatible client or tool. + +**NuGet Package:** +- [Microsoft.Agents.AI.Hosting.OpenAI](https://www.nuget.org/packages/Microsoft.Agents.AI.Hosting.OpenAI) + +## What Are OpenAI Protocols? + +The hosting library supports two OpenAI protocols: + +- **Chat Completions API** - Standard stateless request/response format for chat interactions +- **Responses API** - Advanced format that supports conversations, streaming, and long-running agent processes + +## When to Use Each Protocol + +**The Responses API is now the default and recommended approach** according to OpenAI's documentation. It provides a more comprehensive and feature-rich interface for building AI applications with built-in conversation management, streaming capabilities, and support for long-running processes. + +Use the **Responses API** when: +- Building new applications (recommended default) +- You need server-side conversation management. However, that is not a requirement: you can still use Responses API in stateless mode. +- You want persistent conversation history +- You're building long-running agent processes +- You need advanced streaming capabilities with detailed event types +- You want to track and manage individual responses (e.g., retrieve a specific response by ID, check its status, or cancel a running response) + +Use the **Chat Completions API** when: +- Migrating existing applications that rely on the Chat Completions format +- You need simple, stateless request/response interactions +- State management is handled entirely by your client +- You're integrating with existing tools that only support Chat Completions +- You need maximum compatibility with legacy systems + +## Chat Completions API + +The Chat Completions API provides a simple, stateless interface for interacting with agents using the standard OpenAI chat format. + +### Setting up an agent in ASP.NET Core with ChatCompletions integration + +Here's a complete example exposing an agent via the Chat Completions API: + +#### Prerequisites + +#### 1. Create an ASP.NET Core Web API project + +Create a new ASP.NET Core Web API project or use an existing one. + +#### 2. Install required dependencies + +Install the following packages: + + ## [.NET CLI](#tab/dotnet-cli) + + Run the following commands in your project directory to install the required NuGet packages: + + ```bash + # Hosting.A2A.AspNetCore for OpenAI ChatCompletions/Responses protocol(s) integration + dotnet add package Microsoft.Agents.AI.Hosting.OpenAI --prerelease + + # Libraries to connect to Azure OpenAI + dotnet add package Azure.AI.OpenAI --prerelease + dotnet add package Azure.Identity + dotnet add package Microsoft.Extensions.AI + dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease + + # Swagger to test app + dotnet add package Microsoft.AspNetCore.OpenApi + dotnet add package Swashbuckle.AspNetCore + ``` + ## [Package Reference](#tab/package-reference) + + Add the following `` elements to your `.csproj` file within an ``: + + ```xml + + + + + + + + + + + + + + + + + ``` + + --- + + +#### 3. Configure Azure OpenAI connection + +The application requires an Azure OpenAI connection. Configure the endpoint and deployment name using `dotnet user-secrets` or environment variables. +You can also simply edit the `appsettings.json`, but that's not recommended for the apps deployed in production since some of the data can be considered to be secret. + + ## [User-Secrets](#tab/user-secrets) + ```bash + dotnet user-secrets set "AZURE_OPENAI_ENDPOINT" "https://.openai.azure.com/" + dotnet user-secrets set "AZURE_OPENAI_DEPLOYMENT_NAME" "gpt-4o-mini" + ``` + ## [ENV Windows](#tab/env-windows) + ```powershell + $env:AZURE_OPENAI_ENDPOINT = "https://.openai.azure.com/" + $env:AZURE_OPENAI_DEPLOYMENT_NAME = "gpt-4o-mini" + ``` + ## [ENV unix](#tab/env-unix) + ```bash + export AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/" + export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini" + ``` + ## [appsettings](#tab/appsettings) + ```json + "AZURE_OPENAI_ENDPOINT": "https://.openai.azure.com/", + "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-4o-mini" + ``` + + --- + + +#### 4. Add the code to Program.cs + +Replace the contents of `Program.cs` with the following code: + +```csharp +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Extensions.AI; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(); +builder.Services.AddSwaggerGen(); + +string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] + ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); + +// Register the chat client +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); +builder.Services.AddSingleton(chatClient); + +builder.AddOpenAIChatCompletions(); + +// Register an agent +var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate."); + +var app = builder.Build(); + +app.MapOpenApi(); +app.UseSwagger(); +app.UseSwaggerUI(); + +// Expose the agent via OpenAI ChatCompletions protocol +app.MapOpenAIChatCompletions(pirateAgent); + +app.Run(); +``` + +### Testing the Chat Completions Endpoint + +Once the application is running, you can test the agent using the OpenAI SDK or HTTP requests: + +#### Using HTTP Request + +```http +POST {{baseAddress}}/pirate/v1/chat/completions +Content-Type: application/json +{ + "model": "pirate", + "stream": false, + "messages": [ + { + "role": "user", + "content": "Hey mate!" + } + ] +} +``` +_Note: Replace `{{baseAddress}}` with your server endpoint._ + +Here is a sample response: +```json +{ + "id": "chatcmpl-nxAZsM6SNI2BRPMbzgjFyvWWULTFr", + "object": "chat.completion", + "created": 1762280028, + "model": "gpt-5", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Ahoy there, matey! How be ye farin' on this fine day?" + } + } + ], + "usage": { + "completion_tokens": 18, + "prompt_tokens": 22, + "total_tokens": 40, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 0 + } + }, + "service_tier": "default" +} +``` + +The response includes the message ID, content, and usage statistics. + +Chat Completions also supports **streaming**, where output is returned in chunks as soon as content is available. +This capability enables displaying output progressively. You can enable streaming by specifying `"stream": true`. +The output format consists of Server-Sent Events (SSE) chunks as defined in the OpenAI Chat Completions specification. + +```http +POST {{baseAddress}}/pirate/v1/chat/completions +Content-Type: application/json +{ + "model": "pirate", + "stream": true, + "messages": [ + { + "role": "user", + "content": "Hey mate!" + } + ] +} +``` + +And the output we get is a set of ChatCompletions chunks: +``` +data: {"id":"chatcmpl-xwKgBbFtSEQ3OtMf21ctMS2Q8lo93","choices":[],"object":"chat.completion.chunk","created":0,"model":"gpt-5"} + +data: {"id":"chatcmpl-xwKgBbFtSEQ3OtMf21ctMS2Q8lo93","choices":[{"index":0,"finish_reason":"stop","delta":{"content":"","role":"assistant"}}],"object":"chat.completion.chunk","created":0,"model":"gpt-5"} + +... + +data: {"id":"chatcmpl-xwKgBbFtSEQ3OtMf21ctMS2Q8lo93","choices":[],"object":"chat.completion.chunk","created":0,"model":"gpt-5","usage":{"completion_tokens":34,"prompt_tokens":23,"total_tokens":57,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0}}} +``` + +The streaming response contains similar information, but delivered as Server-Sent Events. + +## Responses API + +The Responses API provides advanced features including conversation management, streaming, and support for long-running agent processes. + +### Setting up an agent in ASP.NET Core with Responses API integration + +Here's a complete example using the Responses API: + +#### Prerequisites + +Follow the same prerequisites as the Chat Completions example (steps 1-3). + +#### 4. Add the code to Program.cs + +```csharp +using Azure.AI.OpenAI; +using Azure.Identity; +using Microsoft.Agents.AI.Hosting; +using Microsoft.Extensions.AI; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(); +builder.Services.AddSwaggerGen(); + +string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] + ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); +string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] + ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); + +// Register the chat client +IChatClient chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new DefaultAzureCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); +builder.Services.AddSingleton(chatClient); + +builder.AddOpenAIResponses(); +builder.AddOpenAIConversations(); + +// Register an agent +var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate."); + +var app = builder.Build(); + +app.MapOpenApi(); +app.UseSwagger(); +app.UseSwaggerUI(); + +// Expose the agent via OpenAI Responses protocol +app.MapOpenAIResponses(pirateAgent); +app.MapOpenAIConversations(); + +app.Run(); +``` + +### Testing the Responses API + +The Responses API is similar to Chat Completions but is stateful, allowing you to pass a `conversation` parameter. +Like Chat Completions, it supports the `stream` parameter, which controls the output format: either a single JSON response or a stream of events. +The Responses API defines its own streaming event types, including `response.created`, `response.output_item.added`, `response.output_item.done`, `response.completed`, and others. + +#### Create a Conversation and Response + +You can send a Responses request directly, or you can first create a conversation using the Conversations API +and then link subsequent requests to that conversation. + +To begin, create a new conversation: +```http +POST http://localhost:5209/v1/conversations +Content-Type: application/json +{ + "items": [ + { + "type": "message", + "role": "user", + "content": "Hello!" + } + ] +} +``` + +The response includes the conversation ID: +```json +{ + "id": "conv_E9Ma6nQpRzYxRHxRRqoOWWsDjZVyZfKxlHhfCf02Yxyy9N2y", + "object": "conversation", + "created_at": 1762881679, + "metadata": {} +} +``` + +Next, send a request and specify the conversation parameter. +_(To receive the response as streaming events, set `"stream": true` in the request.)_ +```http +POST http://localhost:5209/pirate/v1/responses +Content-Type: application/json +{ + "stream": false, + "conversation": "conv_E9Ma6nQpRzYxRHxRRqoOWWsDjZVyZfKxlHhfCf02Yxyy9N2y", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "are you a feminist?" + } + ] + } + ] +} +``` + +The agent returns the response and saves the conversation items to storage for later retrieval: +```json +{ + "id": "resp_FP01K4bnMsyQydQhUpovK6ysJJroZMs1pnYCUvEqCZqGCkac", + "conversation": "conv_E9Ma6nQpRzYxRHxRRqoOWWsDjZVyZfKxlHhfCf02Yxyy9N2y", + "object": "response", + "created_at": 1762881518, + "status": "completed", + "incomplete_details": null, + "output": [ + { + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Arrr, matey! As a pirate, I be all about respect for the crew, no matter their gender! We sail these seas together, and every hand on deck be valuable. A true buccaneer knows that fairness and equality be what keeps the ship afloat. So, in me own way, I’d say I be supportin’ all hearty souls who seek what be right! What say ye?" + } + ], + "type": "message", + "status": "completed", + "id": "msg_1FAQyZcWgsBdmgJgiXmDyavWimUs8irClHhfCf02Yxyy9N2y" + } + ], + "usage": { + "input_tokens": 26, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 85, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 111 + }, + "tool_choice": null, + "temperature": 1, + "top_p": 1 +} +``` + +The response includes conversation and message identifiers, content, and usage statistics. + +To retrieve the conversation items, send this request: +```http +GET http://localhost:5209/v1/conversations/conv_E9Ma6nQpRzYxRHxRRqoOWWsDjZVyZfKxlHhfCf02Yxyy9N2y/items?include=string +``` + +This returns a JSON response containing both input and output messages: +```JSON +{ + "object": "list", + "data": [ + { + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Arrr, matey! As a pirate, I be all about respect for the crew, no matter their gender! We sail these seas together, and every hand on deck be valuable. A true buccaneer knows that fairness and equality be what keeps the ship afloat. So, in me own way, I’d say I be supportin’ all hearty souls who seek what be right! What say ye?", + "annotations": [], + "logprobs": [] + } + ], + "type": "message", + "status": "completed", + "id": "msg_1FAQyZcWgsBdmgJgiXmDyavWimUs8irClHhfCf02Yxyy9N2y" + }, + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": "are you a feminist?" + } + ], + "type": "message", + "status": "completed", + "id": "msg_iLVtSEJL0Nd2b3ayr9sJWeV9VyEASMlilHhfCf02Yxyy9N2y" + } + ], + "first_id": "msg_1FAQyZcWgsBdmgJgiXmDyavWimUs8irClHhfCf02Yxyy9N2y", + "last_id": "msg_lUpquo0Hisvo6cLdFXMKdYACqFRWcFDrlHhfCf02Yxyy9N2y", + "has_more": false +} +``` + +## Exposing Multiple Agents + +You can expose multiple agents simultaneously using both protocols: + +```csharp +var mathAgent = builder.AddAIAgent("math", instructions: "You are a math expert."); +var scienceAgent = builder.AddAIAgent("science", instructions: "You are a science expert."); + +// Add both protocols +builder.AddOpenAIChatCompletions(); +builder.AddOpenAIResponses(); + +var app = builder.Build(); + +// Expose both agents via Chat Completions +app.MapOpenAIChatCompletions(mathAgent); +app.MapOpenAIChatCompletions(scienceAgent); + +// Expose both agents via Responses +app.MapOpenAIResponses(mathAgent); +app.MapOpenAIResponses(scienceAgent); +``` + +Agents will be available at: +- Chat Completions: `/math/v1/chat/completions` and `/science/v1/chat/completions` +- Responses: `/math/v1/responses` and `/science/v1/responses` + +## Custom Endpoints + +You can customize the endpoint paths: + +```csharp +// Custom path for Chat Completions +app.MapOpenAIChatCompletions(mathAgent, path: "/api/chat"); + +// Custom path for Responses +app.MapOpenAIResponses(scienceAgent, responsesPath: "/api/responses"); +``` + +## See Also + +- [Hosting Overview](index.md) +- [A2A Integration](agent-to-agent-integration.md) +- [OpenAI Chat Completions API Reference](https://platform.openai.com/docs/api-reference/chat) +- [OpenAI Responses API Reference](https://platform.openai.com/docs/api-reference/responses) diff --git a/agent-framework/user-guide/workflows/TOC.yml b/agent-framework/user-guide/workflows/TOC.yml index 4a6327fa..f7b2bbb6 100644 --- a/agent-framework/user-guide/workflows/TOC.yml +++ b/agent-framework/user-guide/workflows/TOC.yml @@ -16,5 +16,7 @@ href: checkpoints.md - name: Observability href: observability.md +- name: State Isolation + href: state-isolation.md - name: Visualization href: visualization.md \ No newline at end of file diff --git a/agent-framework/user-guide/workflows/as-agents.md b/agent-framework/user-guide/workflows/as-agents.md index 208df682..4cda31ed 100644 --- a/agent-framework/user-guide/workflows/as-agents.md +++ b/agent-framework/user-guide/workflows/as-agents.md @@ -69,7 +69,9 @@ async for update in workflow_agent.run_streaming(input, workflow_agent_thread): ## Next Steps -- [Learn how to use agents in workflows](./using-agents.md) to build intelligent workflows. - [Learn how to handle requests and responses](./requests-and-responses.md) in workflows. - [Learn how to manage state](./shared-states.md) in workflows. - [Learn how to create checkpoints and resume from them](./checkpoints.md). +- [Learn how to monitor workflows](./observability.md). +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/checkpoints.md b/agent-framework/user-guide/workflows/checkpoints.md index fd8e3490..a94e2dce 100644 --- a/agent-framework/user-guide/workflows/checkpoints.md +++ b/agent-framework/user-guide/workflows/checkpoints.md @@ -110,9 +110,9 @@ CheckpointInfo savedCheckpoint = checkpoints[5]; await checkpointedRun.RestoreCheckpointAsync(savedCheckpoint, CancellationToken.None).ConfigureAwait(false); await foreach (WorkflowEvent evt in checkpointedRun.Run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is WorkflowCompletedEvent workflowCompletedEvt) + if (evt is WorkflowOutputEvent workflowOutputEvt) { - Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}"); + Console.WriteLine($"Workflow completed with result: {workflowOutputEvt.Data}"); } } ``` @@ -146,9 +146,9 @@ Checkpointed newCheckpointedRun = await InProcessExecution .ConfigureAwait(false); await foreach (WorkflowEvent evt in newCheckpointedRun.Run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is WorkflowCompletedEvent workflowCompletedEvt) + if (evt is WorkflowOutputEvent workflowOutputEvt) { - Console.WriteLine($"Workflow completed with result: {workflowCompletedEvt.Data}"); + Console.WriteLine($"Workflow completed with result: {workflowOutputEvt.Data}"); } } ``` @@ -191,7 +191,7 @@ To ensure that the state of an executor is captured in a checkpoint, the executo using Microsoft.Agents.Workflows; using Microsoft.Agents.Workflows.Reflection; -internal sealed class CustomExecutor() : ReflectingExecutor("CustomExecutor"), IMessageHandler +internal sealed class CustomExecutor() : Executor("CustomExecutor") { private const string StateKey = "CustomExecutorState"; @@ -221,9 +221,36 @@ protected override async ValueTask OnCheckpointRestoredAsync(IWorkflowContext co ::: zone-end +::: zone pivot="programming-language-python" + +To ensure that the state of an executor is captured in a checkpoint, the executor must override the `on_checkpoint_save` method and save its state to the workflow context. + +```python +class CustomExecutor(Executor): + def __init__(self, id: str) -> None: + super().__init__(id=id) + self._messages: list[str] = [] + + @handler + async def handle(self, message: str, ctx: WorkflowContext): + self._messages.append(message) + # Executor logic... + + async def on_checkpoint_save(self) -> dict[str, Any]: + return {"messages": self._messages} +``` + +Also, to ensure the state is correctly restored when resuming from a checkpoint, the executor must override the `on_checkpoint_restore` method and load its state from the workflow context. + +```python +async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + self._messages = state.get("messages", []) +``` + +::: zone-end + ## Next Steps -- [Learn how to use agents in workflows](./using-agents.md) to build intelligent workflows. -- [Learn how to use workflows as agents](./as-agents.md). -- [Learn how to handle requests and responses](./requests-and-responses.md) in workflows. -- [Learn how to manage state](./shared-states.md) in workflows. +- [Learn how to monitor workflows](./observability.md). +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/core-concepts/edges.md b/agent-framework/user-guide/workflows/core-concepts/edges.md index 2d278df3..2b4736d1 100644 --- a/agent-framework/user-guide/workflows/core-concepts/edges.md +++ b/agent-framework/user-guide/workflows/core-concepts/edges.md @@ -154,10 +154,10 @@ Distribute messages from one executor to multiple targets: // Send to all targets builder.AddFanOutEdge(splitterExecutor, targets: [worker1, worker2, worker3]); -// Send to specific targets based on partitioner function +// Send to specific targets based on target selector function builder.AddFanOutEdge( source: routerExecutor, - partitioner: (message, targetCount) => message.Priority switch + targetSelector: (message, targetCount) => message.Priority switch { Priority.High => [0], // Route to first worker only Priority.Normal => [1, 2], // Route to workers 2 and 3 diff --git a/agent-framework/user-guide/workflows/core-concepts/events.md b/agent-framework/user-guide/workflows/core-concepts/events.md index 7bb18154..564a8bb0 100644 --- a/agent-framework/user-guide/workflows/core-concepts/events.md +++ b/agent-framework/user-guide/workflows/core-concepts/events.md @@ -52,10 +52,18 @@ RequestInfoEvent // A request is issued WorkflowStartedEvent # Workflow execution begins WorkflowOutputEvent # Workflow produces an output WorkflowErrorEvent # Workflow encounters an error +WorkflowWarningEvent # Workflow encountered a warning # Executor events -ExecutorInvokeEvent # Executor starts processing -ExecutorCompleteEvent # Executor finishes processing +ExecutorInvokedEvent # Executor starts processing +ExecutorCompletedEvent # Executor finishes processing +ExecutorFailedEvent # Executor encounters an error +AgentRunEvent # An agent run produces output +AgentRunUpdateEvent # An agent run produces a streaming update + +# Superstep events +SuperStepStartedEvent # Superstep begins +SuperStepCompletedEvent # Superstep completes # Request events RequestInfoEvent # A request is issued @@ -176,3 +184,6 @@ class CustomExecutor(Executor): - [Learn how to handle requests and responses](./../requests-and-responses.md) in workflows. - [Learn how to manage state](./../shared-states.md) in workflows. - [Learn how to create checkpoints and resume from them](./../checkpoints.md). +- [Learn how to monitor workflows](./../observability.md). +- [Learn about state isolation in workflows](./../state-isolation.md). +- [Learn how to visualize workflows](./../visualization.md). diff --git a/agent-framework/user-guide/workflows/core-concepts/executors.md b/agent-framework/user-guide/workflows/core-concepts/executors.md index 0571870f..158581a6 100644 --- a/agent-framework/user-guide/workflows/core-concepts/executors.md +++ b/agent-framework/user-guide/workflows/core-concepts/executors.md @@ -19,7 +19,7 @@ Executors are the fundamental building blocks that process messages in a workflo ::: zone pivot="programming-language-csharp" -Executors implement the `IMessageHandler` or `IMessageHandler` interfaces and inherit from the `ReflectingExecutor` base class. Each executor has a unique identifier and can handle specific message types. +Executors inherit from the `Executor` base class. Each executor has a unique identifier and can handle specific message types. ### Basic Executor Structure @@ -27,8 +27,7 @@ Executors implement the `IMessageHandler` or `IMessageHandler("UppercaseExecutor"), - IMessageHandler +internal sealed class UppercaseExecutor() : Executor("UppercaseExecutor") { public async ValueTask HandleAsync(string message, IWorkflowContext context) { @@ -41,8 +40,7 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor("UppercaseExecutor"), - IMessageHandler +internal sealed class UppercaseExecutor() : Executor("UppercaseExecutor") { public async ValueTask HandleAsync(string message, IWorkflowContext context) { @@ -52,16 +50,22 @@ internal sealed class UppercaseExecutor() : ReflectingExecutor("SampleExecutor"), - IMessageHandler, IMessageHandler +internal sealed class SampleExecutor() : Executor("SampleExecutor") { + protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) + { + return routeBuilder + .AddHandler(this.HandleStringAsync) + .AddHandler(this.HandleIntAsync); + } + /// /// Converts input string to uppercase /// - public async ValueTask HandleAsync(string message, IWorkflowContext context) + public async ValueTask HandleStringAsync(string message, IWorkflowContext context) { string result = message.ToUpperInvariant(); return result; @@ -70,7 +74,7 @@ internal sealed class SampleExecutor() : ReflectingExecutor("Sam /// /// Doubles the input integer /// - public async ValueTask HandleAsync(int message, IWorkflowContext context) + public async ValueTask HandleIntAsync(int message, IWorkflowContext context) { int result = message * 2; return result; @@ -78,6 +82,13 @@ internal sealed class SampleExecutor() : ReflectingExecutor("Sam } ``` +It is also possible to create an executor from a function by using the `BindExecutor` extension method: + +```csharp +Func uppercaseFunc = s => s.ToUpperInvariant(); +var uppercase = uppercaseFunc.BindExecutor("UppercaseExecutor"); +``` + ::: zone-end ::: zone pivot="programming-language-python" diff --git a/agent-framework/user-guide/workflows/core-concepts/workflows.md b/agent-framework/user-guide/workflows/core-concepts/workflows.md index b378ab80..8e270d64 100644 --- a/agent-framework/user-guide/workflows/core-concepts/workflows.md +++ b/agent-framework/user-guide/workflows/core-concepts/workflows.md @@ -79,9 +79,9 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync()) Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}"); } - if (evt is WorkflowCompletedEvent completed) + if (evt is WorkflowOutputEvent outputEvt) { - Console.WriteLine($"Workflow completed: {completed.Data}"); + Console.WriteLine($"Workflow completed: {outputEvt.Data}"); } } @@ -89,9 +89,9 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync()) Run result = await InProcessExecution.RunAsync(workflow, inputMessage); foreach (WorkflowEvent evt in result.NewEvents) { - if (evt is WorkflowCompletedEvent completed) + if (evt is WorkflowOutputEvent outputEvt) { - Console.WriteLine($"Final result: {completed.Data}"); + Console.WriteLine($"Final result: {outputEvt.Data}"); } } ``` @@ -101,16 +101,16 @@ foreach (WorkflowEvent evt in result.NewEvents) ::: zone pivot="programming-language-python" ```python -from agent_framework import WorkflowCompletedEvent +from agent_framework import WorkflowOutputEvent # Streaming execution - get events as they happen async for event in workflow.run_stream(input_message): - if isinstance(event, WorkflowCompletedEvent): + if isinstance(event, WorkflowOutputEvent): print(f"Workflow completed: {event.data}") # Non-streaming execution - wait for completion events = await workflow.run(input_message) -print(f"Final result: {events.get_completed_event()}") +print(f"Final result: {events.get_outputs()}") ``` ::: zone-end diff --git a/agent-framework/user-guide/workflows/observability.md b/agent-framework/user-guide/workflows/observability.md index 4f173118..b064f682 100644 --- a/agent-framework/user-guide/workflows/observability.md +++ b/agent-framework/user-guide/workflows/observability.md @@ -1,6 +1,7 @@ --- title: Microsoft Agent Framework Workflows - Observability description: In-depth look at Observability in Microsoft Agent Framework Workflows. +zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial ms.author: taochen @@ -12,24 +13,24 @@ ms.service: agent-framework Observability provides insights into the internal state and behavior of workflows during execution. This includes logging, metrics, and tracing capabilities that help monitor and debug workflows. +> [!TIP] +> Observability is a framework-wide feature and is not limited to workflows. For more information, refer to [Agent Observability](../agents/agent-observability.md). + Aside from the standard [GenAI telemetry](https://opentelemetry.io/docs/specs/semconv/gen-ai/), Agent Framework Workflows emits additional spans, logs, and metrics to provide deeper insights into workflow execution. These observability features help developers understand the flow of messages, the performance of executors, and any errors that may occur. ## Enable Observability -Observability is enabled framework-wide by setting the `ENABLE_OTEL=true` environment variable or calling `setup_observability()` at the beginning of your application. +::: zone pivot="programming-language-csharp" + +Please refer to [Enabling Observability](../agents/agent-observability.md#enable-observability-c) for instructions on enabling observability in your applications. + +::: zone-end -```env -# This is not required if you run `setup_observability()` in your code -ENABLE_OTEL=true -# Sensitive data (e.g., message content) will be included in logs and traces if this is set to true -ENABLE_SENSITIVE_DATA=true -``` +::: zone pivot="programming-language-python" -```python -from agent_framework.observability import setup_observability +Please refer to [Enabling Observability](../agents/agent-observability.md#enable-observability-python) for instructions on enabling observability in your applications. -setup_observability(enable_sensitive_data=True) -``` +::: zone-end ## Workflow Spans @@ -51,7 +52,5 @@ For example: ## Next Steps -- [Learn how to use agents in workflows](./using-agents.md) to build intelligent workflows. -- [Learn how to handle requests and responses](./requests-and-responses.md) in workflows. -- [Learn how to manage state](./shared-states.md) in workflows. -- [Learn how to create checkpoints and resume from them](./checkpoints.md). \ No newline at end of file +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/orchestrations/concurrent.md b/agent-framework/user-guide/workflows/orchestrations/concurrent.md index 66f26ebd..2dfb117e 100644 --- a/agent-framework/user-guide/workflows/orchestrations/concurrent.md +++ b/agent-framework/user-guide/workflows/orchestrations/concurrent.md @@ -91,9 +91,9 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } - else if (evt is WorkflowCompletedEvent completed) + else if (evt is WorkflowOutputEvent outputEvt) { - result = (List)completed.Data!; + result = (List)outputEvt.Data!; break; } } @@ -180,17 +180,17 @@ workflow = ConcurrentBuilder().participants([researcher, marketer, legal]).build ## Run the Concurrent Workflow and Collect the Results ```python -from agent_framework import ChatMessage, WorkflowCompletedEvent +from agent_framework import ChatMessage, WorkflowOutputEvent # 3) Run with a single prompt, stream progress, and pretty-print the final combined messages -completion: WorkflowCompletedEvent | None = None +output_evt: WorkflowOutputEvent | None = None async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."): - if isinstance(event, WorkflowCompletedEvent): - completion = event + if isinstance(event, WorkflowOutputEvent): + output_evt = event -if completion: +if output_evt: print("===== Final Aggregated Conversation (messages) =====") - messages: list[ChatMessage] | Any = completion.data + messages: list[ChatMessage] | Any = output_evt.data for i, msg in enumerate(messages, start=1): name = msg.author_name if msg.author_name else "user" print(f"{'-' * 60}\n\n{i:02d} [{name}]:\n{msg.text}") @@ -362,14 +362,14 @@ workflow = ( .build() ) -completion: WorkflowCompletedEvent | None = None +output_evt: WorkflowOutputEvent | None = None async for event in workflow.run_stream("We are launching a new budget-friendly electric bike for urban commuters."): - if isinstance(event, WorkflowCompletedEvent): - completion = event + if isinstance(event, WorkflowOutputEvent): + output_evt = event -if completion: +if output_evt: print("===== Final Consolidated Output =====") - print(completion.data) + print(output_evt.data) ``` ### Sample Output with Custom Aggregator diff --git a/agent-framework/user-guide/workflows/orchestrations/group-chat.md b/agent-framework/user-guide/workflows/orchestrations/group-chat.md index e64d071d..0fc22ee6 100644 --- a/agent-framework/user-guide/workflows/orchestrations/group-chat.md +++ b/agent-framework/user-guide/workflows/orchestrations/group-chat.md @@ -213,25 +213,41 @@ def select_next_speaker(state: GroupChatStateSnapshot) -> str | None: # Build the group chat workflow workflow = ( GroupChatBuilder() - .select_speakers(select_next_speaker, display_name="Orchestrator") + .set_select_speakers_func(select_next_speaker, display_name="Orchestrator") .participants([researcher, writer]) .build() ) ``` -## Configure Group Chat with Prompt-Based Manager +## Configure Group Chat with Agent-Based Manager -Alternatively, use an AI-powered manager for dynamic speaker selection: +Alternatively, use an agent-based manager for intelligent speaker selection. The manager is a full `ChatAgent` with access to tools, context, and observability: ```python -# Build group chat with prompt-based manager +# Create coordinator agent for speaker selection +coordinator = ChatAgent( + name="Coordinator", + description="Coordinates multi-agent collaboration by selecting speakers", + instructions=""" +You coordinate a team conversation to solve the user's task. + +Review the conversation history and select the next participant to speak. + +Guidelines: +- Start with Researcher to gather information +- Then have Writer synthesize the final answer +- Only finish after both have contributed meaningfully +- Allow for multiple rounds of information gathering if needed +""", + chat_client=chat_client, +) + +# Build group chat with agent-based manager workflow = ( GroupChatBuilder() - .set_prompt_based_manager( - chat_client=chat_client, - display_name="Coordinator" - ) - .participants(researcher=researcher, writer=writer) + .set_manager(coordinator, display_name="Orchestrator") + .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == Role.ASSISTANT) >= 4) + .participants([researcher, writer]) .build() ) ``` @@ -241,24 +257,39 @@ workflow = ( Execute the workflow and process events: ```python -from agent_framework import AgentRunUpdateEvent, WorkflowOutputEvent +from typing import cast +from agent_framework import AgentRunUpdateEvent, Role, WorkflowOutputEvent task = "What are the key benefits of async/await in Python?" print(f"Task: {task}\n") print("=" * 80) +final_conversation: list[ChatMessage] = [] +last_executor_id: str | None = None + # Run the workflow async for event in workflow.run_stream(task): if isinstance(event, AgentRunUpdateEvent): # Print streaming agent updates - print(f"[{event.executor_id}]: {event.data}", end="", flush=True) + eid = event.executor_id + if eid != last_executor_id: + if last_executor_id is not None: + print() + print(f"[{eid}]:", end=" ", flush=True) + last_executor_id = eid + print(event.data, end="", flush=True) elif isinstance(event, WorkflowOutputEvent): - # Workflow completed - final_message = event.data - author = getattr(final_message, "author_name", "System") - text = getattr(final_message, "text", str(final_message)) - print(f"\n\n[{author}]\n{text}") + # Workflow completed - data is a list of ChatMessage + final_conversation = cast(list[ChatMessage], event.data) + +if final_conversation: + print("\n\n" + "=" * 80) + print("Final Conversation:") + for msg in final_conversation: + author = getattr(msg, "author_name", "Unknown") + text = getattr(msg, "text", str(msg)) + print(f"\n[{author}]\n{text}") print("-" * 80) print("\nWorkflow completed.") @@ -314,13 +345,14 @@ Workflow completed. ::: zone pivot="programming-language-python" -- **Flexible Manager Strategies**: Choose between simple selectors, prompt-based managers, or custom logic +- **Flexible Manager Strategies**: Choose between simple selectors, agent-based managers, or custom logic - **GroupChatBuilder**: Creates workflows with configurable speaker selection -- **select_speakers()**: Define custom Python functions for speaker selection -- **set_prompt_based_manager()**: Use AI-powered coordination for dynamic speaker selection +- **set_select_speakers_func()**: Define custom Python functions for speaker selection +- **set_manager()**: Use an agent-based manager for intelligent speaker coordination - **GroupChatStateSnapshot**: Provides conversation state for selection decisions - **Iterative Collaboration**: Agents build upon each other's contributions -- **Event Streaming**: Process agent updates and outputs in real-time +- **Event Streaming**: Process `AgentRunUpdateEvent` and `WorkflowOutputEvent` in real-time +- **list[ChatMessage] Output**: All orchestrations return a list of chat messages ::: zone-end @@ -403,7 +435,7 @@ def smart_selector(state: GroupChatStateSnapshot) -> str | None: workflow = ( GroupChatBuilder() - .select_speakers(smart_selector, display_name="SmartOrchestrator") + .set_select_speakers_func(smart_selector, display_name="SmartOrchestrator") .participants([researcher, writer]) .build() ) diff --git a/agent-framework/user-guide/workflows/orchestrations/handoff.md b/agent-framework/user-guide/workflows/orchestrations/handoff.md index a2d309c9..c317d8f8 100644 --- a/agent-framework/user-guide/workflows/orchestrations/handoff.md +++ b/agent-framework/user-guide/workflows/orchestrations/handoff.md @@ -29,6 +29,8 @@ While agent-as-tools is commonly considered as a multi-agent pattern and it may - How to configure handoff rules between agents - How to build interactive workflows with dynamic agent routing - How to handle multi-turn conversations with agent switching +- How to implement tool approval for sensitive operations (HITL) +- How to use checkpointing for durable handoff workflows In handoff orchestration, agents can transfer control to one another based on context, allowing for dynamic routing and specialized expertise handling. @@ -115,9 +117,9 @@ while (true) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } - else if (evt is WorkflowCompletedEvent completed) + else if (evt is WorkflowOutputEvent outputEvt) { - newMessages = (List)completed.Data!; + newMessages = (List)outputEvt.Data!; break; } } @@ -279,6 +281,172 @@ while pending_requests: print(f"{msg.author_name}: {msg.text}") ``` +## Advanced: Tool Approval in Handoff Workflows + +Handoff workflows can include agents with tools that require human approval before execution. This is useful for sensitive operations like processing refunds, making purchases, or executing irreversible actions. + +### Define Tools with Approval Required + +```python +from typing import Annotated +from agent_framework import ai_function + +@ai_function(approval_mode="always_require") +def submit_refund( + refund_description: Annotated[str, "Description of the refund reason"], + amount: Annotated[str, "Refund amount"], + order_id: Annotated[str, "Order ID for the refund"], +) -> str: + """Submit a refund request for manual review before processing.""" + return f"Refund recorded for order {order_id} (amount: {amount}): {refund_description}" +``` + +### Create Agents with Approval-Required Tools + +```python +from agent_framework import ChatAgent +from agent_framework.azure import AzureOpenAIChatClient +from azure.identity import AzureCliCredential + +client = AzureOpenAIChatClient(credential=AzureCliCredential()) + +triage_agent = client.create_agent( + name="triage_agent", + instructions=( + "You are a customer service triage agent. Listen to customer issues and determine " + "if they need refund help or order tracking. Use handoff_to_refund_agent or " + "handoff_to_order_agent to transfer them." + ), +) + +refund_agent = client.create_agent( + name="refund_agent", + instructions=( + "You are a refund specialist. Help customers with refund requests. " + "When the user confirms they want a refund and supplies order details, " + "call submit_refund to record the request." + ), + tools=[submit_refund], +) + +order_agent = client.create_agent( + name="order_agent", + instructions="You are an order tracking specialist. Help customers track their orders.", +) +``` + +### Handle Both User Input and Tool Approval Requests + +```python +from agent_framework import ( + FunctionApprovalRequestContent, + HandoffBuilder, + HandoffUserInputRequest, + RequestInfoEvent, + WorkflowOutputEvent, +) + +workflow = ( + HandoffBuilder( + name="support_with_approvals", + participants=[triage_agent, refund_agent, order_agent], + ) + .set_coordinator("triage_agent") + .build() +) + +pending_requests: list[RequestInfoEvent] = [] + +# Start workflow +async for event in workflow.run_stream("My order 12345 arrived damaged. I need a refund."): + if isinstance(event, RequestInfoEvent): + pending_requests.append(event) + +# Process pending requests - could be user input OR tool approval +while pending_requests: + responses: dict[str, object] = {} + + for request in pending_requests: + if isinstance(request.data, HandoffUserInputRequest): + # Agent needs user input + print(f"Agent {request.data.awaiting_agent_id} asks:") + for msg in request.data.conversation[-2:]: + print(f" {msg.author_name}: {msg.text}") + + user_input = input("You: ") + responses[request.request_id] = user_input + + elif isinstance(request.data, FunctionApprovalRequestContent): + # Agent wants to call a tool that requires approval + func_call = request.data.function_call + args = func_call.parse_arguments() or {} + + print(f"\nTool approval requested: {func_call.name}") + print(f"Arguments: {args}") + + approval = input("Approve? (y/n): ").strip().lower() == "y" + responses[request.request_id] = request.data.create_response(approved=approval) + + # Send all responses and collect new requests + pending_requests = [] + async for event in workflow.send_responses_streaming(responses): + if isinstance(event, RequestInfoEvent): + pending_requests.append(event) + elif isinstance(event, WorkflowOutputEvent): + print("\nWorkflow completed!") +``` + +### With Checkpointing for Durable Workflows + +For long-running workflows where tool approvals may happen hours or days later, use checkpointing: + +```python +from agent_framework import FileCheckpointStorage + +storage = FileCheckpointStorage(storage_path="./checkpoints") + +workflow = ( + HandoffBuilder( + name="durable_support", + participants=[triage_agent, refund_agent, order_agent], + ) + .set_coordinator("triage_agent") + .with_checkpointing(storage) + .build() +) + +# Initial run - workflow pauses when approval is needed +pending_requests = [] +async for event in workflow.run_stream("I need a refund for order 12345"): + if isinstance(event, RequestInfoEvent): + pending_requests.append(event) + +# Process can exit here - checkpoint is saved automatically + +# Later: Resume from checkpoint and provide approval +checkpoints = await storage.list_checkpoints() +latest = sorted(checkpoints, key=lambda c: c.timestamp, reverse=True)[0] + +# Step 1: Restore checkpoint to reload pending requests +restored_requests = [] +async for event in workflow.run_stream(checkpoint_id=latest.checkpoint_id): + if isinstance(event, RequestInfoEvent): + restored_requests.append(event) + +# Step 2: Send responses +responses = {} +for req in restored_requests: + if isinstance(req.data, FunctionApprovalRequestContent): + responses[req.request_id] = req.data.create_response(approved=True) + elif isinstance(req.data, HandoffUserInputRequest): + responses[req.request_id] = "Yes, please process the refund." + +async for event in workflow.send_responses_streaming(responses): + if isinstance(event, WorkflowOutputEvent): + print("Refund workflow completed!") +``` +``` + ## Sample Interaction ```plaintext @@ -331,6 +499,9 @@ Could you provide photos of the damage to expedite the process? - **enable_return_to_previous()**: Routes user inputs directly to the current specialist, skipping coordinator re-evaluation - **Context Preservation**: Full conversation history is maintained across all handoffs - **Request/Response Cycle**: Workflow requests user input, processes responses, and continues until termination condition is met +- **Tool Approval**: Use `@ai_function(approval_mode="always_require")` for sensitive operations that need human approval +- **FunctionApprovalRequestContent**: Emitted when an agent calls a tool requiring approval; use `create_response(approved=...)` to respond +- **Checkpointing**: Use `with_checkpointing()` for durable workflows that can pause and resume across process restarts - **Specialized Expertise**: Each agent focuses on their domain while collaborating through handoffs ::: zone-end diff --git a/agent-framework/user-guide/workflows/orchestrations/magentic.md b/agent-framework/user-guide/workflows/orchestrations/magentic.md index 138e2d62..58206a12 100644 --- a/agent-framework/user-guide/workflows/orchestrations/magentic.md +++ b/agent-framework/user-guide/workflows/orchestrations/magentic.md @@ -20,8 +20,8 @@ The Magentic manager maintains a shared context, tracks progress, and adapts the ## What You'll Learn - How to set up a Magentic manager to coordinate multiple specialized agents -- How to configure callbacks for streaming and event handling -- How to implement human-in-the-loop plan review +- How to handle streaming events with `AgentRunUpdateEvent` +- How to implement human-in-the-loop plan review, tool approval, and stall intervention - How to track agent collaboration and progress through complex tasks ## Define Your Specialized Agents @@ -47,7 +47,7 @@ researcher_agent = ChatAgent( "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 - chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"), + chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), ) coder_agent = ChatAgent( @@ -57,46 +57,14 @@ coder_agent = ChatAgent( chat_client=OpenAIResponsesClient(), tools=HostedCodeInterpreterTool(), ) -``` - -## Set Up Event Callbacks - -Magentic orchestration provides rich event callbacks to monitor the workflow progress in real-time: -```python -from agent_framework import ( - MagenticAgentDeltaEvent, - MagenticAgentMessageEvent, - MagenticCallbackEvent, - MagenticFinalResultEvent, - MagenticOrchestratorMessageEvent, +# Create a manager agent for orchestration +manager_agent = ChatAgent( + name="MagenticManager", + description="Orchestrator that coordinates the research and coding workflow", + instructions="You coordinate a team to complete complex tasks efficiently.", + chat_client=OpenAIChatClient(), ) - -# Unified callback for all events -async def on_event(event: MagenticCallbackEvent) -> None: - if isinstance(event, MagenticOrchestratorMessageEvent): - # Manager's planning and coordination messages - print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}") - - elif isinstance(event, MagenticAgentDeltaEvent): - # Streaming tokens from agents - print(event.text, end="", flush=True) - - elif isinstance(event, MagenticAgentMessageEvent): - # Complete agent responses - msg = event.message - if msg is not None: - response_text = (msg.text or "").replace("\n", " ") - print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}") - - elif isinstance(event, MagenticFinalResultEvent): - # Final synthesized result - print("\n" + "=" * 50) - print("FINAL RESULT:") - print("=" * 50) - if event.message is not None: - print(event.message.text) - print("=" * 50) ``` ## Build the Magentic Workflow @@ -104,14 +72,13 @@ async def on_event(event: MagenticCallbackEvent) -> None: Use `MagenticBuilder` to configure the workflow with a standard manager: ```python -from agent_framework import MagenticBuilder, MagenticCallbackMode +from agent_framework import MagenticBuilder workflow = ( MagenticBuilder() .participants(researcher=researcher_agent, coder=coder_agent) - .on_event(on_event, mode=MagenticCallbackMode.STREAMING) .with_standard_manager( - chat_client=OpenAIChatClient(), + agent=manager_agent, max_round_count=10, # Maximum collaboration rounds max_stall_count=3, # Maximum rounds without progress max_reset_count=2, # Maximum plan resets allowed @@ -120,12 +87,19 @@ workflow = ( ) ``` -## Run the Workflow +## Run the Workflow with Event Streaming -Execute a complex task that requires multiple agents working together: +Execute a complex task and handle events for streaming output and orchestration updates: ```python -from agent_framework import WorkflowCompletedEvent +from typing import cast +from agent_framework import ( + MAGENTIC_EVENT_TYPE_AGENT_DELTA, + MAGENTIC_EVENT_TYPE_ORCHESTRATOR, + AgentRunUpdateEvent, + ChatMessage, + WorkflowOutputEvent, +) task = ( "I am preparing a report on the energy efficiency of different machine learning model architectures. " @@ -136,15 +110,47 @@ task = ( "per task type (image classification, text classification, and text generation)." ) -completion_event = None +# State for streaming callback +last_stream_agent_id: str | None = None +stream_line_open: bool = False +output: str | None = None + async for event in workflow.run_stream(task): - if isinstance(event, WorkflowCompletedEvent): - completion_event = event + if isinstance(event, AgentRunUpdateEvent): + props = event.data.additional_properties if event.data else None + event_type = props.get("magentic_event_type") if props else None + + if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR: + # Manager's planning and coordination messages + kind = props.get("orchestrator_message_kind", "") if props else "" + text = event.data.text if event.data else "" + print(f"\n[ORCH:{kind}]\n\n{text}\n{'-' * 26}") + + elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA: + # Streaming tokens from agents + agent_id = props.get("agent_id", event.executor_id) if props else event.executor_id + if last_stream_agent_id != agent_id or not stream_line_open: + if stream_line_open: + print() + print(f"\n[STREAM:{agent_id}]: ", end="", flush=True) + last_stream_agent_id = agent_id + stream_line_open = True + if event.data and event.data.text: + print(event.data.text, end="", flush=True) + + elif event.data and event.data.text: + print(event.data.text, end="", flush=True) -if completion_event is not None: - data = getattr(completion_event, "data", None) - preview = getattr(data, "text", None) or (str(data) if data is not None else "") - print(f"Workflow completed with result:\n\n{preview}") + elif isinstance(event, WorkflowOutputEvent): + output_messages = cast(list[ChatMessage], event.data) + if output_messages: + output = output_messages[-1].text + +if stream_line_open: + print() + +if output is not None: + print(f"Workflow completed with result:\n\n{output}") ``` ## Advanced: Human-in-the-Loop Plan Review @@ -154,19 +160,24 @@ Enable human review and approval of the manager's plan before execution: ### Configure Plan Review ```python +from typing import cast from agent_framework import ( - MagenticPlanReviewDecision, - MagenticPlanReviewReply, - MagenticPlanReviewRequest, + MAGENTIC_EVENT_TYPE_AGENT_DELTA, + MAGENTIC_EVENT_TYPE_ORCHESTRATOR, + AgentRunUpdateEvent, + MagenticHumanInterventionDecision, + MagenticHumanInterventionKind, + MagenticHumanInterventionReply, + MagenticHumanInterventionRequest, RequestInfoEvent, + WorkflowOutputEvent, ) workflow = ( MagenticBuilder() .participants(researcher=researcher_agent, coder=coder_agent) - .on_event(on_event, mode=MagenticCallbackMode.STREAMING) .with_standard_manager( - chat_client=OpenAIChatClient(), + agent=manager_agent, max_round_count=10, max_stall_count=3, max_reset_count=2, @@ -179,91 +190,187 @@ workflow = ( ### Handle Plan Review Requests ```python -completion_event: WorkflowCompletedEvent | None = None pending_request: RequestInfoEvent | None = None - -while True: - # Run until completion or review request - if pending_request is None: - async for event in workflow.run_stream(task): - if isinstance(event, WorkflowCompletedEvent): - completion_event = event - - if isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest: +pending_responses: dict[str, MagenticHumanInterventionReply] | None = None +completed = False +workflow_output: str | None = None + +while not completed: + # Use streaming for both initial run and response sending + if pending_responses is not None: + stream = workflow.send_responses_streaming(pending_responses) + else: + stream = workflow.run_stream(task) + + async for event in stream: + if isinstance(event, AgentRunUpdateEvent): + # Handle streaming events as shown above + pass + elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest: + request = cast(MagenticHumanInterventionRequest, event.data) + if request.kind == MagenticHumanInterventionKind.PLAN_REVIEW: pending_request = event - review_req = cast(MagenticPlanReviewRequest, event.data) - if review_req.plan_text: - print(f"\n=== PLAN REVIEW REQUEST ===\n{review_req.plan_text}\n") + if request.plan_text: + print(f"\n=== PLAN REVIEW REQUEST ===\n{request.plan_text}\n") + elif isinstance(event, WorkflowOutputEvent): + workflow_output = str(event.data) if event.data else None + completed = True - # Check if completed - if completion_event is not None: - break + pending_responses = None - # Respond to plan review + # Handle pending plan review request if pending_request is not None: # Collect human decision (approve/reject/modify) # For demo, we auto-approve: - reply = MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE) + reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE) - # Or modify the plan: - # reply = MagenticPlanReviewReply( - # decision=MagenticPlanReviewDecision.APPROVE, - # edited_plan="Modified plan text here..." + # Or approve with comments: + # reply = MagenticHumanInterventionReply( + # decision=MagenticHumanInterventionDecision.APPROVE, + # comments="Looks good, but prioritize efficiency metrics." # ) - async for event in workflow.send_responses_streaming({pending_request.request_id: reply}): - if isinstance(event, WorkflowCompletedEvent): - completion_event = event - elif isinstance(event, RequestInfoEvent): - # Another review cycle if needed - pending_request = event - else: - pending_request = None + # Or request revision: + # reply = MagenticHumanInterventionReply( + # decision=MagenticHumanInterventionDecision.REVISE, + # comments="Please include a comparison with newer models like LLaMA." + # ) + + pending_responses = {pending_request.request_id: reply} + pending_request = None ``` -## Key Concepts +## Advanced: Agent Clarification via Tool Approval -- **Dynamic Coordination**: The Magentic manager dynamically selects which agent should act next based on the evolving context -- **Iterative Refinement**: The system can break down complex problems and iteratively refine solutions through multiple rounds -- **Progress Tracking**: Built-in mechanisms to detect stalls and reset the plan if needed -- **Flexible Collaboration**: Agents can be called multiple times in any order as determined by the manager -- **Human Oversight**: Optional human-in-the-loop review allows manual intervention and plan modification +Agents can ask clarifying questions to users during execution using tool approval. This enables Human-in-the-Loop (HITL) interactions where the agent can request additional information before proceeding. -## Workflow Execution Flow +### Define a Tool with Approval Required -The Magentic orchestration follows this execution pattern: +```python +from typing import Annotated +from agent_framework import ai_function + +@ai_function(approval_mode="always_require") +def ask_user(question: Annotated[str, "The question to ask the user for clarification"]) -> str: + """Ask the user a clarifying question to gather missing information. + + Use this tool when you need additional information from the user to complete + your task effectively. + """ + # This function body is a placeholder - the actual interaction happens via HITL. + return f"User was asked: {question}" +``` -1. **Planning Phase**: The manager analyzes the task and creates an initial plan -2. **Agent Selection**: The manager selects the most appropriate agent for each subtask -3. **Execution**: The selected agent executes their portion of the task -4. **Progress Assessment**: The manager evaluates progress and updates the plan -5. **Iteration**: Steps 2-4 repeat until the task is complete or limits are reached -6. **Final Synthesis**: The manager synthesizes all agent outputs into a final result +### Create an Agent with the Tool -## Error Handling +```python +onboarding_agent = ChatAgent( + name="OnboardingAgent", + description="HR specialist who handles employee onboarding", + instructions=( + "You are an HR Onboarding Specialist. Your job is to onboard new employees.\n\n" + "IMPORTANT: When given an onboarding request, you MUST gather the following " + "information before proceeding:\n" + "1. Department (e.g., Engineering, Sales, Marketing)\n" + "2. Role/Title (e.g., Software Engineer, Account Executive)\n\n" + "Use the ask_user tool to request ANY missing information." + ), + chat_client=OpenAIChatClient(model_id="gpt-4o"), + tools=[ask_user], +) +``` -Add error handling to make your workflow robust: +### Handle Tool Approval Requests ```python -def on_exception(exception: Exception) -> None: - print(f"Exception occurred: {exception}") - logger.exception("Workflow exception", exc_info=exception) +async for event in workflow.run_stream("Onboard Jessica Smith"): + if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest: + req = cast(MagenticHumanInterventionRequest, event.data) + + if req.kind == MagenticHumanInterventionKind.TOOL_APPROVAL: + print(f"Agent: {req.agent_id}") + print(f"Question: {req.prompt}") + + # Get user's answer + answer = input("> ").strip() + + # Send the answer back - it will be fed to the agent as the function result + reply = MagenticHumanInterventionReply( + decision=MagenticHumanInterventionDecision.APPROVE, + response_text=answer, + ) + pending_responses = {event.request_id: reply} + + # Continue workflow with response + async for ev in workflow.send_responses_streaming(pending_responses): + # Handle continuation events + pass +``` + +## Advanced: Human Intervention on Stall + +Enable human intervention when the workflow detects that agents are not making progress: + +### Configure Stall Intervention +```python workflow = ( MagenticBuilder() - .participants(researcher=researcher_agent, coder=coder_agent) - .on_exception(on_exception) - .on_event(on_event, mode=MagenticCallbackMode.STREAMING) + .participants(researcher=researcher_agent, analyst=analyst_agent) .with_standard_manager( - chat_client=OpenAIChatClient(), + agent=manager_agent, max_round_count=10, - max_stall_count=3, + max_stall_count=1, # Stall detection after 1 round without progress max_reset_count=2, ) + .with_human_input_on_stall() # Request human input when stalled .build() ) ``` +### Handle Stall Intervention Requests + +```python +async for event in workflow.run_stream(task): + if isinstance(event, RequestInfoEvent) and event.request_type is MagenticHumanInterventionRequest: + req = cast(MagenticHumanInterventionRequest, event.data) + + if req.kind == MagenticHumanInterventionKind.STALL: + print(f"Workflow stalled after {req.stall_count} rounds") + print(f"Reason: {req.stall_reason}") + if req.plan_text: + print(f"Current plan:\n{req.plan_text}") + + # Choose response: CONTINUE, REPLAN, or GUIDANCE + reply = MagenticHumanInterventionReply( + decision=MagenticHumanInterventionDecision.GUIDANCE, + comments="Focus on completing the research step first before moving to analysis.", + ) + pending_responses = {event.request_id: reply} +``` + +## Key Concepts + +- **Dynamic Coordination**: The Magentic manager dynamically selects which agent should act next based on the evolving context +- **Iterative Refinement**: The system can break down complex problems and iteratively refine solutions through multiple rounds +- **Progress Tracking**: Built-in mechanisms to detect stalls and reset the plan if needed +- **Flexible Collaboration**: Agents can be called multiple times in any order as determined by the manager +- **Human Oversight**: Optional human-in-the-loop mechanisms including plan review, tool approval, and stall intervention +- **Unified Event System**: Use `AgentRunUpdateEvent` with `magentic_event_type` to handle orchestrator and agent streaming events + +## Workflow Execution Flow + +The Magentic orchestration follows this execution pattern: + +1. **Planning Phase**: The manager analyzes the task and creates an initial plan +2. **Optional Plan Review**: If enabled, humans can review and approve/modify the plan +3. **Agent Selection**: The manager selects the most appropriate agent for each subtask +4. **Execution**: The selected agent executes their portion of the task +5. **Progress Assessment**: The manager evaluates progress and updates the plan +6. **Stall Detection**: If progress stalls, either auto-replan or request human intervention +7. **Iteration**: Steps 3-6 repeat until the task is complete or limits are reached +8. **Final Synthesis**: The manager synthesizes all agent outputs into a final result + ## Complete Example Here's a full example that brings together all the concepts: @@ -274,20 +381,18 @@ import logging from typing import cast from agent_framework import ( + MAGENTIC_EVENT_TYPE_AGENT_DELTA, + MAGENTIC_EVENT_TYPE_ORCHESTRATOR, + AgentRunUpdateEvent, ChatAgent, + ChatMessage, HostedCodeInterpreterTool, - MagenticAgentDeltaEvent, - MagenticAgentMessageEvent, MagenticBuilder, - MagenticCallbackEvent, - MagenticCallbackMode, - MagenticFinalResultEvent, - MagenticOrchestratorMessageEvent, - WorkflowCompletedEvent, + WorkflowOutputEvent, ) from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) async def main() -> None: @@ -299,7 +404,7 @@ async def main() -> None: "You are a Researcher. You find information without additional " "computation or quantitative analysis." ), - chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"), + chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), ) coder_agent = ChatAgent( @@ -310,53 +415,26 @@ async def main() -> None: tools=HostedCodeInterpreterTool(), ) - # State for streaming callback + # Create a manager agent for orchestration + manager_agent = ChatAgent( + name="MagenticManager", + description="Orchestrator that coordinates the research and coding workflow", + instructions="You coordinate a team to complete complex tasks efficiently.", + chat_client=OpenAIChatClient(), + ) + + # State for streaming output last_stream_agent_id: str | None = None stream_line_open: bool = False - # Unified callback for all events - async def on_event(event: MagenticCallbackEvent) -> None: - nonlocal last_stream_agent_id, stream_line_open - - if isinstance(event, MagenticOrchestratorMessageEvent): - print(f"\n[ORCH:{event.kind}]\n\n{getattr(event.message, 'text', '')}\n{'-' * 26}") - - elif isinstance(event, MagenticAgentDeltaEvent): - if last_stream_agent_id != event.agent_id or not stream_line_open: - if stream_line_open: - print() - print(f"\n[STREAM:{event.agent_id}]: ", end="", flush=True) - last_stream_agent_id = event.agent_id - stream_line_open = True - print(event.text, end="", flush=True) - - elif isinstance(event, MagenticAgentMessageEvent): - if stream_line_open: - print(" (final)") - stream_line_open = False - print() - msg = event.message - if msg is not None: - response_text = (msg.text or "").replace("\n", " ") - print(f"\n[AGENT:{event.agent_id}] {msg.role.value}\n\n{response_text}\n{'-' * 26}") - - elif isinstance(event, MagenticFinalResultEvent): - print("\n" + "=" * 50) - print("FINAL RESULT:") - print("=" * 50) - if event.message is not None: - print(event.message.text) - print("=" * 50) - # Build the workflow print("\nBuilding Magentic Workflow...") workflow = ( MagenticBuilder() .participants(researcher=researcher_agent, coder=coder_agent) - .on_event(on_event, mode=MagenticCallbackMode.STREAMING) .with_standard_manager( - chat_client=OpenAIChatClient(), + agent=manager_agent, max_round_count=10, max_stall_count=3, max_reset_count=2, @@ -379,17 +457,38 @@ async def main() -> None: # Run the workflow try: - completion_event = None + output: str | None = None async for event in workflow.run_stream(task): - print(f"Event: {event}") - - if isinstance(event, WorkflowCompletedEvent): - completion_event = event - - if completion_event is not None: - data = getattr(completion_event, "data", None) - preview = getattr(data, "text", None) or (str(data) if data is not None else "") - print(f"Workflow completed with result:\n\n{preview}") + if isinstance(event, AgentRunUpdateEvent): + props = event.data.additional_properties if event.data else None + event_type = props.get("magentic_event_type") if props else None + + if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR: + kind = props.get("orchestrator_message_kind", "") if props else "" + text = event.data.text if event.data else "" + print(f"\n[ORCH:{kind}]\n\n{text}\n{'-' * 26}") + elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA: + agent_id = props.get("agent_id", event.executor_id) if props else event.executor_id + if last_stream_agent_id != agent_id or not stream_line_open: + if stream_line_open: + print() + print(f"\n[STREAM:{agent_id}]: ", end="", flush=True) + last_stream_agent_id = agent_id + stream_line_open = True + if event.data and event.data.text: + print(event.data.text, end="", flush=True) + elif event.data and event.data.text: + print(event.data.text, end="", flush=True) + elif isinstance(event, WorkflowOutputEvent): + output_messages = cast(list[ChatMessage], event.data) + if output_messages: + output = output_messages[-1].text + + if stream_line_open: + print() + + if output is not None: + print(f"Workflow completed with result:\n\n{output}") except Exception as e: print(f"Workflow execution failed: {e}") @@ -403,17 +502,27 @@ if __name__ == "__main__": ### Manager Parameters - `max_round_count`: Maximum number of collaboration rounds (default: 10) -- `max_stall_count`: Maximum rounds without progress before reset (default: 3) +- `max_stall_count`: Maximum rounds without progress before triggering stall handling (default: 3) - `max_reset_count`: Maximum number of plan resets allowed (default: 2) -### Callback Modes -- `MagenticCallbackMode.STREAMING`: Receive incremental token updates -- `MagenticCallbackMode.COMPLETE`: Receive only complete messages - -### Plan Review Decisions -- `APPROVE`: Accept the plan as-is -- `REJECT`: Reject and request a new plan -- `APPROVE` with `edited_plan`: Accept with modifications +### Human Intervention Kinds +- `PLAN_REVIEW`: Review and approve/revise the initial plan +- `TOOL_APPROVAL`: Approve a tool/function call (used for agent clarification) +- `STALL`: Workflow has stalled and needs guidance + +### Human Intervention Decisions +- `APPROVE`: Accept the plan or tool call as-is +- `REVISE`: Request revision with feedback (plan review) +- `REJECT`: Reject/deny (tool approval) +- `CONTINUE`: Continue with current state (stall) +- `REPLAN`: Trigger replanning (stall) +- `GUIDANCE`: Provide guidance text (stall, tool approval) + +### Event Types +Events are emitted via `AgentRunUpdateEvent` with metadata in `additional_properties`: +- `magentic_event_type`: Either `MAGENTIC_EVENT_TYPE_ORCHESTRATOR` or `MAGENTIC_EVENT_TYPE_AGENT_DELTA` +- `orchestrator_message_kind`: For orchestrator events, indicates the message type (e.g., "instruction", "notice", "task_ledger") +- `agent_id`: For agent delta events, identifies the streaming agent ## Sample Output diff --git a/agent-framework/user-guide/workflows/orchestrations/sequential.md b/agent-framework/user-guide/workflows/orchestrations/sequential.md index fd4a622a..2fc87dac 100644 --- a/agent-framework/user-guide/workflows/orchestrations/sequential.md +++ b/agent-framework/user-guide/workflows/orchestrations/sequential.md @@ -91,9 +91,9 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } - else if (evt is WorkflowCompletedEvent completed) + else if (evt is WorkflowOutputEvent outputEvt) { - result = (List)completed.Data!; + result = (List)outputEvt.Data!; break; } } @@ -120,7 +120,7 @@ English_Translation: Assistant: Spanish detected. Hello, world! - **AgentWorkflowBuilder.BuildSequential()**: Creates a pipeline workflow from a collection of agents - **ChatClientAgent**: Represents an agent backed by a chat client with specific instructions - **StreamingRun**: Provides real-time execution with event streaming capabilities -- **Event Handling**: Monitor agent progress through `AgentRunUpdateEvent` and completion through `WorkflowCompletedEvent` +- **Event Handling**: Monitor agent progress through `AgentRunUpdateEvent` and completion through `WorkflowOutputEvent` ::: zone-end @@ -166,17 +166,17 @@ workflow = SequentialBuilder().participants([writer, reviewer]).build() Execute the workflow and collect the final conversation showing each agent's contribution: ```python -from agent_framework import ChatMessage, WorkflowCompletedEvent +from agent_framework import ChatMessage, WorkflowOutputEvent # 3) Run and print final conversation -completion: WorkflowCompletedEvent | None = None +output_evt: WorkflowOutputEvent | None = None async for event in workflow.run_stream("Write a tagline for a budget-friendly eBike."): - if isinstance(event, WorkflowCompletedEvent): - completion = event + if isinstance(event, WorkflowOutputEvent): + output_evt = event -if completion: +if output_evt: print("===== Final Conversation =====") - messages: list[ChatMessage] | Any = completion.data + messages: list[ChatMessage] | Any = output_evt.data for i, msg in enumerate(messages, start=1): name = msg.author_name or ("assistant" if msg.role == Role.ASSISTANT else "user") print(f"{'-' * 60}\n{i:02d} [{name}]\n{msg.text}") diff --git a/agent-framework/user-guide/workflows/overview.md b/agent-framework/user-guide/workflows/overview.md index bd444be9..a7a0e701 100644 --- a/agent-framework/user-guide/workflows/overview.md +++ b/agent-framework/user-guide/workflows/overview.md @@ -18,8 +18,17 @@ Microsoft Agent Framework Workflows empowers you to build intelligent automation While an AI agent and a workflow can involve multiple steps to achieve a goal, they serve different purposes and operate at different levels of abstraction: -- **AI Agent**: An AI agent is typically driven by a large language model (LLM) and it has access to various tools to help it accomplish tasks. The steps an agent takes are dynamic and determined by the LLM based on the context of the conversation and the tools available. AI Agent -- **Workflow**: A workflow, on the other hand, is a predefined sequence of operations that can include AI agents as components. Workflows are designed to handle complex business processes that may involve multiple agents, human interactions, and integrations with external systems. The flow of a workflow is explicitly defined, allowing for more control over the execution path. Workflows Overview +- **AI Agent**: An AI agent is typically driven by a large language model (LLM) and it has access to various tools to help it accomplish tasks. The steps an agent takes are dynamic and determined by the LLM based on the context of the conversation and the tools available. + +

+ AI Agent +

+ +- **Workflow**: A workflow, on the other hand, is a predefined sequence of operations that can include AI agents as components. Workflows are designed to handle complex business processes that may involve multiple agents, human interactions, and integrations with external systems. The flow of a workflow is explicitly defined, allowing for more control over the execution path. + +

+ Workflows Overview +

## Key Features @@ -40,7 +49,7 @@ While an AI agent and a workflow can involve multiple steps to achieve a goal, t Begin your journey with Microsoft Agent Framework Workflows by exploring our getting started samples: - [C# Getting Started Sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/GettingStarted/Workflows) -- [Python Getting Started Sample](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/workflow) +- [Python Getting Started Sample](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/workflows) ## Next Steps diff --git a/agent-framework/user-guide/workflows/requests-and-responses.md b/agent-framework/user-guide/workflows/requests-and-responses.md index dcf6684c..8855fa3b 100644 --- a/agent-framework/user-guide/workflows/requests-and-responses.md +++ b/agent-framework/user-guide/workflows/requests-and-responses.md @@ -41,7 +41,7 @@ var workflow = new WorkflowBuilder(inputPort) Now, because in the workflow we have `executorA` connected to the `inputPort` in both directions, `executorA` needs to be able to send requests and receive responses via the `inputPort`. Here is what we need to do in `SomeExecutor` to send a request and receive a response. ```csharp -internal sealed class SomeExecutor() : ReflectingExecutor("SomeExecutor"), IMessageHandler +internal sealed class SomeExecutor() : Executor("SomeExecutor") { public async ValueTask HandleAsync(CustomResponseType message, IWorkflowContext context) { @@ -56,15 +56,22 @@ internal sealed class SomeExecutor() : ReflectingExecutor("SomeExe Alternatively, `SomeExecutor` can separate the request sending and response handling into two handlers. ```csharp -internal sealed class SomeExecutor() : ReflectingExecutor("SomeExecutor"), IMessageHandler, IMessageHandler +internal sealed class SomeExecutor() : Executor("SomeExecutor") { - public async ValueTask HandleAsync(CustomResponseType message, IWorkflowContext context) + protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) + { + return routeBuilder + .AddHandler(this.HandleCustomResponseAsync) + .AddHandler(this.HandleOtherDataAsync); + } + + public async ValueTask HandleCustomResponseAsync(CustomResponseType message, IWorkflowContext context) { // Process the response... ... } - public async ValueTask HandleAsync(OtherDataType message, IWorkflowContext context) + public async ValueTask HandleOtherDataAsync(OtherDataType message, IWorkflowContext context) { // Process the message... ... @@ -151,9 +158,9 @@ await foreach (WorkflowEvent evt in handle.WatchStreamAsync().ConfigureAwait(fal await handle.SendResponseAsync(response).ConfigureAwait(false); break; - case WorkflowCompletedEvent workflowCompleteEvt: + case WorkflowOutputEvent workflowOutputEvt: // The workflow has completed successfully - Console.WriteLine($"Workflow completed with result: {workflowCompleteEvt.Data}"); + Console.WriteLine($"Workflow completed with result: {workflowOutputEvt.Data}"); return; } } @@ -198,7 +205,8 @@ When a checkpoint is created, pending requests are also saved as part of the che ## Next Steps -- [Learn how to use agents in workflows](./using-agents.md) to build intelligent workflows. -- [Learn how to use workflows as agents](./as-agents.md). - [Learn how to manage state](./shared-states.md) in workflows. - [Learn how to create checkpoints and resume from them](./checkpoints.md). +- [Learn how to monitor workflows](./observability.md). +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/shared-states.md b/agent-framework/user-guide/workflows/shared-states.md index 78d7eade..791d5a9f 100644 --- a/agent-framework/user-guide/workflows/shared-states.md +++ b/agent-framework/user-guide/workflows/shared-states.md @@ -25,7 +25,7 @@ Shared States allow multiple executors within a workflow to access and modify co using Microsoft.Agents.Workflows; using Microsoft.Agents.Workflows.Reflection; -internal sealed class FileReadExecutor() : ReflectingExecutor("FileReadExecutor"), IMessageHandler +internal sealed class FileReadExecutor() : Executor("FileReadExecutor") { /// /// Reads a file and stores its content in a shared state. @@ -81,7 +81,7 @@ class FileReadExecutor(Executor): using Microsoft.Agents.Workflows; using Microsoft.Agents.Workflows.Reflection; -internal sealed class WordCountingExecutor() : ReflectingExecutor("WordCountingExecutor"), IMessageHandler +internal sealed class WordCountingExecutor() : Executor("WordCountingExecutor") { /// /// Counts the number of words in the file content stored in a shared state. @@ -127,7 +127,7 @@ class WordCountingExecutor(Executor): ## Next Steps -- [Learn how to use agents in workflows](./using-agents.md) to build intelligent workflows. -- [Learn how to use workflows as agents](./as-agents.md). -- [Learn how to handle requests and responses](./requests-and-responses.md) in workflows. - [Learn how to create checkpoints and resume from them](./checkpoints.md). +- [Learn how to monitor workflows](./observability.md). +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/state-isolation.md b/agent-framework/user-guide/workflows/state-isolation.md new file mode 100644 index 00000000..c16a4dfd --- /dev/null +++ b/agent-framework/user-guide/workflows/state-isolation.md @@ -0,0 +1,157 @@ +--- +title: Microsoft Agent Framework Workflows - State Isolation +description: In-depth look at state isolation and thread safety in Microsoft Agent Framework Workflows. +zone_pivot_groups: programming-languages +author: TaoChenOSU +ms.topic: tutorial +ms.author: taochen +ms.date: 09/12/2025 +ms.service: agent-framework +--- + +# Microsoft Agent Framework Workflows - State Isolation + +In real-world applications, properly managing state is critical when handling multiple tasks or requests. Without proper isolation, shared state between different workflow executions can lead to unexpected behavior, data corruption, and race conditions. This article explains how to ensure state isolation within Microsoft Agent Framework Workflows, providing insights into best practices and common pitfalls. + +## Mutable Workflow Builders vs Immutable Workflows + +Workflows are created by workflow builders. Workflow builders are generally considered mutable, where one can add, modify start executor or other configurations after the builder is created or even after a workflow has been built. On the other hand, workflows are immutable in that once a workflow is built, it cannot be modified (no public API to modify a workflow). + +This distinction is important because it affects how state is managed across different workflow executions. It is not recommended to reuse a single workflow instance for multiple tasks or requests, as this can lead to unintended state sharing. Instead, it is recommended to create a new workflow instance from the builder for each task or request to ensure proper state isolation and thread safety. + +## Ensuring State Isolation in Workflow Builders + +When an executor instance is passed directly to a workflow builder, that executor instance is shared among all workflow instances created from the builder. This can lead to issues if the executor instance contains state that should not be shared across multiple workflow executions. To ensure proper state isolation and thread safety, it is recommended to use factory functions that create a new executor instance for each workflow instance. + +::: zone pivot="programming-language-csharp" + +Coming soon... + +::: zone-end + +::: zone pivot="programming-language-python" + +Non-thread-safe example: + +```python +executor_a = CustomExecutorA() +executor_b = CustomExecutorB() + +workflow_builder = WorkflowBuilder() +# executor_a and executor_b are passed directly to the workflow builder +workflow_builder.add_edge(executor_a, executor_b) +workflow_builder.set_start_executor(executor_b) + +# All workflow instances created from the builder will share the same executor instances +workflow_a = workflow_builder.build() +workflow_b = workflow_builder.build() +``` + +Thread-safe example: + +```python +workflow_builder = WorkflowBuilder() +# Register executor factory functions with the workflow builder +workflow_builder.register_executor(factory_func=CustomExecutorA, name="executor_a") +workflow_builder.register_executor(factory_func=CustomExecutorB, name="executor_b") +# Add edges using registered factory function names +workflow_builder.add_edge("executor_a", "executor_b") +workflow_builder.set_start_executor("executor_b") + +# Each workflow instance created from the builder will have its own executor instances +workflow_a = workflow_builder.build() +workflow_b = workflow_builder.build() +``` + +::: zone-end + +> [!TIP] +> To ensure proper state isolation and thread safety, also make sure that executor instances created by factory functions do not share mutable state. + +## Agent State Management + +Agent context is managed via agent threads. By default, each agent in a workflow will get its own thread unless the agent is managed by a custom executor. For more information, refer to [Working with Agents](./using-agents.md). + +Agent threads are persisted across workflow runs. This means that if an agent is invoked in the first run of a workflow, content generated by the agent will be available in subsequent runs of the same workflow instance. While this can be useful for maintaining continuity within a single task, it can also lead to unintended state sharing if the same workflow instance is reused for different tasks or requests. To ensure each task has isolated agent state, use agent factory functions in your workflow builder to create a new workflow instance for each task or request. + +::: zone pivot="programming-language-csharp" + +Coming soon... + +::: zone-end + +::: zone pivot="programming-language-python" + +Non-thread-safe example: + +```python +writer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( + instructions=( + "You are an excellent content writer. You create new content and edit contents based on the feedback." + ), + name="writer_agent", +) +reviewer_agent = AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( + instructions=( + "You are an excellent content reviewer." + "Provide actionable feedback to the writer about the provided content." + "Provide the feedback in the most concise manner possible." + ), + name="reviewer_agent", +) + +builder = WorkflowBuilder() +# writer_agent and reviewer_agent are passed directly to the workflow builder +builder.add_edge(writer_agent, reviewer_agent) +builder.set_start_executor(writer_agent) + +# All workflow instances created from the builder will share the same agent +# instances and agent threads +workflow = builder.build() +``` + +Thread-safe example: + +```python +def create_writer_agent() -> ChatAgent: + """Factory function to create a Writer agent.""" + return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( + instructions=( + "You are an excellent content writer. You create new content and edit contents based on the feedback." + ), + name="writer_agent", + ) + +def create_reviewer_agent() -> ChatAgent: + """Factory function to create a Reviewer agent.""" + return AzureOpenAIChatClient(credential=AzureCliCredential()).create_agent( + instructions=( + "You are an excellent content reviewer." + "Provide actionable feedback to the writer about the provided content." + "Provide the feedback in the most concise manner possible." + ), + name="reviewer_agent", + ) + +builder = WorkflowBuilder() +# Register agent factory functions with the workflow builder +builder.register_agent(factory_func=create_writer_agent, name="writer_agent") +builder.register_agent(factory_func=create_reviewer_agent, name="reviewer_agent") +# Add edges using registered factory function names +builder.add_edge("writer_agent", "reviewer_agent") +builder.set_start_executor("writer_agent") + +# Each workflow instance created from the builder will have its own agent +# instances and agent threads +workflow = builder.build() +``` + +::: zone-end + +## Conclusion + +State isolation in Microsoft Agent Framework Workflows can be effectively managed by using factory functions with workflow builders to create fresh executor and agent instances. By creating new workflow instances for each task or request, you can maintain proper state isolation and avoid unintended state sharing between different workflow executions. + +## Next Steps + +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/using-agents.md b/agent-framework/user-guide/workflows/using-agents.md index fdf9c80b..e0bfca82 100644 --- a/agent-framework/user-guide/workflows/using-agents.md +++ b/agent-framework/user-guide/workflows/using-agents.md @@ -151,7 +151,7 @@ Sometimes you may want to customize how AI agents are integrated into a workflow ::: zone pivot="programming-language-csharp" ```csharp -internal sealed class CustomAgentExecutor : ReflectingExecutor, IMessageHandler +internal sealed class CustomAgentExecutor : Executor("CustomAgentExecutor") { private readonly AIAgent _agent; @@ -228,3 +228,6 @@ class Writer(Executor): - [Learn how to handle requests and responses](./requests-and-responses.md) in workflows. - [Learn how to manage state](./shared-states.md) in workflows. - [Learn how to create checkpoints and resume from them](./checkpoints.md). +- [Learn how to monitor workflows](./observability.md). +- [Learn about state isolation in workflows](./state-isolation.md). +- [Learn how to visualize workflows](./visualization.md). diff --git a/agent-framework/user-guide/workflows/visualization.md b/agent-framework/user-guide/workflows/visualization.md index 521ab194..bbb71b4f 100644 --- a/agent-framework/user-guide/workflows/visualization.md +++ b/agent-framework/user-guide/workflows/visualization.md @@ -1,6 +1,7 @@ --- title: Microsoft Agent Framework Workflows - Visualization description: In-depth look at Visualization in Microsoft Agent Framework Workflows. +zone_pivot_groups: programming-languages author: TaoChenOSU ms.topic: tutorial ms.author: taochen @@ -12,10 +13,43 @@ ms.service: agent-framework Sometimes a workflow that has multiple executors and complex interactions can be hard to understand from just reading the code. Visualization can help you see the structure of the workflow more clearly, so that you can verify that it has the intended design. -Workflow visualization is done via a `WorkflowViz` object that can be instantiated with a `Workflow` object. The `WorkflowViz` object can then generate visualizations in different formats, such as Graphviz DOT format or Mermaid diagram format. +::: zone pivot="programming-language-csharp" + +Workflow visualization can be achieved via extension methods on the `Workflow` class: `ToMermaidString()`, and `ToDotString()`, which generate Mermaid diagram format and Graphviz DOT format respectively. + +```csharp +using Microsoft.Agents.AI.Workflows; + +// Create a workflow with a fan-out and fan-in pattern +var workflow = new WorkflowBuilder() + .SetStartExecutor(dispatcher) + .AddFanOutEdges(dispatcher, [researcher, marketer, legal]) + .AddFanInEdges([researcher, marketer, legal], aggregator) + .Build(); + +// Mermaid diagram +Console.WriteLine(workflow.ToMermaidString()); + +// DiGraph string +Console.WriteLine(workflow.ToDotString()); +``` + +To create an image file from the DOT format, you can use GraphViz tools with the following command: + +```bash +dotnet run | tail -n +20 | dot -Tpng -o workflow.png +``` > [!TIP] -> To export visualization images you also need to [install GraphViz](https://graphviz.org/download/). +> To export visualization images you need to [install GraphViz](https://graphviz.org/download/). + +For a complete working implementation with visualization, see the [Visualization sample](https://github.com/microsoft/agent-framework/tree/main/dotnet/samples/GettingStarted/Workflows/Visualization). + +::: zone-end + +::: zone pivot="programming-language-python" + +Workflow visualization is done via a `WorkflowViz` object that can be instantiated with a `Workflow` object. The `WorkflowViz` object can then generate visualizations in different formats, such as Graphviz DOT format or Mermaid diagram format. Creating a `WorkflowViz` object is straightforward: @@ -43,8 +77,25 @@ print(viz.to_mermaid()) print(viz.to_digraph()) # Export to a file print(viz.export(format="svg")) +# Different formats are also supported +print(viz.export(format="png")) +print(viz.export(format="pdf")) +print(viz.export(format="dot")) +# Export with custom filenames +print(viz.export(format="svg", filename="my_workflow.svg")) +# Convenience methods +print(viz.save_svg("workflow.svg")) +print(viz.save_png("workflow.png")) +print(viz.save_pdf("workflow.pdf")) ``` +> [!TIP] +> For basic text output (Mermaid and DOT), no additional dependencies are needed. For image export, you need to install the `graphviz` Python package by running: `pip install graphviz>=0.20.0` and [install GraphViz](https://graphviz.org/download/). + +For a complete working implementation with visualization, see the [Concurrent with Visualization sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/workflows/visualization/concurrent_with_visualization.py). + +::: zone-end + The exported diagram will look similar to the following for the example workflow: ```mermaid @@ -67,3 +118,23 @@ flowchart TD or in Graphviz DOT format: ![Workflow Diagram](./resources/images/workflow-viz.svg) + +## Visualization Features + +### Node Styling + +- **Start executors**: Green background with "(Start)" label +- **Regular executors**: Blue background with executor ID +- **Fan-in nodes**: Golden background with ellipse shape (DOT) or double circles (Mermaid) + +### Edge Styling + +- **Normal edges**: Solid arrows +- **Conditional edges**: Dashed/dotted arrows with "conditional" labels +- **Fan-out/Fan-in**: Automatic routing through intermediate nodes + +### Layout Options + +- **Top-down layout**: Clear hierarchical flow visualization +- **Subgraph clustering**: Nested workflows shown as grouped clusters +- **Automatic positioning**: GraphViz handles optimal node placement \ No newline at end of file diff --git a/semantic-kernel/support/archive/agent-chat-example.md b/semantic-kernel/support/archive/agent-chat-example.md index dbf36f91..7a22a94f 100644 --- a/semantic-kernel/support/archive/agent-chat-example.md +++ b/semantic-kernel/support/archive/agent-chat-example.md @@ -11,7 +11,10 @@ ms.service: semantic-kernel # How-To: Coordinate Agent Collaboration using Agent Group Chat > [!IMPORTANT] -> This feature is in the experimental stage. Features at this stage are under development and subject to change before advancing to the preview or release candidate stage. +> This is an archived document. + +> [!IMPORTANT] +> This feature is in the experimental stage but no longer maintained. For a replacement, see the [Group Chat Orchestration](./../../Frameworks/agent/agent-orchestration/group-chat.md) and the migration guide [Migrating from AgentChat to Group Chat Orchestration](./../migration/group-chat-orchestration-migration-guide.md). ## Overview diff --git a/semantic-kernel/support/archive/agent-chat.md b/semantic-kernel/support/archive/agent-chat.md index f3923a1e..5923f1b3 100644 --- a/semantic-kernel/support/archive/agent-chat.md +++ b/semantic-kernel/support/archive/agent-chat.md @@ -11,7 +11,10 @@ ms.service: semantic-kernel # Exploring Agent Collaboration in `AgentChat` > [!IMPORTANT] -> This feature is in the experimental stage. Features at this stage are under development and subject to change before advancing to the preview or release candidate stage. +> This is an archived document. + +> [!IMPORTANT] +> This feature is in the experimental stage but no longer maintained. For a replacement, see the [Group Chat Orchestration](./../../Frameworks/agent/agent-orchestration/group-chat.md) and the migration guide [Migrating from AgentChat to Group Chat Orchestration](./../migration/group-chat-orchestration-migration-guide.md). Detailed API documentation related to this discussion is available at: