diff --git a/.openpublishing.publish.config.json b/.openpublishing.publish.config.json index 70a1d2476..509932cdc 100644 --- a/.openpublishing.publish.config.json +++ b/.openpublishing.publish.config.json @@ -6,7 +6,7 @@ "build_output_subfolder": "agent-framework", "locale": "en-us", "monikers": [], - "open_to_public_contributors": false, + "open_to_public_contributors": true, "type_mapping": { "Conceptual": "Content" }, diff --git a/agent-framework/integrations/ag-ui/backend-tool-rendering.md b/agent-framework/integrations/ag-ui/backend-tool-rendering.md index edae2f910..8063a3d4b 100644 --- a/agent-framework/integrations/ag-ui/backend-tool-rendering.md +++ b/agent-framework/integrations/ag-ui/backend-tool-rendering.md @@ -186,7 +186,7 @@ To see tool calls and results in real-time, extend the client's streaming loop t ```csharp // Inside the streaming loop from getting-started.md -await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread)) { ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); diff --git a/agent-framework/integrations/ag-ui/frontend-tools.md b/agent-framework/integrations/ag-ui/frontend-tools.md index b31b7689f..5c0403a3c 100644 --- a/agent-framework/integrations/ag-ui/frontend-tools.md +++ b/agent-framework/integrations/ag-ui/frontend-tools.md @@ -85,7 +85,7 @@ AIAgent inspectableAgent = baseAgent .Use(runFunc: null, runStreamingFunc: InspectToolsMiddleware) .Build(); -static async IAsyncEnumerable InspectToolsMiddleware( +static async IAsyncEnumerable InspectToolsMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, @@ -109,7 +109,7 @@ static async IAsyncEnumerable InspectToolsMiddleware( } } - await foreach (AgentRunResponseUpdate update in innerAgent.RunStreamingAsync(messages, thread, options, cancellationToken)) + await foreach (AgentResponseUpdate update in innerAgent.RunStreamingAsync(messages, thread, options, cancellationToken)) { yield return update; } diff --git a/agent-framework/integrations/ag-ui/getting-started.md b/agent-framework/integrations/ag-ui/getting-started.md index c9bc539cd..ce632f929 100644 --- a/agent-framework/integrations/ag-ui/getting-started.md +++ b/agent-framework/integrations/ag-ui/getting-started.md @@ -209,7 +209,7 @@ try bool isFirstUpdate = true; string? threadId = null; - await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(messages, thread)) + await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(messages, thread)) { ChatResponseUpdate chatUpdate = update.AsChatResponseUpdate(); @@ -257,7 +257,7 @@ catch (Exception ex) - **Server-Sent Events (SSE)**: The protocol uses SSE for streaming responses - **AGUIChatClient**: Client class that connects to AG-UI servers and implements `IChatClient` - **CreateAIAgent**: Extension method on `AGUIChatClient` to create an agent from the client -- **RunStreamingAsync**: Streams responses as `AgentRunResponseUpdate` objects +- **RunStreamingAsync**: Streams responses as `AgentResponseUpdate` objects - **AsChatResponseUpdate**: Extension method to access chat-specific properties like `ConversationId` and `ResponseId` - **Thread Management**: The `AgentThread` maintains conversation context across requests - **Content Types**: Responses include `TextContent` for messages and `ErrorContent` for errors @@ -327,7 +327,7 @@ The client displays different content types with distinct colors: 1. `AGUIChatClient` sends HTTP POST request to server endpoint 2. Server responds with SSE stream -3. Client parses incoming events into `AgentRunResponseUpdate` objects +3. Client parses incoming events into `AgentResponseUpdate` objects 4. Each update is displayed based on its content type 5. `ConversationId` is captured for conversation continuity 6. Stream completes when run finishes diff --git a/agent-framework/integrations/ag-ui/human-in-the-loop.md b/agent-framework/integrations/ag-ui/human-in-the-loop.md index 763b2b331..57a343d5a 100644 --- a/agent-framework/integrations/ag-ui/human-in-the-loop.md +++ b/agent-framework/integrations/ag-ui/human-in-the-loop.md @@ -126,7 +126,7 @@ var agent = baseAgent cancellationToken)) .Build(); -static async IAsyncEnumerable HandleApprovalRequestsMiddleware( +static async IAsyncEnumerable HandleApprovalRequestsMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, @@ -250,8 +250,8 @@ static async IAsyncEnumerable HandleApprovalRequestsMidd } // Local function: Convert FunctionApprovalRequestContent to client tool calls - static async IAsyncEnumerable ConvertFunctionApprovalsToToolCalls( - AgentRunResponseUpdate update, + static async IAsyncEnumerable ConvertFunctionApprovalsToToolCalls( + AgentResponseUpdate update, JsonSerializerOptions jsonSerializerOptions) { // Check if this update contains a FunctionApprovalRequestContent @@ -292,7 +292,7 @@ static async IAsyncEnumerable HandleApprovalRequestsMidd var approvalJson = JsonSerializer.Serialize(approvalData, jsonSerializerOptions.GetTypeInfo(typeof(ApprovalRequest))); // Yield a tool call update that represents the approval request - yield return new AgentRunResponseUpdate(ChatRole.Assistant, [ + yield return new AgentResponseUpdate(ChatRole.Assistant, [ new FunctionCallContent( callId: approvalId, name: "request_approval", @@ -337,7 +337,7 @@ var wrappedAgent = agent cancellationToken)) .Build(); -static async IAsyncEnumerable HandleApprovalRequestsClientMiddleware( +static async IAsyncEnumerable HandleApprovalRequestsClientMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, @@ -414,8 +414,8 @@ static async IAsyncEnumerable HandleApprovalRequestsClie } // Local function: Convert request_approval tool calls to FunctionApprovalRequestContent - static async IAsyncEnumerable ConvertToolCallsToApprovalRequests( - AgentRunResponseUpdate update, + static async IAsyncEnumerable ConvertToolCallsToApprovalRequests( + AgentResponseUpdate update, JsonSerializerOptions jsonSerializerOptions) { FunctionCallContent? approvalToolCall = null; @@ -452,7 +452,7 @@ static async IAsyncEnumerable HandleApprovalRequestsClie arguments: functionArguments); // Yield the original tool call first (for message history) - yield return new AgentRunResponseUpdate(ChatRole.Assistant, [approvalToolCall]); + yield return new AgentResponseUpdate(ChatRole.Assistant, [approvalToolCall]); // Create approval request with CallId stored in AdditionalProperties var approvalRequestContent = new FunctionApprovalRequestContent( @@ -463,7 +463,7 @@ static async IAsyncEnumerable HandleApprovalRequestsClie approvalRequestContent.AdditionalProperties ??= new Dictionary(); approvalRequestContent.AdditionalProperties["request_approval_call_id"] = approvalToolCall.CallId; - yield return new AgentRunResponseUpdate(ChatRole.Assistant, [approvalRequestContent]); + yield return new AgentResponseUpdate(ChatRole.Assistant, [approvalRequestContent]); } } #pragma warning restore MEAI001 @@ -490,7 +490,7 @@ do approvalResponses.Clear(); approvalToolCalls.Clear(); - await foreach (AgentRunResponseUpdate update in wrappedAgent.RunStreamingAsync( + await foreach (AgentResponseUpdate update in wrappedAgent.RunStreamingAsync( messages, thread, cancellationToken: cancellationToken)) { foreach (AIContent content in update.Contents) diff --git a/agent-framework/integrations/ag-ui/index.md b/agent-framework/integrations/ag-ui/index.md index 2fac4fcfb..59eab55d6 100644 --- a/agent-framework/integrations/ag-ui/index.md +++ b/agent-framework/integrations/ag-ui/index.md @@ -114,7 +114,7 @@ Understanding how Agent Framework concepts map to AG-UI helps you build effectiv | `AIAgent` | Agent Endpoint | Each agent becomes an HTTP endpoint | | `agent.Run()` | HTTP POST Request | Client sends messages via HTTP | | `agent.RunStreamingAsync()` | Server-Sent Events | Streaming responses via SSE | -| `AgentRunResponseUpdate` | AG-UI Events | Converted to protocol events automatically | +| `AgentResponseUpdate` | AG-UI Events | Converted to protocol events automatically | | `AIFunctionFactory.Create()` | Backend Tools | Executed on server, results streamed | | `ApprovalRequiredAIFunction` | Human-in-the-Loop | Middleware converts to approval protocol | | `AgentThread` | Thread Management | `ConversationId` maintains context | diff --git a/agent-framework/integrations/ag-ui/state-management.md b/agent-framework/integrations/ag-ui/state-management.md index 149abf0a1..c2591b0b3 100644 --- a/agent-framework/integrations/ag-ui/state-management.md +++ b/agent-framework/integrations/ag-ui/state-management.md @@ -113,17 +113,17 @@ internal sealed class SharedStateAgent : DelegatingAIAgent this._jsonSerializerOptions = jsonSerializerOptions; } - public override Task RunAsync( + public override Task RunAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { return this.RunStreamingAsync(messages, thread, options, cancellationToken) - .ToAgentRunResponseAsync(cancellationToken); + .ToAgentResponseAsync(cancellationToken); } - public override async IAsyncEnumerable RunStreamingAsync( + public override async IAsyncEnumerable RunStreamingAsync( IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, @@ -187,7 +187,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent var firstRunMessages = messages.Append(stateUpdateMessage); // Collect all updates from first run - var allUpdates = new List(); + var allUpdates = new List(); await foreach (var update in this.InnerAgent.RunStreamingAsync(firstRunMessages, thread, firstRunOptions, cancellationToken).ConfigureAwait(false)) { allUpdates.Add(update); @@ -200,7 +200,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent } } - var response = allUpdates.ToAgentRunResponse(); + var response = allUpdates.ToAgentResponse(); // Try to deserialize the structured state response if (response.TryDeserialize(this._jsonSerializerOptions, out JsonElement stateSnapshot)) @@ -209,7 +209,7 @@ internal sealed class SharedStateAgent : DelegatingAIAgent byte[] stateBytes = JsonSerializer.SerializeToUtf8Bytes( stateSnapshot, this._jsonSerializerOptions.GetTypeInfo(typeof(JsonElement))); - yield return new AgentRunResponseUpdate + yield return new AgentResponseUpdate { Contents = [new DataContent(stateBytes, "application/json")] }; diff --git a/agent-framework/migration-guide/from-autogen/index.md b/agent-framework/migration-guide/from-autogen/index.md index 3cbde666c..64911ca4b 100644 --- a/agent-framework/migration-guide/from-autogen/index.md +++ b/agent-framework/migration-guide/from-autogen/index.md @@ -221,33 +221,39 @@ def get_time() -> str: client = OpenAIChatClient(model_id="gpt-5") async def example(): - # Direct creation + # Direct creation with default options agent = ChatAgent( name="assistant", chat_client=client, instructions="You are a helpful assistant.", - tools=[get_weather] # Multi-turn by default + tools=[get_weather], # Multi-turn by default + default_options={ + "temperature": 0.7, + "max_tokens": 1000, + } ) # Factory method (more convenient) agent = client.create_agent( name="assistant", instructions="You are a helpful assistant.", - tools=[get_weather] + tools=[get_weather], + default_options={"temperature": 0.7} ) - # Execution with runtime tool configuration + # Execution with runtime tool and options configuration result = await agent.run( "What's the weather?", - tools=[get_time], # Can add tools at runtime - tool_choice="auto" + tools=[get_time], # Can add tools at runtime (keyword arg) + options={"tool_choice": "auto"} # Other options go in options dict ) ``` **Key Differences:** - **Default behavior**: `ChatAgent` automatically iterates through tool calls, while `AssistantAgent` requires explicit `max_tool_iterations` setting -- **Runtime configuration**: `ChatAgent.run()` accepts `tools` and `tool_choice` parameters for per-invocation customization +- **Runtime configuration**: `ChatAgent.run()` accepts `tools` as a keyword argument and other options via the `options` dict parameter for per-invocation customization +- **Options system**: Agent Framework uses TypedDict-based options (e.g., `OpenAIChatOptions`) for type safety and IDE autocomplete. Options are passed via `default_options` at construction and `options` at runtime - **Factory methods**: Agent Framework provides convenient factory methods directly from chat clients - **State management**: `ChatAgent` is stateless and doesn't maintain conversation history between invocations, unlike `AssistantAgent` which maintains conversation history as part of its state @@ -340,18 +346,21 @@ async for event in agent.run_stream(task="Hello"): ```python # Assume we have client, agent, and tools from previous examples async def streaming_example(): - # Chat client streaming - async for chunk in client.get_streaming_response("Hello", tools=tools): + # Chat client streaming - tools go in options dict + async for chunk in client.get_streaming_response( + "Hello", + options={"tools": tools} + ): if chunk.text: print(chunk.text, end="") - # Agent streaming - async for chunk in agent.run_stream("Hello"): + # Agent streaming - tools can be keyword arg on agents + async for chunk in agent.run_stream("Hello", tools=tools): if chunk.text: print(chunk.text, end="", flush=True) ``` -Tip: In Agent Framework, both clients and agents yield the same update shape; you can read `chunk.text` in either case. +Tip: In Agent Framework, both clients and agents yield the same update shape; you can read `chunk.text` in either case. Note that for chat clients, `tools` goes in the `options` dict, while for agents, `tools` remains a direct keyword argument. ### Message Types and Creation @@ -688,8 +697,8 @@ Notes: from collections.abc import AsyncIterable from typing import Any from agent_framework import ( - AgentRunResponse, - AgentRunResponseUpdate, + AgentResponse, + AgentResponseUpdate, AgentThread, BaseAgent, ChatMessage, @@ -704,7 +713,7 @@ class StaticAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AgentRunResponse: + ) -> AgentResponse: # Build a static reply reply = ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Hello from AF custom agent")]) @@ -713,7 +722,7 @@ class StaticAgent(BaseAgent): normalized = self._normalize_messages(messages) await self._notify_thread_of_new_messages(thread, normalized, reply) - return AgentRunResponse(messages=[reply]) + return AgentResponse(messages=[reply]) async def run_stream( self, @@ -721,9 +730,9 @@ class StaticAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AsyncIterable[AgentRunResponseUpdate]: + ) -> AsyncIterable[AgentResponseUpdate]: # Stream the same static response in a single chunk for simplicity - yield AgentRunResponseUpdate(contents=[TextContent(text="Hello from AF custom agent")], role=Role.ASSISTANT) + yield AgentResponseUpdate(contents=[TextContent(text="Hello from AF custom agent")], role=Role.ASSISTANT) # Notify thread of input and the complete response once streaming ends if thread is not None: @@ -1199,7 +1208,7 @@ from typing import cast from agent_framework import ( MAGENTIC_EVENT_TYPE_AGENT_DELTA, MAGENTIC_EVENT_TYPE_ORCHESTRATOR, - AgentRunUpdateEvent, + AgentResponseUpdateEvent, ChatAgent, ChatMessage, MagenticBuilder, @@ -1231,7 +1240,7 @@ workflow = ( async def magentic_example(): output: str | None = None async for event in workflow.run_stream("Complex research task"): - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdateEvent): props = event.data.additional_properties if event.data else None event_type = props.get("magentic_event_type") if props else None @@ -1255,7 +1264,7 @@ The Magentic workflow provides extensive customization options: - **Manager configuration**: Use a ChatAgent with custom instructions and model settings - **Round limits**: `max_round_count`, `max_stall_count`, `max_reset_count` -- **Event streaming**: Use `AgentRunUpdateEvent` with `magentic_event_type` metadata +- **Event streaming**: Use `AgentResponseUpdateEvent` with `magentic_event_type` metadata - **Agent specialization**: Custom instructions and tools per agent - **Human-in-the-loop**: Plan review, tool approval, and stall intervention @@ -1265,7 +1274,7 @@ from typing import cast from agent_framework import ( MAGENTIC_EVENT_TYPE_AGENT_DELTA, MAGENTIC_EVENT_TYPE_ORCHESTRATOR, - AgentRunUpdateEvent, + AgentResponseUpdateEvent, ChatAgent, MagenticBuilder, MagenticHumanInterventionDecision, @@ -1345,7 +1354,7 @@ Agent Framework provides built-in request-response capabilities where any execut ```python from agent_framework import ( - RequestInfoEvent, WorkflowBuilder, WorkflowContext, + RequestInfoEvent, WorkflowBuilder, WorkflowContext, Executor, handler, response_handler ) from dataclasses import dataclass @@ -1361,7 +1370,7 @@ class ApprovalRequest: # Workflow executor that requests human approval class ReviewerExecutor(Executor): - + @handler async def review_content( self, @@ -1374,7 +1383,7 @@ class ReviewerExecutor(Executor): agent_name="writer_agent" ) await ctx.request_info(request_data=approval_request, response_type=str) - + @response_handler async def handle_approval_response( self, diff --git a/agent-framework/migration-guide/from-semantic-kernel/index.md b/agent-framework/migration-guide/from-semantic-kernel/index.md index d0166e816..183716c08 100644 --- a/agent-framework/migration-guide/from-semantic-kernel/index.md +++ b/agent-framework/migration-guide/from-semantic-kernel/index.md @@ -180,13 +180,13 @@ await foreach (AgentResponseItem result in agent.InvokeAsync ### Agent Framework -The Non-Streaming returns a single `AgentRunResponse` with the agent response that can contain multiple messages. -The text result of the run is available in `AgentRunResponse.Text` or `AgentRunResponse.ToString()`. -All messages created as part of the response are returned in the `AgentRunResponse.Messages` list. +The Non-Streaming returns a single `AgentResponse` with the agent response that can contain multiple messages. +The text result of the run is available in `AgentResponse.Text` or `AgentResponse.ToString()`. +All messages created as part of the response are returned in the `AgentResponse.Messages` list. This might include tool call messages, function results, reasoning updates, and final results. ```csharp -AgentRunResponse agentResponse = await agent.RunAsync(userInput, thread); +AgentResponse agentResponse = await agent.RunAsync(userInput, thread); ``` ## 7. Agent Streaming Invocation @@ -204,12 +204,12 @@ await foreach (StreamingChatMessageContent update in agent.InvokeStreamingAsync( ### Agent Framework -Agent Framework has a similar streaming API pattern, with the key difference being that it returns `AgentRunResponseUpdate` objects that include more agent-related information per update. +Agent Framework has a similar streaming API pattern, with the key difference being that it returns `AgentResponseUpdate` objects that include more agent-related information per update. -All updates produced by any service underlying the AIAgent are returned. The textual result of the agent is available by concatenating the `AgentRunResponse.Text` values. +All updates produced by any service underlying the AIAgent are returned. The textual result of the agent is available by concatenating the `AgentResponse.Text` values. ```csharp -await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync(userInput, thread)) +await foreach (AgentResponseUpdate update in agent.RunStreamingAsync(userInput, thread)) { Console.Write(update); // Update is ToString() friendly } @@ -677,7 +677,7 @@ This compatibility layer allows you to gradually migrate your code from Semantic ## 6. Agent Non-Streaming Invocation -Key differences can be seen in the method names from `invoke` to `run`, return types (for example, `AgentRunResponse`) and parameters. +Key differences can be seen in the method names from `invoke` to `run`, return types (for example, `AgentResponse`) and parameters. ### Semantic Kernel @@ -701,7 +701,7 @@ print(f"# {response.role}: {response}") ### Agent Framework -The Non-Streaming run returns a single `AgentRunResponse` with the agent response that can contain multiple messages. +The Non-Streaming run returns a single `AgentResponse` with the agent response that can contain multiple messages. The text result of the run is available in `response.text` or `str(response)`. All messages created as part of the response are returned in the `response.messages` list. This might include tool call messages, function results, reasoning updates and final results. @@ -716,7 +716,7 @@ print("Agent response:", response.text) ## 7. Agent Streaming Invocation -Key differences in the method names from `invoke` to `run_stream`, return types (`AgentRunResponseUpdate`) and parameters. +Key differences in the method names from `invoke` to `run_stream`, return types (`AgentResponseUpdate`) and parameters. ### Semantic Kernel @@ -731,28 +731,28 @@ async for update in agent.invoke_stream( ### Agent Framework -Similar streaming API pattern with the key difference being that it returns `AgentRunResponseUpdate` objects including more agent related information per update. +Similar streaming API pattern with the key difference being that it returns `AgentResponseUpdate` objects including more agent related information per update. All contents produced by any service underlying the Agent are returned. The final result of the agent is available by combining the `update` values into a single response. ```python -from agent_framework import AgentRunResponse +from agent_framework import AgentResponse agent = ... updates = [] async for update in agent.run_stream(user_input, thread): updates.append(update) print(update.text) -full_response = AgentRunResponse.from_agent_run_response_updates(updates) +full_response = AgentResponse.from_agent_response_updates(updates) print("Full agent response:", full_response.text) ``` You can even do that directly: ```python -from agent_framework import AgentRunResponse +from agent_framework import AgentResponse agent = ... -full_response = AgentRunResponse.from_agent_response_generator(agent.run_stream(user_input, thread)) +full_response = AgentResponse.from_agent_response_generator(agent.run_stream(user_input, thread)) print("Full agent response:", full_response.text) ``` @@ -769,18 +769,37 @@ arguments = KernelArguments(settings) response = await agent.get_response(user_input, thread=thread, arguments=arguments) ``` -**Solution**: Simplified options in Agent Framework +**Solution**: Simplified TypedDict-based options in Agent Framework -Agent Framework allows the passing of all parameters directly to the relevant methods, so that you don't have to import anything extra, or create any options objects, unless you want to. Internally, it uses a `ChatOptions` object for `ChatClients` and `ChatAgents`, which you can also create and pass in if you want to. This is also created in a `ChatAgent` to hold the options and can be overridden per call. +Agent Framework uses a TypedDict-based options system for `ChatClients` and `ChatAgents`. Options are passed via a single `options` parameter as a typed dictionary, with provider-specific TypedDict classes (like `OpenAIChatOptions`) for full IDE autocomplete and type checking. ```python -agent = ... +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() -response = await agent.run(user_input, thread, max_tokens=1000, frequency_penalty=0.5) +# Set default options at agent creation +agent = client.create_agent( + instructions="You are a helpful assistant.", + default_options={ + "max_tokens": 1000, + "temperature": 0.7, + } +) + +# Override options per call +response = await agent.run( + user_input, + thread, + options={ + "max_tokens": 500, + "frequency_penalty": 0.5, + } +) ``` > [!NOTE] -> The above is specific to a `ChatAgent`, because other agents might have different options, they should all accepts `messages` as a parameter, since that is defined in the `AgentProtocol`. +> The `tools` and `instructions` parameters remain as direct keyword arguments on agent creation and `run()` methods, and are not passed via the `options` dictionary. See the [Typed Options Upgrade Guide](../../support/upgrade/typed-options-guide-python.md) for detailed migration patterns. ::: zone-end diff --git a/agent-framework/support/upgrade/TOC.yml b/agent-framework/support/upgrade/TOC.yml index 8efb8b5bf..81028e120 100644 --- a/agent-framework/support/upgrade/TOC.yml +++ b/agent-framework/support/upgrade/TOC.yml @@ -1,2 +1,4 @@ - name: Upgrade Guide - Workflow APIs and Request-Response System in Python - href: requests-and-responses-upgrade-guide-python.md \ No newline at end of file + href: requests-and-responses-upgrade-guide-python.md +- name: Upgrade Guide - Python Options based on TypedDicts + href: typed-options-guide-python.md diff --git a/agent-framework/support/upgrade/typed-options-guide-python.md b/agent-framework/support/upgrade/typed-options-guide-python.md new file mode 100644 index 000000000..38de39f39 --- /dev/null +++ b/agent-framework/support/upgrade/typed-options-guide-python.md @@ -0,0 +1,617 @@ +--- +title: Upgrade Guide - Chat Client and Chat Agent options through TypedDicts +description: Guide on upgrading chat client and chat agent options to use TypedDicts in the Agent Framework. +author: eavanvalkenburg +ms.topic: tutorial +ms.author: edvan +ms.date: 01/15/2026 +ms.service: agent-framework +--- + +# Upgrade Guide: Chat Options as TypedDict with Generics + +This guide helps you upgrade your Python code to the new TypedDict-based `Options` system introduced in version [1.0.0b260114](https://github.com/microsoft/agent-framework/releases/tag/python-1.0.0b260114) of the Microsoft Agent Framework. This is a **breaking change** that provides improved type safety, IDE autocomplete, and runtime extensibility. + +## Overview of Changes + +This release introduces a major refactoring of how options are passed to chat clients and chat agents. + +### How It Worked Before + +Previously, options were passed as **direct keyword arguments** on methods like `get_response()`, `get_streaming_response()`, `run()`, and agent constructors: + +```python +# Options were individual keyword arguments +response = await client.get_response( + "Hello!", + model_id="gpt-4", + temperature=0.7, + max_tokens=1000, +) + +# For provider-specific options not in the base set, you used additional_properties +response = await client.get_response( + "Hello!", + model_id="gpt-4", + additional_properties={"reasoning_effort": "medium"}, +) +``` + +### How It Works Now + +Most options are now passed through a single `options` parameter as a typed dictionary: + +```python +# Most options go in a single typed dict +response = await client.get_response( + "Hello!", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "max_tokens": 1000, + "reasoning_effort": "medium", # Provider-specific options included directly + }, +) +``` + +> **Note:** For **Agents**, the `instructions` and `tools` parameters remain available as direct keyword arguments on `ChatAgent.__init__()` and `client.create_agent()`. For `agent.run()`, only `tools` is available as a keyword argument: +> +> ```python +> # Agent creation accepts both tools and instructions as keyword arguments +> agent = ChatAgent( +> chat_client=client, +> tools=[my_function], +> instructions="You are a helpful assistant.", +> default_options={"model_id": "gpt-4", "temperature": 0.7}, +> ) +> +> # agent.run() only accepts tools as a keyword argument +> response = await agent.run( +> "Hello!", +> tools=[another_function], # Can override tools per-run +> ) +> ``` + +### Key Changes + +1. **Consolidated Options Parameter**: Most keyword arguments (`model_id`, `temperature`, etc.) are now passed via a single `options` dict +2. **Exception for Agent Creation**: `instructions` and `tools` remain available as direct keyword arguments on `ChatAgent.__init__()` and `create_agent()` +3. **Exception for Agent Run**: `tools` remains available as a direct keyword argument on `agent.run()` +4. **TypedDict-based Options**: Options are defined as `TypedDict` classes for type safety +5. **Generic Type Support**: Chat clients and agents support generics for provider-specific options, to allow runtime overloads +6. **Provider-specific Options**: Each provider has its own default TypedDict (e.g., `OpenAIChatOptions`, `OllamaChatOptions`) +7. **No More additional_properties**: Provider-specific parameters are now first-class typed fields + +### Benefits + +- **Type Safety**: IDE autocomplete and type checking for all options +- **Provider Flexibility**: Support for provider-specific parameters on day one +- **Cleaner Code**: Consistent dict-based parameter passing +- **Easier Extension**: Create custom options for specialized use cases (e.g., reasoning models or other API backends) + +## Migration Guide + +### 1. Convert Keyword Arguments to Options Dict + +The most common change is converting individual keyword arguments to the `options` dictionary. + +**Before (keyword arguments):** + +```python +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() + +# Options passed as individual keyword arguments +response = await client.get_response( + "Hello!", + model_id="gpt-4", + temperature=0.7, + max_tokens=1000, +) + +# Streaming also used keyword arguments +async for chunk in client.get_streaming_response( + "Tell me a story", + model_id="gpt-4", + temperature=0.9, +): + print(chunk.text, end="") +``` + +**After (options dict):** + +```python +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() + +# All options now go in a single 'options' parameter +response = await client.get_response( + "Hello!", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "max_tokens": 1000, + }, +) + +# Same pattern for streaming +async for chunk in client.get_streaming_response( + "Tell me a story", + options={ + "model_id": "gpt-4", + "temperature": 0.9, + }, +): + print(chunk.text, end="") +``` + +If you pass options that are not appropriate for that client, you will get a type error in your IDE. + +### 2. Using Provider-Specific Options (No More additional_properties) + +Previously, to pass provider-specific parameters that weren't part of the base set of keyword arguments, you had to use the `additional_properties` parameter: + +**Before (using additional_properties):** + +```python +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() +response = await client.get_response( + "What is 2 + 2?", + model_id="gpt-4", + temperature=0.7, + additional_properties={ + "reasoning_effort": "medium", # No type checking or autocomplete + }, +) +``` + +**After (direct options with TypedDict):** + +```python +from agent_framework.openai import OpenAIChatClient + +# Provider-specific options are now first-class citizens with full type support +client = OpenAIChatClient() +response = await client.get_response( + "What is 2 + 2?", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "reasoning_effort": "medium", # Type checking or autocomplete + }, +) +``` + +**After (custom subclassing for new parameters):** + +Or if it is a parameter that is not yet part of Agent Framework (because it is new, or because it is custom for a OpenAI compatible backend), you can now subclass the options and use the generic support: + +```python +from typing import Literal +from agent_framework.openai import OpenAIChatOptions, OpenAIChatClient + +class MyCustomOpenAIChatOptions(OpenAIChatOptions, total=False): + """Custom OpenAI chat options with additional parameters.""" + + # New or custom parameters + custom_param: str + +# Use with the client +client = OpenAIChatClient[MyCustomOpenAIChatOptions]() +response = await client.get_response( + "Hello!", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "custom_param": "my_value", # IDE autocomplete works! + }, +) +``` + +The key benefit is that most provider-specific parameters are now part of the typed options dictionary, giving you: +- **IDE autocomplete** for all available options +- **Type checking** to catch invalid keys or values +- **No need for additional_properties** for known provider parameters +- **Easy extension** for custom or new parameters + +### 3. Update ChatAgent Configuration + +ChatAgent initialization and run methods follow the same pattern: + +**Before (keyword arguments on constructor and run):** + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() + +# Default options as keyword arguments on constructor +agent = ChatAgent( + chat_client=client, + name="assistant", + model_id="gpt-4", + temperature=0.7, +) + +# Run also took keyword arguments +response = await agent.run( + "Hello!", + max_tokens=1000, +) +``` + +**After:** + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions + +client = OpenAIChatClient() +agent = ChatAgent( + chat_client=client, + name="assistant", + default_options={ # <- type checkers will verify this dict + "model_id": "gpt-4", + "temperature": 0.7, + }, +) + +response = await agent.run("Hello!", options={ # <- and this dict too + "max_tokens": 1000, +}) +``` + +### 4. Provider-Specific Options + +Each provider now has its own TypedDict for options, these are enabled by default. This allows you to use provider-specific parameters with full type safety: + +**OpenAI Example:** + +```python +from agent_framework.openai import OpenAIChatClient + +client = OpenAIChatClient() +response = await client.get_response( + "Hello!", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "reasoning_effort": "medium", + }, +) +``` + +But you can also make it explicit: + +```python +from agent_framework_anthropic import AnthropicClient, AnthropicChatOptions + +client = AnthropicClient[AnthropicChatOptions]() +response = await client.get_response( + "Hello!", + options={ + "model_id": "claude-3-opus-20240229", + "max_tokens": 1000, + }, +) +``` + + +### 5. Creating Custom Options for Specialized Models + +One powerful feature of the new system is the ability to create custom TypedDict options for specialized models. This is particularly useful for models that have unique parameters, such as reasoning models with OpenAI: + +```python +from typing import Literal +from agent_framework.openai import OpenAIChatOptions, OpenAIChatClient + +class OpenAIReasoningChatOptions(OpenAIChatOptions, total=False): + """Chat options for OpenAI reasoning models (o1, o3, o4-mini, etc.).""" + + # Reasoning-specific parameters + reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh"] + + # Unsupported parameters for reasoning models (override with None) + temperature: None + top_p: None + frequency_penalty: None + presence_penalty: None + logit_bias: None + logprobs: None + top_logprobs: None + stop: None + + +# Use with the client +client = OpenAIChatClient[OpenAIReasoningChatOptions]() +response = await client.get_response( + "What is 2 + 2?", + options={ + "model_id": "o3", + "max_tokens": 100, + "allow_multiple_tool_calls": True, + "reasoning_effort": "medium", # IDE autocomplete works! + # "temperature": 0.7, # Would raise a type error, because the value is not None + }, +) +``` + +### 6. Chat Agents with Options + +The generic setup has also been extended to Chat Agents: + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient + +agent = ChatAgent( + chat_client=OpenAIChatClient[OpenAIReasoningChatOptions](), + default_options={ + "model_id": "o3", + "max_tokens": 100, + "allow_multiple_tool_calls": True, + "reasoning_effort": "medium", + }, +) +``` +and you can specify the generic on both the client and the agent, so this is also valid: + +```python +from agent_framework import ChatAgent +from agent_framework.openai import OpenAIChatClient + +agent = ChatAgent[OpenAIReasoningChatOptions]( + chat_client=OpenAIChatClient(), + default_options={ + "model_id": "o3", + "max_tokens": 100, + "allow_multiple_tool_calls": True, + "reasoning_effort": "medium", + }, +) +``` + +### 6. Update Custom Chat Client Implementations + +If you have implemented a custom chat client by extending `BaseChatClient`, update the internal methods: + +**Before:** + +```python +from agent_framework import BaseChatClient, ChatMessage, ChatOptions, ChatResponse + +class MyCustomClient(BaseChatClient): + async def _inner_get_response( + self, + *, + messages: MutableSequence[ChatMessage], + chat_options: ChatOptions, + **kwargs: Any, + ) -> ChatResponse: + # Access options via class attributes + model = chat_options.model_id + temp = chat_options.temperature + # ... +``` + +**After:** + +```python +from typing import Generic +from agent_framework import BaseChatClient, ChatMessage, ChatOptions, ChatResponse + +# Define your provider's options TypedDict +class MyCustomChatOptions(ChatOptions, total=False): + my_custom_param: str + +# This requires the TypeVar from Python 3.13+ or from typing_extensions, so for Python 3.13+: +from typing import TypeVar + +TOptions = TypeVar("TOptions", bound=TypedDict, default=MyCustomChatOptions, covariant=True) + +class MyCustomClient(BaseChatClient[TOptions], Generic[TOptions]): + async def _inner_get_response( + self, + *, + messages: MutableSequence[ChatMessage], + options: dict[str, Any], # Note: parameter renamed and just a dict + **kwargs: Any, + ) -> ChatResponse: + # Access options via dict access + model = options.get("model_id") + temp = options.get("temperature") + # ... +``` + +## Common Migration Patterns + +### Pattern 1: Simple Parameter Update + +```python +# Before - keyword arguments +await client.get_response("Hello", temperature=0.7) + +# After - options dict +await client.get_response("Hello", options={"temperature": 0.7}) +``` + +### Pattern 2: Multiple Parameters + +```python +# Before - multiple keyword arguments +await client.get_response( + "Hello", + model_id="gpt-4", + temperature=0.7, + max_tokens=1000, +) + +# After - all in options dict +await client.get_response( + "Hello", + options={ + "model_id": "gpt-4", + "temperature": 0.7, + "max_tokens": 1000, + }, +) +``` + +### Pattern 3: Chat Client with Tools + +For chat clients, `tools` now goes in the options dict: + +```python +# Before - tools as keyword argument on chat client +await client.get_response( + "What's the weather?", + model_id="gpt-4", + tools=[my_function], + tool_choice="auto", +) + +# After - tools in options dict for chat clients +await client.get_response( + "What's the weather?", + options={ + "model_id": "gpt-4", + "tools": [my_function], + "tool_choice": "auto", + }, +) +``` + +### Pattern 4: Agent with Tools and Instructions + +For agent creation, `tools` and `instructions` can remain as keyword arguments. For `run()`, only `tools` is available: + +```python +# Before +agent = ChatAgent( + chat_client=client, + name="assistant", + tools=[my_function], + instructions="You are helpful.", + model_id="gpt-4", +) + +# After - tools and instructions stay as keyword args on creation +agent = ChatAgent( + chat_client=client, + name="assistant", + tools=[my_function], # Still a keyword argument! + instructions="You are helpful.", # Still a keyword argument! + default_options={"model_id": "gpt-4"}, +) + +# For run(), only tools is available as keyword argument +response = await agent.run( + "Hello!", + tools=[another_function], # Can override tools + options={"max_tokens": 100}, +) +``` + +```python +# Before - using additional_properties +await client.get_response( + "Solve this problem", + model_id="o3", + additional_properties={"reasoning_effort": "high"}, +) + +# After - directly in options +await client.get_response( + "Solve this problem", + options={ + "model_id": "o3", + "reasoning_effort": "high", + }, +) +``` + +### Pattern 5: Provider-Specific Parameters + +```python +# Define reusable options +my_options: OpenAIChatOptions = { + "model_id": "gpt-4", + "temperature": 0.7, +} + +# Use with different messages +await client.get_response("Hello", options=my_options) +await client.get_response("Goodbye", options=my_options) + +# Extend options using dict merge +extended_options = {**my_options, "max_tokens": 500} +``` + +## Summary of Breaking Changes + +| Aspect | Before | After | +|--------|--------|-------| +| Chat client options | Individual keyword arguments (`temperature=0.7`) | Single `options` dict (`options={"temperature": 0.7}`) | +| Chat client tools | `tools=[...]` keyword argument | `options={"tools": [...]}` | +| Agent creation `tools` and `instructions` | Keyword arguments | **Still keyword arguments** (unchanged) | +| Agent `run()` `tools` | Keyword argument | **Still keyword argument** (unchanged) | +| Agent `run()` `instructions` | Keyword argument | Moved to `options={"instructions": ...}` | +| Provider-specific options | `additional_properties={...}` | Included directly in `options` dict | +| Agent default options | Keyword arguments on constructor | `default_options={...}` | +| Agent run options | Keyword arguments on `run()` | `options={...}` parameter | +| Client typing | `OpenAIChatClient()` | `OpenAIChatClient[CustomOptions]()` (optional) | +| Agent typing | `ChatAgent(...)` | `ChatAgent[CustomOptions](...)` (optional) | + +## Testing Your Migration + +### ChatClient Updates + +1. Find all calls to `get_response()` and `get_streaming_response()` that use keyword arguments like `model_id=`, `temperature=`, `tools=`, etc. +2. Move all keyword arguments into an `options={...}` dictionary +3. Move any `additional_properties` values directly into the `options` dict + +### ChatAgent Updates + +1. Find all `ChatAgent` constructors and `run()` calls that use keyword arguments +2. Move keyword arguments on constructors to `default_options={...}` +3. Move keyword arguments on `run()` to `options={...}` +4. **Exception**: `tools` and `instructions` can remain as keyword arguments on `ChatAgent.__init__()` and `create_agent()` +5. **Exception**: `tools` can remain as a keyword argument on `run()` + +### Custom Chat Client Updates + +1. Update the `_inner_get_response()` and `_inner_get_streaming_response()` method signatures: change `chat_options: ChatOptions` parameter to `options: dict[str, Any]` +2. Update attribute access (e.g., `chat_options.model_id`) to dict access (e.g., `options.get("model_id")`) +3. **(Optional)** If using non-standard parameters: Define a custom TypedDict +4. Add generic type parameters to your client class + +### For All + +1. **Run Type Checker**: Use `mypy` or `pyright` to catch type errors +2. **Test End-to-End**: Run your application to verify functionality + +## IDE Support + +The new TypedDict-based system provides excellent IDE support: + +- **Autocomplete**: Get suggestions for all available options +- **Type Checking**: Catch invalid option keys at development time +- **Documentation**: Hover over keys to see descriptions +- **Provider-specific**: Each provider's options show only relevant parameters + +## Next Steps + +To see the typed dicts in action for the case of using OpenAI Reasoning Models with the Chat Completion API, explore [this sample](https://github.com/microsoft/agent-framework/blob/main/python/samples/getting_started/chat_client/typed_options.py) + +After completing the migration: + +1. Explore provider-specific options in the [API documentation](../../api-docs/TOC.yml) +2. Review updated [samples](https://github.com/microsoft/agent-framework/tree/main/python/samples) +3. Learn about creating [custom chat clients](../../user-guide/agents/agent-types/custom-agent.md) + +For additional help, refer to the [Agent Framework documentation](../../overview/agent-framework-overview.md) or reach out to the community. diff --git a/agent-framework/tutorials/agents/function-tools-approvals.md b/agent-framework/tutorials/agents/function-tools-approvals.md index f486637a4..ac1e61014 100644 --- a/agent-framework/tutorials/agents/function-tools-approvals.md +++ b/agent-framework/tutorials/agents/function-tools-approvals.md @@ -67,7 +67,7 @@ You can check the response content for any `FunctionApprovalRequestContent` inst ```csharp AgentThread thread = agent.GetNewThread(); -AgentRunResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", thread); +AgentResponse response = await agent.RunAsync("What is the weather like in Amsterdam?", thread); var functionApprovalRequests = response.Messages .SelectMany(x => x.Contents) diff --git a/agent-framework/tutorials/agents/middleware.md b/agent-framework/tutorials/agents/middleware.md index 53f161e88..0ef55e8eb 100644 --- a/agent-framework/tutorials/agents/middleware.md +++ b/agent-framework/tutorials/agents/middleware.md @@ -62,7 +62,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -async Task CustomAgentRunMiddleware( +async Task CustomAgentRunMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, diff --git a/agent-framework/tutorials/agents/orchestrate-durable-agents.md b/agent-framework/tutorials/agents/orchestrate-durable-agents.md index 26c12ff58..f0e36aadb 100644 --- a/agent-framework/tutorials/agents/orchestrate-durable-agents.md +++ b/agent-framework/tutorials/agents/orchestrate-durable-agents.md @@ -184,15 +184,15 @@ public static class AgentOrchestration // Step 1: Get the main agent's response DurableAIAgent mainAgent = context.GetAgent("MyDurableAgent"); - AgentRunResponse mainResponse = await mainAgent.RunAsync(input); + AgentResponse 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); + 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); diff --git a/agent-framework/tutorials/agents/run-agent.md b/agent-framework/tutorials/agents/run-agent.md index bef4ca87a..8e411f878 100644 --- a/agent-framework/tutorials/agents/run-agent.md +++ b/agent-framework/tutorials/agents/run-agent.md @@ -67,7 +67,7 @@ AIAgent agent = new AzureOpenAIClient( ## Running the agent To run the agent, call the `RunAsync` method on the agent instance, providing the user input. -The agent will return an `AgentRunResponse` object, and calling `.ToString()` or `.Text` on this response object, provides the text result from the agent. +The agent will return an `AgentResponse` object, and calling `.ToString()` or `.Text` on this response object, provides the text result from the agent. ```csharp Console.WriteLine(await agent.RunAsync("Tell me a joke about a pirate.")); @@ -84,7 +84,7 @@ Because he wanted to improve his "arrr-ticulation"! 🏴‍☠️ ## Running the agent with streaming To run the agent with streaming, call the `RunStreamingAsync` method on the agent instance, providing the user input. -The agent will return a stream `AgentRunResponseUpdate` objects, and calling `.ToString()` or `.Text` on each update object provides the part of the text result contained in that update. +The agent will return a stream `AgentResponseUpdate` objects, and calling `.ToString()` or `.Text` on each update object provides the part of the text result contained in that update. ```csharp await foreach (var update in agent.RunStreamingAsync("Tell me a joke about a pirate.")) diff --git a/agent-framework/tutorials/agents/structured-output.md b/agent-framework/tutorials/agents/structured-output.md index e6b262ec8..0aa44794d 100644 --- a/agent-framework/tutorials/agents/structured-output.md +++ b/agent-framework/tutorials/agents/structured-output.md @@ -108,7 +108,7 @@ You must assemble all the updates into a single response before deserializing it ```csharp var updates = agent.RunStreamingAsync("Please provide information about John Smith, who is a 35-year-old software engineer."); -personInfo = (await updates.ToAgentRunResponseAsync()).Deserialize(JsonSerializerOptions.Web); +personInfo = (await updates.ToAgentResponseAsync()).Deserialize(JsonSerializerOptions.Web); ``` ::: zone-end @@ -181,11 +181,11 @@ else: When streaming, the agent response is streamed as a series of updates. To get the structured output, you must collect all the updates and then access the final response value: ```python -from agent_framework import AgentRunResponse +from agent_framework import AgentResponse -# Get structured response from streaming agent using AgentRunResponse.from_agent_response_generator -# This method collects all streaming updates and combines them into a single AgentRunResponse -final_response = await AgentRunResponse.from_agent_response_generator( +# Get structured response from streaming agent using AgentResponse.from_agent_response_generator +# This method collects all streaming updates and combines them into a single AgentResponse +final_response = await AgentResponse.from_agent_response_generator( agent.run_stream(query, response_format=PersonInfo), output_format_type=PersonInfo, ) 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 index e49aa36c4..8223e9454 100644 --- a/agent-framework/tutorials/plugins/use-purview-with-agent-framework-sdk.md +++ b/agent-framework/tutorials/plugins/use-purview-with-agent-framework-sdk.md @@ -67,7 +67,7 @@ AIAgent agent = new AzureOpenAIClient( .WithPurview(browserCredential, new PurviewSettings("My Secure Agent")) .Build(); -AgentRunResponse response = await agent.RunAsync("Summarize zero trust in one sentence.").ConfigureAwait(false); +AgentResponse response = await agent.RunAsync("Summarize zero trust in one sentence.").ConfigureAwait(false); Console.WriteLine(response); ``` diff --git a/agent-framework/tutorials/workflows/agents-in-workflows.md b/agent-framework/tutorials/workflows/agents-in-workflows.md index 6250c634e..9545995a1 100644 --- a/agent-framework/tutorials/workflows/agents-in-workflows.md +++ b/agent-framework/tutorials/workflows/agents-in-workflows.md @@ -138,7 +138,7 @@ Run the workflow with streaming to observe real-time updates from all agents: await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is AgentRunUpdateEvent executorComplete) + if (evt is AgentResponseUpdateEvent executorComplete) { Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}"); } @@ -163,14 +163,14 @@ Properly clean up the Azure Foundry agents after use: 2. **Agent Creation**: Creates persistent agents on Azure Foundry with specific instructions for translation 3. **Sequential Processing**: French agent translates input first, then Spanish agent, then English agent 4. **Turn Token Pattern**: Agents cache messages and only process when they receive a `TurnToken` -5. **Streaming Updates**: `AgentRunUpdateEvent` provides real-time token updates as agents generate responses +5. **Streaming Updates**: `AgentResponseUpdateEvent` provides real-time token updates as agents generate responses 6. **Resource Management**: Proper cleanup of Azure Foundry agents using the Administration API ## Key Concepts - **Azure Foundry Agent Service**: Cloud-based AI agents with advanced reasoning capabilities - **PersistentAgentsClient**: Client for creating and managing agents on Azure Foundry -- **AgentRunUpdateEvent**: Real-time streaming updates during agent execution +- **AgentResponseUpdateEvent**: Real-time streaming updates during agent execution - **TurnToken**: Signal that triggers agent processing after message caching - **Sequential Workflow**: Agents connected in a pipeline where output flows from one to the next @@ -216,7 +216,7 @@ from collections.abc import Awaitable, Callable from contextlib import AsyncExitStack from typing import Any -from agent_framework import AgentRunUpdateEvent, WorkflowBuilder, WorkflowOutputEvent +from agent_framework import AgentResponseUpdateEvent, WorkflowBuilder, WorkflowOutputEvent from agent_framework.azure import AzureAIAgentClient from azure.identity.aio import AzureCliCredential ``` @@ -290,7 +290,7 @@ Run the workflow with streaming to observe real-time updates from both agents: events = workflow.run_stream("Create a slogan for a new electric SUV that is affordable and fun to drive.") async for event in events: - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdateEvent): # Handle streaming updates from agents eid = event.executor_id if eid != last_executor_id: @@ -320,13 +320,13 @@ if __name__ == "__main__": 1. **Azure AI Client Setup**: Uses `AzureAIAgentClient` with Azure CLI credentials for authentication 2. **Agent Factory Pattern**: Creates a factory function that manages async context lifecycle for multiple agents 3. **Sequential Processing**: Writer agent generates content first, then passes it to the Reviewer agent -4. **Streaming Updates**: `AgentRunUpdateEvent` provides real-time token updates as agents generate responses +4. **Streaming Updates**: `AgentResponseUpdateEvent` provides real-time token updates as agents generate responses 5. **Context Management**: Proper cleanup of Azure AI resources using `AsyncExitStack` ## Key Concepts - **Azure AI Agent Service**: Cloud-based AI agents with advanced reasoning capabilities -- **AgentRunUpdateEvent**: Real-time streaming updates during agent execution +- **AgentResponseUpdateEvent**: Real-time streaming updates during agent execution - **AsyncExitStack**: Proper async context management for multiple resources - **Agent Factory Pattern**: Reusable agent creation with shared client configuration - **Sequential Workflow**: Agents connected in a pipeline where output flows from one to the next diff --git a/agent-framework/user-guide/agents/agent-background-responses.md b/agent-framework/user-guide/agents/agent-background-responses.md index 6ed736b91..f1472316a 100644 --- a/agent-framework/user-guide/agents/agent-background-responses.md +++ b/agent-framework/user-guide/agents/agent-background-responses.md @@ -69,7 +69,7 @@ AgentRunOptions options = new() AgentThread thread = agent.GetNewThread(); // Get initial response - may return with or without a continuation token -AgentRunResponse response = await agent.RunAsync("Write a very long novel about otters in space.", thread, options); +AgentResponse response = await agent.RunAsync("Write a very long novel about otters in space.", thread, options); // Continue to poll until the final response is received while (response.ContinuationToken is not null) @@ -110,7 +110,7 @@ AgentRunOptions options = new() AgentThread thread = agent.GetNewThread(); -AgentRunResponseUpdate? latestReceivedUpdate = null; +AgentResponseUpdate? latestReceivedUpdate = null; await foreach (var update in agent.RunStreamingAsync("Write a very long novel about otters in space.", thread, options)) { @@ -132,7 +132,7 @@ await foreach (var update in agent.RunStreamingAsync(thread, options)) ### Key Points: -- Each `AgentRunResponseUpdate` contains a continuation token that can be used for resumption +- Each `AgentResponseUpdate` contains a continuation token that can be used for resumption - Store the continuation token from the last received update before interruption - Use the stored continuation token to resume the stream from the interruption point diff --git a/agent-framework/user-guide/agents/agent-middleware.md b/agent-framework/user-guide/agents/agent-middleware.md index f3f24c81d..179e10ed5 100644 --- a/agent-framework/user-guide/agents/agent-middleware.md +++ b/agent-framework/user-guide/agents/agent-middleware.md @@ -72,7 +72,7 @@ var agent = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()) Here is an example of agent run middleware, that can inspect and/or modify the input and output from the agent run. ```csharp -async Task CustomAgentRunMiddleware( +async Task CustomAgentRunMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, @@ -91,7 +91,7 @@ async Task CustomAgentRunMiddleware( 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( + async IAsyncEnumerable CustomAgentRunStreamingMiddleware( IEnumerable messages, AgentThread? thread, AgentRunOptions? options, @@ -99,14 +99,14 @@ async IAsyncEnumerable CustomAgentRunStreamingMiddleware [EnumeratorCancellation] CancellationToken cancellationToken) { Console.WriteLine(messages.Count()); - List updates = []; + 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); + Console.WriteLine(updates.ToAgentResponse().Messages.Count); } ``` @@ -238,7 +238,7 @@ Chat middleware intercepts chat requests sent to AI models. It uses the `ChatCon - `chat_client`: The chat client being invoked - `messages`: List of messages being sent to the AI service -- `chat_options`: The options for the chat request +- `options`: The options for the chat request - `is_streaming`: Boolean indicating if this is a streaming invocation - `metadata`: Dictionary for storing additional data between middleware - `result`: The chat response from the AI (can be modified) @@ -451,8 +451,8 @@ Middleware can override results in both non-streaming and streaming scenarios, a The result type in `context.result` depends on whether the agent invocation is streaming or non-streaming: -- **Non-streaming**: `context.result` contains an `AgentRunResponse` with the complete response -- **Streaming**: `context.result` contains an async generator that yields `AgentRunResponseUpdate` chunks +- **Non-streaming**: `context.result` contains an `AgentResponse` with the complete response +- **Streaming**: `context.result` contains an async generator that yields `AgentResponseUpdate` chunks You can use `context.is_streaming` to differentiate between these scenarios and handle result overrides appropriately. @@ -477,15 +477,15 @@ async def weather_override_middleware( if context.is_streaming: # Streaming override - async def override_stream() -> AsyncIterable[AgentRunResponseUpdate]: + async def override_stream() -> AsyncIterable[AgentResponseUpdate]: for chunk in custom_message_parts: - yield AgentRunResponseUpdate(contents=[TextContent(text=chunk)]) + yield AgentResponseUpdate(contents=[TextContent(text=chunk)]) context.result = override_stream() else: # Non-streaming override custom_message = "".join(custom_message_parts) - context.result = AgentRunResponse( + context.result = AgentResponse( messages=[ChatMessage(role=Role.ASSISTANT, text=custom_message)] ) ``` 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 cc4034a99..db5720da0 100644 --- a/agent-framework/user-guide/agents/agent-types/anthropic-agent.md +++ b/agent-framework/user-guide/agents/agent-types/anthropic-agent.md @@ -331,14 +331,15 @@ Anthropic supports extended thinking capabilities through the `thinking` feature ```python from agent_framework import TextReasoningContent, UsageContent +from agent_framework.anthropic import AnthropicClient async def thinking_example(): agent = AnthropicClient().create_agent( name="DocsAgent", instructions="You are a helpful agent.", tools=[HostedWebSearchTool()], - max_tokens=20000, - additional_chat_options={ + default_options={ + "max_tokens": 20000, "thinking": {"type": "enabled", "budget_tokens": 10000} }, ) @@ -359,6 +360,65 @@ async def thinking_example(): print() ``` +### Anthropic Skills + +Anthropic provides managed skills that extend agent capabilities, such as creating PowerPoint presentations. Skills require the Code Interpreter tool to function: + +```python +from agent_framework import HostedCodeInterpreterTool, HostedFileContent +from agent_framework.anthropic import AnthropicClient + +async def skills_example(): + # Create client with skills beta flag + client = AnthropicClient(additional_beta_flags=["skills-2025-10-02"]) + + # Create an agent with the pptx skill enabled + # Skills require the Code Interpreter tool + agent = client.create_agent( + name="PresentationAgent", + instructions="You are a helpful agent for creating PowerPoint presentations.", + tools=HostedCodeInterpreterTool(), + default_options={ + "max_tokens": 20000, + "thinking": {"type": "enabled", "budget_tokens": 10000}, + "container": { + "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] + }, + }, + ) + + query = "Create a presentation about renewable energy with 5 slides" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + + files: list[HostedFileContent] = [] + async for chunk in agent.run_stream(query): + for content in chunk.contents: + match content.type: + case "text": + print(content.text, end="", flush=True) + case "text_reasoning": + print(f"\033[32m{content.text}\033[0m", end="", flush=True) + case "hosted_file": + # Catch generated files + files.append(content) + + print("\n") + + # Download generated files + if files: + print("Generated files:") + for idx, file in enumerate(files): + file_content = await client.anthropic_client.beta.files.download( + file_id=file.file_id, + betas=["files-api-2025-04-14"] + ) + filename = f"presentation-{idx}.pptx" + with open(filename, "wb") as f: + await file_content.write_to_file(f.name) + print(f"File {idx}: {filename} saved to disk.") +``` + ## Using the Agent The agent is a standard `BaseAgent` and supports all standard agent operations. 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 ab3af981d..30559c417 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 --pre +pip install agent-framework-core --pre ``` ## Getting Started 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 ea73bf759..c86de0f50 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 --pre +pip install agent-framework-core --pre ``` ## Getting Started 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 9aadde779..eef41b294 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 @@ -9,7 +9,7 @@ ms.date: 09/25/2025 ms.service: agent-framework --- -# Agent based on any IChatClient +# Agent based on any Chat Client ::: zone pivot="programming-language-csharp" 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 f45138873..f475ca0a4 100644 --- a/agent-framework/user-guide/agents/agent-types/custom-agent.md +++ b/agent-framework/user-guide/agents/agent-types/custom-agent.md @@ -108,12 +108,12 @@ The thread can then be updated with the new messages by calling `NotifyThreadOfN If you don't do this, the user won't be able to have a multi-turn conversation with the agent and each run will be a fresh interaction. ```csharp - public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) { thread ??= this.GetNewThread(); List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList(); await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken); - return new AgentRunResponse + return new AgentResponse { AgentId = this.Id, ResponseId = Guid.NewGuid().ToString(), @@ -121,14 +121,14 @@ If you don't do this, the user won't be able to have a multi-turn conversation w }; } - public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public override async IAsyncEnumerable RunStreamingAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { thread ??= this.GetNewThread(); List responseMessages = CloneAndToUpperCase(messages, this.DisplayName).ToList(); await NotifyThreadOfNewMessagesAsync(thread, messages.Concat(responseMessages), cancellationToken); foreach (var message in responseMessages) { - yield return new AgentRunResponseUpdate + yield return new AgentResponseUpdate { AgentId = this.Id, AuthorName = this.DisplayName, @@ -170,7 +170,7 @@ pip install agent-framework-core --pre The framework provides the `AgentProtocol` protocol that defines the interface all agents must implement. Custom agents can either implement this protocol directly or extend the `BaseAgent` class for convenience. ```python -from agent_framework import AgentProtocol, AgentRunResponse, AgentRunResponseUpdate, AgentThread, ChatMessage +from agent_framework import AgentProtocol, AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage from collections.abc import AsyncIterable from typing import Any @@ -188,7 +188,7 @@ class MyCustomAgent(AgentProtocol): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AgentRunResponse: + ) -> AgentResponse: """Execute the agent and return a complete response.""" ... @@ -198,7 +198,7 @@ class MyCustomAgent(AgentProtocol): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AsyncIterable[AgentRunResponseUpdate]: + ) -> AsyncIterable[AgentResponseUpdate]: """Execute the agent and yield streaming response updates.""" ... ``` @@ -210,8 +210,8 @@ The recommended approach is to extend the `BaseAgent` class, which provides comm ```python from agent_framework import ( BaseAgent, - AgentRunResponse, - AgentRunResponseUpdate, + AgentResponse, + AgentResponseUpdate, AgentThread, ChatMessage, Role, @@ -255,7 +255,7 @@ class EchoAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AgentRunResponse: + ) -> AgentResponse: """Execute the agent and return a complete response. Args: @@ -264,7 +264,7 @@ class EchoAgent(BaseAgent): **kwargs: Additional keyword arguments. Returns: - An AgentRunResponse containing the agent's reply. + An AgentResponse containing the agent's reply. """ # Normalize input messages to a list normalized_messages = self._normalize_messages(messages) @@ -288,7 +288,7 @@ class EchoAgent(BaseAgent): if thread is not None: await self._notify_thread_of_new_messages(thread, normalized_messages, response_message) - return AgentRunResponse(messages=[response_message]) + return AgentResponse(messages=[response_message]) async def run_stream( self, @@ -296,7 +296,7 @@ class EchoAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AsyncIterable[AgentRunResponseUpdate]: + ) -> AsyncIterable[AgentResponseUpdate]: """Execute the agent and yield streaming response updates. Args: @@ -305,7 +305,7 @@ class EchoAgent(BaseAgent): **kwargs: Additional keyword arguments. Yields: - AgentRunResponseUpdate objects containing chunks of the response. + AgentResponseUpdate objects containing chunks of the response. """ # Normalize input messages to a list normalized_messages = self._normalize_messages(messages) @@ -326,7 +326,7 @@ class EchoAgent(BaseAgent): # Add space before word except for the first one chunk_text = f" {word}" if i > 0 else word - yield AgentRunResponseUpdate( + yield AgentResponseUpdate( contents=[TextContent(text=chunk_text)], role=Role.ASSISTANT, ) 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 index 92b7425a7..817d2da6f 100644 --- a/agent-framework/user-guide/agents/agent-types/durable-agent/features.md +++ b/agent-framework/user-guide/agents/agent-types/durable-agent/features.md @@ -44,7 +44,7 @@ public static async Task SpamDetectionOrchestration( DurableAIAgent spamDetectionAgent = context.GetAgent("SpamDetectionAgent"); AgentThread spamThread = spamDetectionAgent.GetNewThread(); - AgentRunResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( + AgentResponse spamDetectionResponse = await spamDetectionAgent.RunAsync( message: $"Analyze this email for spam: {email.EmailContent}", thread: spamThread); DetectionResult result = spamDetectionResponse.Result; @@ -58,7 +58,7 @@ public static async Task SpamDetectionOrchestration( DurableAIAgent emailAssistantAgent = context.GetAgent("EmailAssistantAgent"); AgentThread emailThread = emailAssistantAgent.GetNewThread(); - AgentRunResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( + AgentResponse emailAssistantResponse = await emailAssistantAgent.RunAsync( message: $"Draft a professional response to: {email.EmailContent}", thread: emailThread); @@ -148,11 +148,11 @@ public static async Task ResearchOrchestration( DurableAIAgent competitorAgent = context.GetAgent("CompetitorResearchAgent"); // Start all agent runs concurrently - Task> technicalTask = + Task> technicalTask = technicalAgent.RunAsync($"Research technical aspects of {topic}"); - Task> marketTask = + Task> marketTask = marketAgent.RunAsync($"Research market trends for {topic}"); - Task> competitorTask = + Task> competitorTask = competitorAgent.RunAsync($"Research competitors in {topic}"); // Wait for all tasks to complete @@ -165,7 +165,7 @@ public static async Task ResearchOrchestration( competitorTask.Result.Result.Text); DurableAIAgent summaryAgent = context.GetAgent("SummaryAgent"); - AgentRunResponse summaryResponse = + AgentResponse summaryResponse = await summaryAgent.RunAsync($"Summarize this research:\n{allResearch}"); return summaryResponse.Result.Text; @@ -230,7 +230,7 @@ public static async Task ContentApprovalWorkflow( // Generate content using an agent DurableAIAgent contentAgent = context.GetAgent("ContentGenerationAgent"); - AgentRunResponse contentResponse = + AgentResponse contentResponse = await contentAgent.RunAsync($"Write an article about {topic}"); GeneratedContent draftContent = contentResponse.Result; diff --git a/agent-framework/user-guide/agents/agent-types/index.md b/agent-framework/user-guide/agents/agent-types/index.md index 81adfdd2a..c932b9c12 100644 --- a/agent-framework/user-guide/agents/agent-types/index.md +++ b/agent-framework/user-guide/agents/agent-types/index.md @@ -294,7 +294,7 @@ It is also possible to create fully custom agents that are not just wrappers aro Agent Framework provides the `AgentProtocol` protocol and `BaseAgent` base class, which when implemented/subclassed allows for complete control over the agent's behavior and capabilities. ```python -from agent_framework import BaseAgent, AgentRunResponse, AgentRunResponseUpdate, AgentThread, ChatMessage +from agent_framework import BaseAgent, AgentResponse, AgentResponseUpdate, AgentThread, ChatMessage from collections.abc import AsyncIterable class CustomAgent(BaseAgent): @@ -304,7 +304,7 @@ class CustomAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AgentRunResponse: + ) -> AgentResponse: # Custom agent implementation pass @@ -314,7 +314,7 @@ class CustomAgent(BaseAgent): *, thread: AgentThread | None = None, **kwargs: Any, - ) -> AsyncIterable[AgentRunResponseUpdate]: + ) -> AsyncIterable[AgentResponseUpdate]: # Custom streaming implementation pass ``` 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 840c6b32a..fbf0bfc18 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 @@ For more information on how to run and interact with agents, see the [Agent gett Install the Microsoft Agent Framework package. ```bash -pip install agent-framework --pre +pip install agent-framework-core --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 af40a4f1c..8aea7dcda 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 @@ For more information on how to run and interact with agents, see the [Agent gett Install the Microsoft Agent Framework package. ```bash -pip install agent-framework --pre +pip install agent-framework-core --pre ``` ## Configuration @@ -170,7 +170,7 @@ async def reasoning_example(): instructions="You are a personal math tutor. When asked a math question, " "write and run code to answer the question.", tools=HostedCodeInterpreterTool(), - reasoning={"effort": "high", "summary": "detailed"}, + default_options={"reasoning": {"effort": "high", "summary": "detailed"}}, ) print("Agent: ", end="", flush=True) @@ -191,7 +191,7 @@ Get responses in structured formats: ```python from pydantic import BaseModel -from agent_framework import AgentRunResponse +from agent_framework import AgentResponse class CityInfo(BaseModel): """A structured output for city information.""" @@ -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) + result = await agent.run("Tell me about Paris, France", options={"response_format": CityInfo}) if result.value: city_data = result.value @@ -214,7 +214,7 @@ async def structured_output_example(): # Streaming structured output structured_result = await AgentRunResponse.from_agent_response_generator( - agent.run_stream("Tell me about Tokyo, Japan", response_format=CityInfo), + agent.run_stream("Tell me about Tokyo, Japan", options={"response_format": CityInfo}), output_format_type=CityInfo, ) @@ -281,7 +281,7 @@ async def code_interpreter_with_files_example(): # Create the OpenAI client for file operations openai_client = AsyncOpenAI() - + # Create sample CSV data csv_data = """name,department,salary,years_experience Alice Johnson,Engineering,95000,5 @@ -291,7 +291,7 @@ David Brown,Marketing,68000,2 Emma Davis,Sales,82000,4 Frank Wilson,Engineering,88000,6 """ - + # Create temporary CSV file with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as temp_file: temp_file.write(csv_data) @@ -309,7 +309,7 @@ Frank Wilson,Engineering,88000,6 # Create agent using OpenAI Responses client agent = ChatAgent( - chat_client=OpenAIResponsesClient(), + chat_client=OpenAIResponsesClient(async_client=openai_client), instructions="You are a helpful assistant that can analyze data files using Python code.", tools=HostedCodeInterpreterTool(inputs=[{"file_id": uploaded_file.id}]), ) @@ -365,25 +365,25 @@ from agent_framework import HostedFileSearchTool, HostedVectorStoreContent async def file_search_example(): client = OpenAIResponsesClient() - + # Create a file with sample content file = await client.client.files.create( - file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), + file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), purpose="user_data" ) - + # Create a vector store for document storage vector_store = await client.client.vector_stores.create( name="knowledge_base", expires_after={"anchor": "last_active_at", "days": 1}, ) - + # Add file to vector store and wait for processing result = await client.client.vector_stores.files.create_and_poll( - vector_store_id=vector_store.id, + vector_store_id=vector_store.id, file_id=file.id ) - + # Check if processing was successful if result.last_error is not None: raise Exception(f"Vector store file processing failed with status: {result.last_error.message}") @@ -401,7 +401,7 @@ async def file_search_example(): # Test the file search message = "What is the weather today? Do a file search to find the answer." print(f"User: {message}") - + response = await agent.run(message) print(f"Agent: {response}") @@ -462,31 +462,35 @@ async def image_analysis_example(): Generate images using the Responses API: ```python -from agent_framework import DataContent, UriContent +from agent_framework import DataContent, HostedImageGenerationTool, ImageGenerationToolResultContent, UriContent async def image_generation_example(): agent = OpenAIResponsesClient().create_agent( instructions="You are a helpful AI that can generate images.", - tools=[{ - "type": "image_generation", - "size": "1024x1024", - "quality": "low", - }], + tools=[ + HostedImageGenerationTool( + options={ + "size": "1024x1024", + "output_format": "webp", + } + ) + ], ) 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)): - print(f"Image generated: {content.uri}") + for message in result.messages: + for content in message.contents: + if isinstance(content, ImageGenerationToolResultContent) and content.outputs: + for output in content.outputs: + if isinstance(output, (DataContent, UriContent)) and output.uri: + print(f"Image generated: {output.uri}") ``` -### Model Context Protocol (MCP) Tools - -#### Local MCP Tools +### MCP Tools -Connect to local MCP servers for extended capabilities: +Connect to MCP servers from within the agent for extended capabilities: ```python from agent_framework import MCPStreamableHTTPTool @@ -507,7 +511,7 @@ async def local_mcp_example(): #### Hosted MCP Tools -Use hosted MCP tools for extended capabilities: +Use hosted MCP tools to leverage server-side capabilities: ```python from agent_framework import HostedMCPTool diff --git a/agent-framework/user-guide/agents/running-agents.md b/agent-framework/user-guide/agents/running-agents.md index 339d30fad..480490470 100644 --- a/agent-framework/user-guide/agents/running-agents.md +++ b/agent-framework/user-guide/agents/running-agents.md @@ -75,36 +75,59 @@ Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", ::: zone-end ::: zone pivot="programming-language-python" -Python agents support passing keyword arguments to customize each run. The specific options available depend on the agent type, but `ChatAgent` supports many chat client parameters that can be passed to both `run` and `run_stream` methods. +Python agents support customizing each run via the `options` parameter. Options are passed as a TypedDict and can be set at both construction time (via `default_options`) and per-run (via `options`). Each provider has its own TypedDict class that provides full IDE autocomplete and type checking for provider-specific settings. -Common options for `ChatAgent` include: +Common options include: - `max_tokens`: Maximum number of tokens to generate - `temperature`: Controls randomness in response generation -- `model`: Override the model for this specific run -- `tools`: Add additional tools for this run only -- `response_format`: Specify the response format (for example, structured output) +- `model_id`: Override the model for this specific run +- `top_p`: Nucleus sampling parameter +- `response_format`: Specify the response format (e.g., structured output) + +> [!NOTE] +> The `tools` and `instructions` parameters remain as direct keyword arguments and are not passed via the `options` dictionary. ```python -# Run with custom options +from agent_framework.openai import OpenAIChatClient, OpenAIChatOptions + +# Set default options at construction time +agent = OpenAIChatClient().create_agent( + instructions="You are a helpful assistant", + default_options={ + "temperature": 0.7, + "max_tokens": 500 + } +) + +# Run with custom options (overrides defaults) +# OpenAIChatOptions provides IDE autocomplete for all OpenAI-specific settings +options: OpenAIChatOptions = { + "temperature": 0.3, + "max_tokens": 150, + "model_id": "gpt-4o", + "presence_penalty": 0.5, + "frequency_penalty": 0.3 +} + result = await agent.run( "What is the weather like in Amsterdam?", - temperature=0.3, - max_tokens=150, - model="gpt-4o" + options=options ) # Streaming with custom options async for update in agent.run_stream( "Tell me a detailed weather forecast", - temperature=0.7, - tools=[additional_weather_tool] + options={"temperature": 0.7, "top_p": 0.9}, + tools=[additional_weather_tool] # tools is still a keyword argument ): if update.text: print(update.text, end="", flush=True) ``` -When both agent-level defaults and run-level options are provided, the run-level options take precedence. +Each provider has its own TypedDict class (e.g., `OpenAIChatOptions`, `AnthropicChatOptions`, `OllamaChatOptions`) that exposes the full set of options supported by that provider. + +When both `default_options` and per-run `options` are provided, the per-run options take precedence and are merged with the defaults. ::: zone-end @@ -121,8 +144,8 @@ Since not all content returned is the result, it's important to look for specifi To extract the text result from a response, all `TextContent` items from all `ChatMessages` items need to be aggregated. To simplify this, a `Text` property is available on all response types that aggregates all `TextContent`. -For the non-streaming case, everything is returned in one `AgentRunResponse` object. -`AgentRunResponse` allows access to the produced messages via the `Messages` property. +For the non-streaming case, everything is returned in one `AgentResponse` object. +`AgentResponse` allows access to the produced messages via the `Messages` property. ```csharp var response = await agent.RunAsync("What is the weather like in Amsterdam?"); @@ -130,7 +153,7 @@ Console.WriteLine(response.Text); Console.WriteLine(response.Messages.Count); ``` -For the streaming case, `AgentRunResponseUpdate` objects are streamed as they are produced. +For the streaming case, `AgentResponseUpdate` objects are streamed as they are produced. Each update might contain a part of the result from the agent, and also various other content items. Similar to the non-streaming case, it is possible to use the `Text` property to get the portion of the result contained in the update, and drill into the detail via the `Contents` property. @@ -146,8 +169,8 @@ await foreach (var update in agent.RunStreamingAsync("What is the weather like i ::: zone-end ::: zone pivot="programming-language-python" -For the non-streaming case, everything is returned in one `AgentRunResponse` object. -`AgentRunResponse` allows access to the produced messages via the `messages` property. +For the non-streaming case, everything is returned in one `AgentResponse` object. +`AgentResponse` allows access to the produced messages via the `messages` property. To extract the text result from a response, all `TextContent` items from all `ChatMessage` items need to be aggregated. To simplify this, a `Text` property is available on all response types that aggregates all `TextContent`. @@ -162,7 +185,7 @@ for message in response.messages: print(f"Role: {message.role}, Text: {message.text}") ``` -For the streaming case, `AgentRunResponseUpdate` objects are streamed as they are produced. +For the streaming case, `AgentResponseUpdate` objects are streamed as they are produced. Each update might contain a part of the result from the agent, and also various other content items. Similar to the non-streaming case, it is possible to use the `text` property to get the portion of the result contained in the update, and drill into the detail via the `contents` property. diff --git a/agent-framework/user-guide/workflows/as-agents.md b/agent-framework/user-guide/workflows/as-agents.md index c88a20c11..66984ca92 100644 --- a/agent-framework/user-guide/workflows/as-agents.md +++ b/agent-framework/user-guide/workflows/as-agents.md @@ -94,7 +94,7 @@ var messages = new List new(ChatRole.User, "Write an article about renewable energy trends in 2025") }; -AgentRunResponse response = await workflowAgent.RunAsync(messages, thread); +AgentResponse response = await workflowAgent.RunAsync(messages, thread); foreach (ChatMessage message in response.Messages) { @@ -112,7 +112,7 @@ var messages = new List new(ChatRole.User, "Write an article about renewable energy trends in 2025") }; -await foreach (AgentRunResponseUpdate update in workflowAgent.RunStreamingAsync(messages, thread)) +await foreach (AgentResponseUpdate update in workflowAgent.RunStreamingAsync(messages, thread)) { // Process streaming updates from each agent in the workflow if (!string.IsNullOrEmpty(update.Text)) @@ -127,7 +127,7 @@ await foreach (AgentRunResponseUpdate update in workflowAgent.RunStreamingAsync( When a workflow contains executors that request external input (using `RequestInfoExecutor`), these requests are surfaced as function calls in the agent response: ```csharp -await foreach (AgentRunResponseUpdate update in workflowAgent.RunStreamingAsync(messages, thread)) +await foreach (AgentResponseUpdate update in workflowAgent.RunStreamingAsync(messages, thread)) { // Check for function call requests foreach (AIContent content in update.Contents) @@ -400,14 +400,14 @@ if __name__ == "__main__": When a workflow runs as an agent, workflow events are converted to agent responses. The type of response depends on which method you use: -- `run()`: Returns an `AgentRunResponse` containing the complete result after the workflow finishes -- `run_stream()`: Yields `AgentRunResponseUpdate` objects as the workflow executes, providing real-time updates +- `run()`: Returns an `AgentResponse` containing the complete result after the workflow finishes +- `run_stream()`: Yields `AgentResponseUpdate` objects as the workflow executes, providing real-time updates During execution, internal workflow events are mapped to agent responses as follows: | Workflow Event | Agent Response | |----------------|----------------| -| `AgentRunUpdateEvent` | Passed through as `AgentRunResponseUpdate` (streaming) or aggregated into `AgentRunResponse` (non-streaming) | +| `AgentResponseUpdateEvent` | Passed through as `AgentResponseUpdate` (streaming) or aggregated into `AgentResponse` (non-streaming) | | `RequestInfoEvent` | Converted to `FunctionCallContent` and `FunctionApprovalRequestContent` | | Other events | Included in `raw_representation` for observability | diff --git a/agent-framework/user-guide/workflows/core-concepts/events.md b/agent-framework/user-guide/workflows/core-concepts/events.md index c1462ca85..1ba4b901f 100644 --- a/agent-framework/user-guide/workflows/core-concepts/events.md +++ b/agent-framework/user-guide/workflows/core-concepts/events.md @@ -23,24 +23,24 @@ There are built-in events that provide observability into the workflow execution ```csharp // Workflow lifecycle events -WorkflowStartedEvent // Workflow execution begins -WorkflowOutputEvent // Workflow outputs data -WorkflowErrorEvent // Workflow encounters an error -WorkflowWarningEvent // Workflow encountered a warning +WorkflowStartedEvent // Workflow execution begins +WorkflowOutputEvent // Workflow outputs data +WorkflowErrorEvent // Workflow encounters an error +WorkflowWarningEvent // Workflow encountered a warning // Executor events -ExecutorInvokedEvent // Executor starts processing -ExecutorCompletedEvent // Executor finishes processing -ExecutorFailedEvent // Executor encounters an error -AgentRunResponseEvent // An agent run produces output -AgentRunUpdateEvent // An agent run produces a streaming update +ExecutorInvokedEvent // Executor starts processing +ExecutorCompletedEvent // Executor finishes processing +ExecutorFailedEvent // Executor encounters an error +AgentResponseEvent // An agent run produces output +AgentResponseUpdateEvent // An agent run produces a streaming update // Superstep events -SuperStepStartedEvent // Superstep begins -SuperStepCompletedEvent // Superstep completes +SuperStepStartedEvent // Superstep begins +SuperStepCompletedEvent // Superstep completes // Request events -RequestInfoEvent // A request is issued +RequestInfoEvent // A request is issued ``` ::: zone-end @@ -49,24 +49,24 @@ RequestInfoEvent // A request is issued ```python # Workflow lifecycle events -WorkflowStartedEvent # Workflow execution begins -WorkflowOutputEvent # Workflow produces an output -WorkflowErrorEvent # Workflow encounters an error -WorkflowWarningEvent # Workflow encountered a warning +WorkflowStartedEvent # Workflow execution begins +WorkflowOutputEvent # Workflow produces an output +WorkflowErrorEvent # Workflow encounters an error +WorkflowWarningEvent # Workflow encountered a warning # Executor events -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 +ExecutorInvokedEvent # Executor starts processing +ExecutorCompletedEvent # Executor finishes processing +ExecutorFailedEvent # Executor encounters an error +AgentRunEvent # An agent run produces output +AgentResponseUpdateEvent # An agent run produces a streaming update # Superstep events -SuperStepStartedEvent # Superstep begins -SuperStepCompletedEvent # Superstep completes +SuperStepStartedEvent # Superstep begins +SuperStepCompletedEvent # Superstep completes # Request events -RequestInfoEvent # A request is issued +RequestInfoEvent # A request is issued ``` ::: zone-end diff --git a/agent-framework/user-guide/workflows/orchestrations/TOC.yml b/agent-framework/user-guide/workflows/orchestrations/TOC.yml index 32ee7ae75..62614bb5b 100644 --- a/agent-framework/user-guide/workflows/orchestrations/TOC.yml +++ b/agent-framework/user-guide/workflows/orchestrations/TOC.yml @@ -6,7 +6,9 @@ href: sequential.md - name: Group Chat href: group-chat.md +- name: Magentic + href: magentic.md - name: Handoff href: handoff.md -- name: Magentic - href: magentic.md \ No newline at end of file +- name: Human-in-the-Loop + href: human-in-the-loop.md diff --git a/agent-framework/user-guide/workflows/orchestrations/concurrent.md b/agent-framework/user-guide/workflows/orchestrations/concurrent.md index 4137e901b..f3f314799 100644 --- a/agent-framework/user-guide/workflows/orchestrations/concurrent.md +++ b/agent-framework/user-guide/workflows/orchestrations/concurrent.md @@ -14,7 +14,9 @@ ms.service: agent-framework Concurrent orchestration enables multiple agents to work on the same task in parallel. Each agent processes the input independently, and their results are collected and aggregated. This approach is well-suited for scenarios where diverse perspectives or solutions are valuable, such as brainstorming, ensemble reasoning, or voting systems. -![Concurrent Orchestration](../resources/images/orchestration-concurrent.png) +

+ Concurrent Orchestration +

## What You'll Learn @@ -87,7 +89,7 @@ await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); List result = new(); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is AgentRunUpdateEvent e) + if (evt is AgentResponseUpdateEvent e) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } @@ -125,7 +127,7 @@ Assistant: English detected. Hello, world! - **Parallel Execution**: All agents process the input simultaneously and independently - **AgentWorkflowBuilder.BuildConcurrent()**: Creates a concurrent workflow from a collection of agents - **Automatic Aggregation**: Results from all agents are automatically collected into the final result -- **Event Streaming**: Real-time monitoring of agent progress through `AgentRunUpdateEvent` +- **Event Streaming**: Real-time monitoring of agent progress through `AgentResponseUpdateEvent` - **Diverse Perspectives**: Each agent brings its unique expertise to the same problem ::: zone-end diff --git a/agent-framework/user-guide/workflows/orchestrations/group-chat.md b/agent-framework/user-guide/workflows/orchestrations/group-chat.md index 9e588975c..eb4dfcdd8 100644 --- a/agent-framework/user-guide/workflows/orchestrations/group-chat.md +++ b/agent-framework/user-guide/workflows/orchestrations/group-chat.md @@ -11,15 +11,21 @@ ms.service: agent-framework # Microsoft Agent Framework Workflows Orchestrations - Group Chat -Group chat orchestration models a collaborative conversation among multiple agents, coordinated by a manager that determines speaker selection and conversation flow. This pattern is ideal for scenarios requiring iterative refinement, collaborative problem-solving, or multi-perspective analysis. +Group chat orchestration models a collaborative conversation among multiple agents, coordinated by an orchestrator that determines speaker selection and conversation flow. This pattern is ideal for scenarios requiring iterative refinement, collaborative problem-solving, or multi-perspective analysis. + +Internally, the group chat orchestration assembles agents in a star topology, with an orchestrator in the middle. The orchestrator can implement various strategies for selecting which agent speaks next, such as round-robin, prompt-based selection, or custom logic based on conversation context, making it a flexible and powerful pattern for multi-agent collaboration. + +

+ Group Chat Orchestration +

## Differences Between Group Chat and Other Patterns Group chat orchestration has distinct characteristics compared to other multi-agent patterns: -- **Centralized Coordination**: Unlike handoff patterns where agents directly transfer control, group chat uses a manager to coordinate who speaks next +- **Centralized Coordination**: Unlike handoff patterns where agents directly transfer control, group chat uses an orchestrator to coordinate who speaks next - **Iterative Refinement**: Agents can review and build upon each other's responses in multiple rounds -- **Flexible Speaker Selection**: The manager can use various strategies (round-robin, prompt-based, custom logic) to select speakers +- **Flexible Speaker Selection**: The orchestrator can use various strategies (round-robin, prompt-based, custom logic) to select speakers - **Shared Context**: All agents see the full conversation history, enabling collaborative refinement ## What You'll Learn @@ -27,7 +33,7 @@ Group chat orchestration has distinct characteristics compared to other multi-ag - How to create specialized agents for group collaboration - How to configure speaker selection strategies - How to build workflows with iterative agent refinement -- How to customize conversation flow with custom managers +- How to customize conversation flow with custom orchestrators ::: zone pivot="programming-language-csharp" @@ -71,7 +77,7 @@ ChatClientAgent reviewer = new(client, "A marketing review agent"); ``` -## Configure Group Chat with Round-Robin Manager +## Configure Group Chat with Round-Robin Orchestrator Build the group chat workflow using `AgentWorkflowBuilder`: @@ -79,9 +85,9 @@ Build the group chat workflow using `AgentWorkflowBuilder`: // Build group chat with round-robin speaker selection // The manager factory receives the list of agents and returns a configured manager var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => - new RoundRobinGroupChatManager(agents) - { + .CreateGroupChatBuilderWith(agents => + new RoundRobinGroupChatManager(agents) + { MaximumIterationCount = 5 // Maximum number of turns }) .AddParticipants(writer, reviewer) @@ -94,8 +100,8 @@ Execute the workflow and observe the iterative conversation: ```csharp // Start the group chat -var messages = new List { - new(ChatRole.User, "Create a slogan for an eco-friendly electric vehicle.") +var messages = new List { + new(ChatRole.User, "Create a slogan for an eco-friendly electric vehicle.") }; StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages); @@ -103,10 +109,10 @@ await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is AgentRunUpdateEvent update) + if (evt is AgentResponseUpdateEvent update) { // Process streaming agent responses - AgentRunResponse response = update.AsResponse(); + AgentResponse response = update.AsResponse(); foreach (ChatMessage message in response.Messages) { Console.WriteLine($"[{update.ExecutorId}]: {message.Text}"); @@ -131,15 +137,15 @@ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false) ```plaintext [CopyWriter]: "Green Dreams, Zero Emissions" - Drive the future with style and sustainability. -[Reviewer]: The slogan is good, but "Green Dreams" might be a bit abstract. Consider something +[Reviewer]: The slogan is good, but "Green Dreams" might be a bit abstract. Consider something more direct like "Pure Power, Zero Impact" to emphasize both performance and environmental benefit. [CopyWriter]: "Pure Power, Zero Impact" - Experience electric excellence without compromise. -[Reviewer]: Excellent! This slogan is clear, impactful, and directly communicates the key benefits. +[Reviewer]: Excellent! This slogan is clear, impactful, and directly communicates the key benefits. The tagline reinforces the message perfectly. Approved for use. -[CopyWriter]: Thank you! The final slogan is: "Pure Power, Zero Impact" - Experience electric +[CopyWriter]: Thank you! The final slogan is: "Pure Power, Zero Impact" - Experience electric excellence without compromise. ``` @@ -186,66 +192,52 @@ writer = ChatAgent( Build a group chat with custom speaker selection logic: ```python -from agent_framework import GroupChatBuilder, GroupChatStateSnapshot - -def select_next_speaker(state: GroupChatStateSnapshot) -> str | None: - """Alternate between researcher and writer for collaborative refinement. - - Args: - state: Contains task, participants, conversation, history, and round_index - - Returns: - Name of next speaker, or None to finish - """ - round_idx = state["round_index"] - history = state["history"] - - # Finish after 4 turns (researcher → writer → researcher → writer) - if round_idx >= 4: - return None - - # Alternate speakers - last_speaker = history[-1].speaker if history else None - if last_speaker == "Researcher": - return "Writer" - return "Researcher" +from agent_framework import GroupChatBuilder, GroupChatState + +def round_robin_selector(state: GroupChatState) -> str: + """A round-robin selector function that picks the next speaker based on the current round index.""" + + participant_names = list(state.participants.keys()) + return participant_names[state.current_round % len(participant_names)] + # Build the group chat workflow workflow = ( GroupChatBuilder() - .set_select_speakers_func(select_next_speaker, display_name="Orchestrator") + .with_select_speaker_func(round_robin_selector) .participants([researcher, writer]) + # Terminate after 4 turns (researcher → writer → researcher → writer) + .with_termination_condition(lambda conversation: len(conversation) >= 4) .build() ) ``` -## Configure Group Chat with Agent-Based Manager +## Configure Group Chat with Agent-Based Orchestrator -Alternatively, use an agent-based manager for intelligent speaker selection. The manager is a full `ChatAgent` with access to tools, context, and observability: +Alternatively, use an agent-based orchestrator for intelligent speaker selection. The orchestrator is a full `ChatAgent` with access to tools, context, and observability: ```python -# Create coordinator agent for speaker selection -coordinator = ChatAgent( - name="Coordinator", +# Create orchestrator agent for speaker selection +orchestrator_agent = ChatAgent( + name="Orchestrator", 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 +# Build group chat with agent-based orchestrator workflow = ( GroupChatBuilder() - .set_manager(coordinator, display_name="Orchestrator") + .with_agent_orchestrator(orchestrator_agent) + # Set a hard termination condition: stop after 4 assistant messages + # The agent orchestrator will intelligently decide when to end before this limit but just in case .with_termination_condition(lambda messages: sum(1 for msg in messages if msg.role == Role.ASSISTANT) >= 4) .participants([researcher, writer]) .build() @@ -258,7 +250,7 @@ Execute the workflow and process events: ```python from typing import cast -from agent_framework import AgentRunUpdateEvent, Role, WorkflowOutputEvent +from agent_framework import AgentResponseUpdateEvent, Role, WorkflowOutputEvent task = "What are the key benefits of async/await in Python?" @@ -270,7 +262,7 @@ last_executor_id: str | None = None # Run the workflow async for event in workflow.run_stream(task): - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdateEvent): # Print streaming agent updates eid = event.executor_id if eid != last_executor_id: @@ -302,24 +294,24 @@ Task: What are the key benefits of async/await in Python? ================================================================================ -[Researcher]: Async/await in Python provides non-blocking I/O operations, enabling -concurrent execution without threading overhead. Key benefits include improved -performance for I/O-bound tasks, better resource utilization, and simplified +[Researcher]: Async/await in Python provides non-blocking I/O operations, enabling +concurrent execution without threading overhead. Key benefits include improved +performance for I/O-bound tasks, better resource utilization, and simplified concurrent code structure using native coroutines. [Writer]: The key benefits of async/await in Python are: -1. **Non-blocking Operations**: Allows I/O operations to run concurrently without - blocking the main thread, significantly improving performance for network +1. **Non-blocking Operations**: Allows I/O operations to run concurrently without + blocking the main thread, significantly improving performance for network requests, file I/O, and database queries. -2. **Resource Efficiency**: Avoids the overhead of thread creation and context +2. **Resource Efficiency**: Avoids the overhead of thread creation and context switching, making it more memory-efficient than traditional threading. -3. **Simplified Concurrency**: Provides a clean, synchronous-looking syntax for +3. **Simplified Concurrency**: Provides a clean, synchronous-looking syntax for asynchronous code, making concurrent programs easier to write and maintain. -4. **Scalability**: Enables handling thousands of concurrent connections with +4. **Scalability**: Enables handling thousands of concurrent connections with minimal resource consumption, ideal for high-performance web servers and APIs. -------------------------------------------------------------------------------- @@ -345,13 +337,13 @@ Workflow completed. ::: zone pivot="programming-language-python" -- **Flexible Manager Strategies**: Choose between simple selectors, agent-based managers, or custom logic +- **Flexible Orchestrator Strategies**: Choose between simple selectors, agent-based orchestrators, or custom logic - **GroupChatBuilder**: Creates workflows with configurable 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 +- **with_select_speaker_func()**: Define custom Python functions for speaker selection +- **with_agent_orchestrator()**: Use an agent-based orchestrator for intelligent speaker coordination +- **GroupChatState**: Provides conversation state for selection decisions - **Iterative Collaboration**: Agents build upon each other's contributions -- **Event Streaming**: Process `AgentRunUpdateEvent` and `WorkflowOutputEvent` in real-time +- **Event Streaming**: Process `AgentResponseUpdateEvent` and `WorkflowOutputEvent` in real-time - **list[ChatMessage] Output**: All orchestrations return a list of chat messages ::: zone-end @@ -366,32 +358,32 @@ You can implement custom manager logic by creating a custom group chat manager: public class ApprovalBasedManager : RoundRobinGroupChatManager { private readonly string _approverName; - - public ApprovalBasedManager(IReadOnlyList agents, string approverName) + + public ApprovalBasedManager(IReadOnlyList agents, string approverName) : base(agents) { _approverName = approverName; } - + // Override to add custom termination logic protected override ValueTask ShouldTerminateAsync( - IReadOnlyList history, + IReadOnlyList history, CancellationToken cancellationToken = default) { var last = history.LastOrDefault(); bool shouldTerminate = last?.AuthorName == _approverName && last.Text?.Contains("approve", StringComparison.OrdinalIgnoreCase) == true; - + return ValueTask.FromResult(shouldTerminate); } } // Use custom manager in workflow var workflow = AgentWorkflowBuilder - .CreateGroupChatBuilderWith(agents => - new ApprovalBasedManager(agents, "Reviewer") - { - MaximumIterationCount = 10 + .CreateGroupChatBuilderWith(agents => + new ApprovalBasedManager(agents, "Reviewer") + { + MaximumIterationCount = 10 }) .AddParticipants(writer, reviewer) .Build(); @@ -404,38 +396,29 @@ var workflow = AgentWorkflowBuilder You can implement sophisticated selection logic based on conversation state: ```python -def smart_selector(state: GroupChatStateSnapshot) -> str | None: +def smart_selector(state: GroupChatState) -> str: """Select speakers based on conversation content and context.""" - round_idx = state["round_index"] - conversation = state["conversation"] - history = state["history"] - - # Maximum 10 rounds - if round_idx >= 10: - return None - - # First round: always start with researcher - if round_idx == 0: + conversation = state.conversation + + last_message = conversation[-1] if conversation else None + + # If no messages yet, start with Researcher + if not last_message: return "Researcher" - + # Check last message content - last_message = conversation[-1] if conversation else None - last_text = getattr(last_message, "text", "").lower() - - # If researcher asked a question, let writer respond - if "?" in last_text and history[-1].speaker == "Researcher": + last_text = last_message.text.lower() + + # If researcher finished gathering info, switch to writer + if "I have finished" in last_text and last_message.author_name == "Researcher": return "Writer" - - # If writer provided info, let researcher validate or extend - if history[-1].speaker == "Writer": - return "Researcher" - - # Default alternation - return "Writer" if history[-1].speaker == "Researcher" else "Researcher" + + # Else continue with researcher until it indicates completion + return "Researcher" workflow = ( GroupChatBuilder() - .set_select_speakers_func(smart_selector, display_name="SmartOrchestrator") + .with_select_speaker_func(smart_selector, orchestrator_name="SmartOrchestrator") .participants([researcher, writer]) .build() ) @@ -443,6 +426,21 @@ workflow = ( ::: zone-end +## Context Synchronization + +As mentioned at the beginning of this guide, all agents in a group chat see the full conversation history. + +Agents in Agent Framework relies on agent threads ([`AgentThread`](../../agents/multi-turn-conversation.md)) to manage context. In a group chat orchestration, agents **do not** share the same thread instance, but the orchestrator ensures that each agent's thread is synchronized with the complete conversation history before each turn. To achieve this, after each agent's turn, the orchestrator broadcasts the response to all other agents, making sure all participants have the latest context for their next turn. + +

+ Group Chat Context Synchronization +

+ +> [!TIP] +> Agents do not share the same thread instance because different [agent types](../../agents/agent-types/index.md) may have different implementations of the `AgentThread` abstraction. Sharing the same thread instance could lead to inconsistencies in how each agent processes and maintains context. + +After broadcasting the response, the orchestrator then decide the next speaker and sends a request to the selected agent, which now has the full conversation history to generate its response. + ## When to Use Group Chat Group chat orchestration is ideal for: @@ -454,6 +452,7 @@ Group chat orchestration is ideal for: - **Quality Assurance**: Automated review and approval processes **Consider alternatives when:** + - You need strict sequential processing (use Sequential orchestration) - Agents should work completely independently (use Concurrent orchestration) - Direct agent-to-agent handoffs are needed (use Handoff orchestration) @@ -462,4 +461,4 @@ Group chat orchestration is ideal for: ## Next steps > [!div class="nextstepaction"] -> [Handoff Orchestration](./handoff.md) +> [Magentic Orchestration](./magentic.md) diff --git a/agent-framework/user-guide/workflows/orchestrations/handoff.md b/agent-framework/user-guide/workflows/orchestrations/handoff.md index b16b501b3..fb25b8e78 100644 --- a/agent-framework/user-guide/workflows/orchestrations/handoff.md +++ b/agent-framework/user-guide/workflows/orchestrations/handoff.md @@ -13,7 +13,14 @@ zone_pivot_groups: programming-languages Handoff orchestration allows agents to transfer control to one another based on the context or user request. Each agent can "handoff" the conversation to another agent with the appropriate expertise, ensuring that the right agent handles each part of the task. This is particularly useful in customer support, expert systems, or any scenario requiring dynamic delegation. -![Handoff Orchestration](../resources/images/orchestration-handoff.png) +Internally, the handoff orchestration is implemented using a mesh topology where agents are connected directly without an orchestrator. Each agent can decide when to hand off the conversation based on predefined rules or the content of the messages. + +

+ Handoff Orchestration +

+ +> [!NOTE] +> Handoff orchestration only supports `ChatAgent` and the agents must support local tools execution. ## Differences Between Handoff and Agent-as-Tools @@ -113,7 +120,7 @@ while (true) List newMessages = new(); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is AgentRunUpdateEvent e) + if (evt is AgentResponseUpdateEvent e) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } @@ -149,6 +156,25 @@ math_tutor: I'd be happy to help with calculus integration! Integration is the r ::: zone pivot="programming-language-python" +## Define a few tools for demonstration + +```python +@ai_function +def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str: + """Simulated function to process a refund for a given order number.""" + return f"Refund processed successfully for order {order_number}." + +@ai_function +def check_order_status(order_number: Annotated[str, "Order number to check status for"]) -> str: + """Simulated function to check the status of a given order number.""" + return f"Order {order_number} is currently being processed and will ship in 2 business days." + +@ai_function +def process_return(order_number: Annotated[str, "Order number to process return for"]) -> str: + """Simulated function to process a return for a given order number.""" + return f"Return initiated successfully for order {order_number}. You will receive return instructions via email." +``` + ## Set Up the Chat Client ```python @@ -167,36 +193,38 @@ Create domain-specific agents with a coordinator for routing: # Create triage/coordinator agent triage_agent = chat_client.create_agent( instructions=( - "You are frontline support triage. Read the latest user message and decide whether " - "to hand off to refund_agent, order_agent, or support_agent. Provide a brief natural-language " - "response for the user. When delegation is required, call the matching handoff tool " - "(`handoff_to_refund_agent`, `handoff_to_order_agent`, or `handoff_to_support_agent`)." + "You are frontline support triage. Route customer issues to the appropriate specialist agents " + "based on the problem described." ), + description="Triage agent that handles general inquiries.", name="triage_agent", ) -# Create specialist agents +# Refund specialist: Handles refund requests refund_agent = chat_client.create_agent( - instructions=( - "You handle refund workflows. Ask for any order identifiers you require and outline the refund steps." - ), + instructions="You process refund requests.", + description="Agent that handles refund requests.", name="refund_agent", + # In a real application, an agent can have multiple tools; here we keep it simple + tools=[process_refund], ) +# Order/shipping specialist: Resolves delivery issues order_agent = chat_client.create_agent( - instructions=( - "You resolve shipping and fulfillment issues. Clarify the delivery problem and describe the actions " - "you will take to remedy it." - ), + instructions="You handle order and shipping inquiries.", + description="Agent that handles order tracking and shipping issues.", name="order_agent", + # In a real application, an agent can have multiple tools; here we keep it simple + tools=[check_order_status], ) -support_agent = chat_client.create_agent( - instructions=( - "You are a general support agent. Offer empathetic troubleshooting and gather missing details if the " - "issue does not match other specialists." - ), - name="support_agent", +# Return specialist: Handles return requests +return_agent = chat_client.create_agent( + instructions="You manage product return requests.", + description="Agent that handles return processing.", + name="return_agent", + # In a real application, an agent can have multiple tools; here we keep it simple + tools=[process_return], ) ``` @@ -211,42 +239,57 @@ from agent_framework import HandoffBuilder workflow = ( HandoffBuilder( name="customer_support_handoff", - participants=[triage_agent, refund_agent, order_agent, support_agent], + participants=[triage_agent, refund_agent, order_agent, return_agent], ) - .set_coordinator("triage_agent") + .with_start_agent(triage_agent) # Triage receives initial user input .with_termination_condition( - # Terminate after a certain number of user messages - lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 10 + # Custom termination: Check if one of the agents has provided a closing message. + # This looks for the last message containing "welcome", which indicates the + # conversation has concluded naturally. + lambda conversation: len(conversation) > 0 and "welcome" in conversation[-1].text.lower() ) .build() ) ``` -For more advanced routing, you can configure specialist-to-specialist handoffs: +By default, all agents can handoff to each other. For more advanced routing, you can configure handoffs: ```python -# Enable return-to-previous and add specialist-to-specialist handoffs workflow = ( HandoffBuilder( - name="advanced_handoff", - participants=[coordinator, technical, account, billing], + name="customer_support_handoff", + participants=[triage_agent, refund_agent, order_agent, return_agent], + ) + .with_start_agent(triage_agent) # Triage receives initial user input + .with_termination_condition( + # Custom termination: Check if one of the agents has provided a closing message. + # This looks for the last message containing "welcome", which indicates the + # conversation has concluded naturally. + lambda conversation: len(conversation) > 0 and "welcome" in conversation[-1].text.lower() ) - .set_coordinator(coordinator) - .add_handoff(coordinator, [technical, account, billing]) # Coordinator routes to all specialists - .add_handoff(technical, [billing, account]) # Technical can route to billing or account - .add_handoff(account, [technical, billing]) # Account can route to technical or billing - .add_handoff(billing, [technical, account]) # Billing can route to technical or account - .enable_return_to_previous(True) # User inputs route directly to current specialist + # Triage cannot route directly to refund agent + .add_handoff(triage_agent, [order_agent, return_agent]) + # Only the return agent can handoff to refund agent - users wanting refunds after returns + .add_handoff(return_agent, [refund_agent]) + # All specialists can handoff back to triage for further routing + .add_handoff(order_agent, [triage_agent]) + .add_handoff(return_agent, [triage_agent]) + .add_handoff(refund_agent, [triage_agent]) .build() ) ``` -## Run Interactive Handoff Workflow +> [!NOTE] +> Even with custom handoff rules, all agents are still connected in a mesh topology. This is because agents need to share context with each other to maintain conversation history (see [Context Synchronization](#context-synchronization) for more details). The handoff rules only govern which agents can take over the conversation next. + +## Run Handoff Agent Interaction -Handle multi-turn conversations with user input requests: +Unlike other orchestrations, handoff is interactive because an agent may not decide to handoff after every turn. If an agent doesn't handoff, human input is required to continue the conversation. See [Autonomous Mode](#autonomous-mode) for bypassing this requirement. In other orchestrations, after an agent responds, the control either goes to the orchestrator or the next agent. + +When an agent in a handoff workflow decides not to handoff (a handoff is triggered by a special tool call), the workflow emits a `RequestInfoEvent` with a `HandoffAgentUserRequest` payload containing the agent's most recent messages. The user must respond to this request to continue the workflow. ```python -from agent_framework import RequestInfoEvent, HandoffUserInputRequest, WorkflowOutputEvent +from agent_framework import RequestInfoEvent, HandoffAgentUserRequest, WorkflowOutputEvent # Start workflow with initial user message events = [event async for event in workflow.run_stream("I need help with my order")] @@ -254,31 +297,97 @@ events = [event async for event in workflow.run_stream("I need help with my orde # Process events and collect pending input requests pending_requests = [] for event in events: - if isinstance(event, RequestInfoEvent): + if isinstance(event, RequestInfoEvent) and isinstance(event.data, HandoffAgentUserRequest): pending_requests.append(event) request_data = event.data - print(f"Agent {request_data.awaiting_agent_id} is awaiting your input") - for msg in request_data.conversation[-3:]: + print(f"Agent {event.source_executor_id} is awaiting your input") + # The request contains the most recent messages generated by the + # agent requesting input + for msg in request_data.agent_response.messages[-3:]: print(f"{msg.author_name}: {msg.text}") # Interactive loop: respond to requests while pending_requests: user_input = input("You: ") - + # Send responses to all pending requests - responses = {req.request_id: user_input for req in pending_requests} + responses = {req.request_id: HandoffAgentUserRequest.create_response(user_input) for req in pending_requests} + # You can also send a `HandoffAgentUserRequest.terminate()` to end the workflow early events = [event async for event in workflow.send_responses_streaming(responses)] - + # Process new events pending_requests = [] for event in events: - if isinstance(event, RequestInfoEvent): - pending_requests.append(event) - elif isinstance(event, WorkflowOutputEvent): - print("Workflow completed!") - conversation = event.data - for msg in conversation: - print(f"{msg.author_name}: {msg.text}") + # Check for new input requests +``` + +## Autonomous Mode + +The Handoff orchestration is designed for interactive scenarios where human input is required when an agent decides not to handoff. However, as an **experimental feature**, you can enable "autonomous mode" to allow the workflow to continue without human intervention. In this mode, when an agent decides not to handoff, the workflow automatically sends a default response (e.g.`User did not respond. Continue assisting autonomously.`) to the agent, allowing it to continue the conversation. + +> [!TIP] +> Why is Handoff orchestration inherently interactive? Unlike other orchestrations where there is only one path to follow after an agent responds (e.g. back to orchestrator or next agent), in a Handoff orchestration, the agent has the option to either handoff to another agent or continue assisting the user itself. And because handoffs are achieved through tool calls, if an agent does not call a handoff tool but generates a response instead, the workflow won't know what to do next but to delegate back to the user for further input. It is also not possible to force an agent to always handoff by requiring it to call the handoff tool because the agent won't be able to generate meaningful responses otherwise. + +**Autonomous Mode** is enabled by calling `with_autonomous_mode()` on the `HandoffBuilder`. This configures the workflow to automatically respond to input requests with a default message, allowing the agent to continue without waiting for human input. + +```python +workflow = ( + HandoffBuilder( + name="autonomous_customer_support", + participants=[triage_agent, refund_agent, order_agent, return_agent], + ) + .with_start_agent(triage_agent) + .with_autonomous_mode() + .build() +) +``` + +You can also enable autonomous mode on only a subset of agents by passing a list of agent instances to `with_autonomous_mode()`. + +```python +workflow = ( + HandoffBuilder( + name="partially_autonomous_support", + participants=[triage_agent, refund_agent, order_agent, return_agent], + ) + .with_start_agent(triage_agent) + .with_autonomous_mode(agents=[triage_agent]) # Only triage_agent runs autonomously + .build() +) +``` + +You can customize the default response message. + +```python +workflow = ( + HandoffBuilder( + name="custom_autonomous_support", + participants=[triage_agent, refund_agent, order_agent, return_agent], + ) + .with_start_agent(triage_agent) + .with_autonomous_mode( + agents=[triage_agent], + prompts={triage_agent.name: "Continue with your best judgment as the user is unavailable."}, + ) + .build() +) +``` + +You can customize the number of turns an agent can run autonomously before requiring human input. This can prevent the workflow from running indefinitely without user involvement. + +```python +workflow = ( + HandoffBuilder( + name="limited_autonomous_support", + participants=[triage_agent, refund_agent, order_agent, return_agent], + ) + .with_start_agent(triage_agent) + .with_autonomous_mode( + agents=[triage_agent], + turn_limits={triage_agent.name: 3}, # Max 3 autonomous turns + ) + .build() +) ``` ## Advanced: Tool Approval in Handoff Workflows @@ -292,13 +401,9 @@ 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}" +def process_refund(order_number: Annotated[str, "Order number to process refund for"]) -> str: + """Simulated function to process a refund for a given order number.""" + return f"Refund processed successfully for order {order_number}." ``` ### Create Agents with Approval-Required Tools @@ -310,28 +415,27 @@ from azure.identity import AzureCliCredential client = AzureOpenAIChatClient(credential=AzureCliCredential()) -triage_agent = client.create_agent( - name="triage_agent", +triage_agent = chat_client.create_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." + "You are frontline support triage. Route customer issues to the appropriate specialist agents " + "based on the problem described." ), + description="Triage agent that handles general inquiries.", + name="triage_agent", ) -refund_agent = client.create_agent( +refund_agent = chat_client.create_agent( + instructions="You process refund requests.", + description="Agent that handles refund requests.", 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], + tools=[process_refund], ) -order_agent = client.create_agent( +order_agent = chat_client.create_agent( + instructions="You handle order and shipping inquiries.", + description="Agent that handles order tracking and shipping issues.", name="order_agent", - instructions="You are an order tracking specialist. Help customers track their orders.", + tools=[check_order_status], ) ``` @@ -341,7 +445,7 @@ order_agent = client.create_agent( from agent_framework import ( FunctionApprovalRequestContent, HandoffBuilder, - HandoffUserInputRequest, + HandoffAgentUserRequest, RequestInfoEvent, WorkflowOutputEvent, ) @@ -351,7 +455,7 @@ workflow = ( name="support_with_approvals", participants=[triage_agent, refund_agent, order_agent], ) - .set_coordinator("triage_agent") + .with_start_agent(triage_agent) .build() ) @@ -365,28 +469,28 @@ async for event in workflow.run_stream("My order 12345 arrived damaged. I need a # 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): + if isinstance(request.data, HandoffAgentUserRequest): # Agent needs user input - print(f"Agent {request.data.awaiting_agent_id} asks:") - for msg in request.data.conversation[-2:]: + print(f"Agent {request.source_executor_id} asks:") + for msg in request.data.agent_response.messages[-2:]: print(f" {msg.author_name}: {msg.text}") - + user_input = input("You: ") - responses[request.request_id] = user_input - + responses[request.request_id] = HandoffAgentUserRequest.create_response(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): @@ -410,7 +514,7 @@ workflow = ( name="durable_support", participants=[triage_agent, refund_agent, order_agent], ) - .set_coordinator("triage_agent") + .with_start_agent(triage_agent) .with_checkpointing(storage) .build() ) @@ -438,14 +542,13 @@ 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." + elif isinstance(req.data, HandoffAgentUserRequest): + responses[req.request_id] = HandoffAgentUserRequest.create_response("Yes, please process the refund.") async for event in workflow.send_responses_streaming(responses): if isinstance(event, WorkflowOutputEvent): print("Refund workflow completed!") ``` -``` ## Sample Interaction @@ -473,10 +576,26 @@ refund_agent: I'll process your refund for order 1234. Here's what will happen n 4. Refund processing within 5-10 business days Could you provide photos of the damage to expedite the process? -``` +```` ::: zone-end +## Context Synchronization + +Agents in Agent Framework relies on agent threads ([`AgentThread`](../../agents/multi-turn-conversation.md)) to manage context. In a Handoff orchestration, agents **do not** share the same thread instance, participants are responsible for ensuring context consistency. To achieve this, participants are designed to broadcast their responses or user inputs received to all others in the workflow whenever they generate a response, making sure all participants have the latest context for their next turn. + +

+ Handoff Context Synchronization +

+ +> [!NOTE] +> Tool related contents, including handoff tool calls, are not broadcasted to other agents. Only user and agent messages are synchronized across all participants. + +> [!TIP] +> Agents do not share the same thread instance because different [agent types](../../agents/agent-types/index.md) may have different implementations of the `AgentThread` abstraction. Sharing the same thread instance could lead to inconsistencies in how each agent processes and maintains context. + +After broadcasting the response, the participant then checks whether it needs to handoff the conversation to another agent. If so, it sends a request to the selected agent to take over the conversation. Otherwise, it requests user input or continues autonomously based on the workflow configuration. + ## Key Concepts ::: zone pivot="programming-language-csharp" @@ -494,9 +613,8 @@ Could you provide photos of the damage to expedite the process? - **Dynamic Routing**: Agents can decide which agent should handle the next interaction based on context - **HandoffBuilder**: Creates workflows with automatic handoff tool registration -- **set_coordinator()**: Defines which agent receives user input first +- **with_start_agent()**: Defines which agent receives user input first - **add_handoff()**: Configures specific handoff relationships between agents -- **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 @@ -509,4 +627,4 @@ Could you provide photos of the damage to expedite the process? ## Next steps > [!div class="nextstepaction"] -> [Magentic Orchestration](./magentic.md) +> [Human-in-the-Loop in Orchestrations](./human-in-the-loop.md) - Learn how to implement human-in-the-loop interactions in orchestrations for enhanced control and oversight. diff --git a/agent-framework/user-guide/workflows/orchestrations/human-in-the-loop.md b/agent-framework/user-guide/workflows/orchestrations/human-in-the-loop.md new file mode 100644 index 000000000..f1e63e313 --- /dev/null +++ b/agent-framework/user-guide/workflows/orchestrations/human-in-the-loop.md @@ -0,0 +1,99 @@ +--- +title: Microsoft Agent Framework Workflows Orchestrations - HITL +description: In-depth look at Human-in-the-Loop in Orchestrations in Microsoft Agent Framework Workflows. +zone_pivot_groups: programming-languages +author: TaoChenOSU +ms.topic: tutorial +ms.author: taochen +ms.date: 01/11/2026 +ms.service: agent-framework +--- + +# Microsoft Agent Framework Workflows Orchestrations - Human-in-the-Loop + +Although fully autonomous agents sound powerful, practical applications often require human intervention for critical decisions, approvals, or feedback before proceeding. + +All Microsoft Agent Framework orchestrations support Human-in-the-Loop (HITL) capabilities, allowing the workflow to pause and request input from a human user at designated points. This ensures the following: + +1. Sensitive actions are reviewed and approved by humans, enhancing safety and reliability. +2. A feedback loop exists where humans can guide agent behavior, improving outcomes. + +> [!IMPORTANT] +> The Handoff orchestration is specifically designed for complex multi-agent scenarios requiring extensive human interaction. Thus, its HITL features are designed differently from other orchestrations. See the [Handoff Orchestration](./handoff.md) documentation for details. + +> [!IMPORTANT] +> For group-chat-based orchestrations (Group Chat and Magentic), the orchestrator can also request human feedback and approvals as needed, depending on the implementation of the orchestrator. + +## How Human-in-the-Loop Works + +> [!TIP] +> The HITL functionality is built on top of the existing request/response mechanism in Microsoft Agent Framework workflows. If you're unfamiliar with this mechanism, please refer to the [Request and Response](../requests-and-responses.md) documentation first. + +::: zone pivot="programming-language-csharp" + +Coming soon... + +::: zone-end + +::: zone pivot="programming-language-python" + +When HITL is enabled in an orchestration, via the `with_request_info()` method on the corresponding builder (e.g., `SequentialBuilder`), a subworkflow is created to facilitate human interaction for the agent participants. + +Take the Sequential orchestration as an example. Without HITL, the agent participants are directly plugged into a sequential pipeline: + +

+ Sequential Orchestration +

+ +With HITL enabled, the agent participants are plugged into a subworkflow that handles human requests and responses in a loop: + +

+ Sequential Orchestration with HITL +

+ +When an agent produces an output, the output doesn't go directly to the next agent or the orchestrator. Instead, it is sent to the `AgentRequestInfoExecutor` in the subworkflow, which sends the output as a request and waits for a response of type `AgentRequestInfoResponse`. + +To proceed, the system (typically a human user) must provide a response to the request. This response can be one of the following: + +1. **Feedback**: The human user can provide feedback on the agent's output, which is then sent back to the agent for further refinement. Can be created via `AgentRequestInfoResponse.from_messages()` or `AgentRequestInfoResponse.from_strings()`. +2. **Approval**: If the agent's output meets the human user's expectations, the user can approve it to allow the subworkflow to output the agent's response and the parent workflow to continue. Can be created via `AgentRequestInfoResponse.approve()`. + +> [!TIP] +> The same process applies to [Concurrent](concurrent.md), [Group Chat](group-chat.md), and [Magnetic](magentic.md) orchestrations. + +::: zone-end + +## Only enable HITL for a subset of agents + +::: zone pivot="programming-language-csharp" + +Coming soon... + +::: zone-end + +::: zone pivot="programming-language-python" + +You can choose to enable HITL for only a subset of agents in the orchestration by specifying the agent IDs when calling `with_request_info()`. For example, in a sequential orchestration with three agents, you can enable HITL only for the second agent: + +```python +builder = ( + SequentialBuilder() + .participants([agent1, agent2, agent3]) + .with_request_info(agents=[agent2]) +) +``` + +::: zone-end + +## Function Approval with HITL + +When your agents use functions that require human approval (e.g., functions decorated with `@ai_function(approval_mode="always_require")`), the HITL mechanism seamlessly integrates function approval requests into the workflow. + +> [!TIP] +> See the [Function Approval](../../../tutorials/agents/function-tools-approvals.md) documentation for more details on function approval. + +When an agent attempts to call such a function, a `FunctionApprovalRequestContent` request is generated and sent to the human user for approval. The workflow pauses if no other path is available and waits for the user's decision. The user can then approve or reject the function call, and the response is sent back to the agent to proceed accordingly. + +## Next steps + +Head over to our samples in the [Microsoft Agent Framework GitHub repository](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/workflows/human-in-the-loop) to see HITL in action. diff --git a/agent-framework/user-guide/workflows/orchestrations/magentic.md b/agent-framework/user-guide/workflows/orchestrations/magentic.md index 223e2196f..f15430070 100644 --- a/agent-framework/user-guide/workflows/orchestrations/magentic.md +++ b/agent-framework/user-guide/workflows/orchestrations/magentic.md @@ -15,13 +15,21 @@ Magentic orchestration is designed based on the [Magentic-One](https://microsoft The Magentic manager maintains a shared context, tracks progress, and adapts the workflow in real time. This enables the system to break down complex problems, delegate subtasks, and iteratively refine solutions through agent collaboration. The orchestration is especially well-suited for scenarios where the solution path is not known in advance and might require multiple rounds of reasoning, research, and computation. -![Magentic Orchestration](../resources/images/orchestration-magentic.png) +

+ Magentic Orchestration +

+ +> [!TIP] +> The Magentic orchestration has the same archetecture as the [Group Chat orchestration](./group-chat.md) pattern, with a very powerful manager that uses planning to coordinate agent collaboration. If your scenario requires simpler coordination without complex planning, consider using the Group Chat pattern instead. + +> [!NOTE] +> In the [Magentic-One](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/magentic-one.html) paper, 4 highly specialized agents are designed to solve a very specific set of tasks. In the Magentic orchestration in Agent Framework, you can define your own specialized agents to suit your specific application needs. However, it is untested how well the Magentic orchestration will perform outside of the original Magentic-One design. ## What You'll Learn - How to set up a Magentic manager to coordinate multiple specialized agents - How to handle streaming events with `AgentRunUpdateEvent` -- How to implement human-in-the-loop plan review, tool approval, and stall intervention +- How to implement human-in-the-loop plan review - How to track agent collaboration and progress through complex tasks ## Define Your Specialized Agents @@ -76,28 +84,34 @@ from agent_framework import MagenticBuilder workflow = ( MagenticBuilder() - .participants(researcher=researcher_agent, coder=coder_agent) + .participants([researcher_agent, coder_agent]) .with_standard_manager( 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 + max_round_count=10, + max_stall_count=3, + max_reset_count=2, ) .build() ) ``` +> [!TIP] +> A standard manager is implemented based on the Magentic-One design, with fixed prompts taken from the original paper. You can customize the manager's behavior by passing in your own prompts to `with_standard_manager()`. To further customize the manager, you can also implement your own manager by sub classing the `MagenticManagerBase` class. + ## Run the Workflow with Event Streaming Execute a complex task and handle events for streaming output and orchestration updates: ```python +import json +import asyncio from typing import cast + from agent_framework import ( - MAGENTIC_EVENT_TYPE_AGENT_DELTA, - MAGENTIC_EVENT_TYPE_ORCHESTRATOR, AgentRunUpdateEvent, ChatMessage, + MagenticOrchestratorEvent, + MagenticProgressLedger, WorkflowOutputEvent, ) @@ -110,243 +124,132 @@ task = ( "per task type (image classification, text classification, and text generation)." ) -# State for streaming callback -last_stream_agent_id: str | None = None -stream_line_open: bool = False -output: str | None = None - +# Keep track of the last executor to format output nicely in streaming mode +last_message_id: str | None = None +output_event: WorkflowOutputEvent | None = None async for event in workflow.run_stream(task): 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) + message_id = event.data.message_id + if message_id != last_message_id: + if last_message_id is not None: + print("\n") + print(f"- {event.executor_id}:", end=" ", flush=True) + last_message_id = message_id + print(event.data, end="", flush=True) + + elif isinstance(event, MagenticOrchestratorEvent): + print(f"\n[Magentic Orchestrator Event] Type: {event.event_type.name}") + if isinstance(event.data, MagenticProgressLedger): + print(f"Please review progress ledger:\n{json.dumps(event.data.to_dict(), indent=2)}") + else: + print(f"Unknown data type in MagenticOrchestratorEvent: {type(event.data)}") + + # Block to allow user to read the plan/progress before continuing + # Note: this is for demonstration only and is not the recommended way to handle human interaction. + # Please refer to `with_plan_review` for proper human interaction during planning phases. + await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...") elif isinstance(event, WorkflowOutputEvent): - output_messages = cast(list[ChatMessage], event.data) - if output_messages: - output = output_messages[-1].text + output_event = event -if stream_line_open: - print() - -if output is not None: - print(f"Workflow completed with result:\n\n{output}") +# The output of the Magentic workflow is a list of ChatMessages with only one final message +# generated by the orchestrator. +output_messages = cast(list[ChatMessage], output_event.data) +output = output_messages[-1].text +print(output) ``` ## Advanced: Human-in-the-Loop Plan Review -Enable human review and approval of the manager's plan before execution: +Enable human-in-the-loop (HITL) to allow users to review and approve the manager's proposed plan before execution. This is useful for ensuring that the plan aligns with user expectations and requirements. + +There are two options for plan review: + +1. **Revise**: The user can provide feedback to revise the plan, which will trigger the manage to replan based on the feedback. +2. **Approve**: The user can approve the plan as-is, allowing the workflow to proceed. -### Configure Plan Review +Enaable plan review simply by adding `.with_plan_review()` when building the Magentic workflow: ```python -from typing import cast from agent_framework import ( - MAGENTIC_EVENT_TYPE_AGENT_DELTA, - MAGENTIC_EVENT_TYPE_ORCHESTRATOR, AgentRunUpdateEvent, - MagenticHumanInterventionDecision, - MagenticHumanInterventionKind, - MagenticHumanInterventionReply, - MagenticHumanInterventionRequest, + ChatAgent, + ChatMessage, + MagenticBuilder, + MagenticPlanReviewRequest, RequestInfoEvent, WorkflowOutputEvent, ) workflow = ( MagenticBuilder() - .participants(researcher=researcher_agent, coder=coder_agent) + .participants([researcher_agent, analyst_agent]) .with_standard_manager( agent=manager_agent, max_round_count=10, - max_stall_count=3, + max_stall_count=1, max_reset_count=2, ) - .with_plan_review() # Enable plan review + .with_plan_review() # Request human input for plan review .build() ) ``` -### Handle Plan Review Requests +Plan review requests are emitted as `RequestInfoEvent` with `MagenticPlanReviewRequest` data. You can handle these requests in the event stream: + +> [!TIP] +> Learn more about requests and responses in the [Requests and Responses](../requests-and-responses.md) guide. ```python pending_request: RequestInfoEvent | None = None -pending_responses: dict[str, MagenticHumanInterventionReply] | None = None -completed = False -workflow_output: str | None = None +pending_responses: dict[str, MagenticPlanReviewResponse] | None = None +output_event: WorkflowOutputEvent | None = None -while not completed: - # Use streaming for both initial run and response sending +while not output_event: if pending_responses is not None: stream = workflow.send_responses_streaming(pending_responses) else: stream = workflow.run_stream(task) + last_message_id: str | None = None 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 - if request.plan_text: - print(f"\n=== PLAN REVIEW REQUEST ===\n{request.plan_text}\n") + message_id = event.data.message_id + if message_id != last_message_id: + if last_message_id is not None: + print("\n") + print(f"- {event.executor_id}:", end=" ", flush=True) + last_message_id = message_id + print(event.data, end="", flush=True) + + elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest: + pending_request = event + elif isinstance(event, WorkflowOutputEvent): - workflow_output = str(event.data) if event.data else None - completed = True + output_event = event pending_responses = None - # Handle pending plan review request + # Handle plan review request if any if pending_request is not None: - # Collect human decision (approve/reject/modify) - # For demo, we auto-approve: - reply = MagenticHumanInterventionReply(decision=MagenticHumanInterventionDecision.APPROVE) - - # Or approve with comments: - # reply = MagenticHumanInterventionReply( - # decision=MagenticHumanInterventionDecision.APPROVE, - # comments="Looks good, but prioritize efficiency metrics." - # ) - - # 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 -``` - -## Advanced: Agent Clarification via Tool Approval - -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. - -### Define a Tool with Approval Required - -```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}" -``` - -### Create an Agent with the Tool - -```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], -) -``` - -### Handle Tool Approval Requests - -```python -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, analyst=analyst_agent) - .with_standard_manager( - agent=manager_agent, - max_round_count=10, - 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() -) -``` + event_data = cast(MagenticPlanReviewRequest, pending_request.data) -### 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} + print("\n\n[Magentic Plan Review Request]") + if event_data.current_progress is not None: + print("Current Progress Ledger:") + print(json.dumps(event_data.current_progress.to_dict(), indent=2)) + print() + print(f"Proposed Plan:\n{event_data.plan.text}\n") + print("Please provide your feedback (press Enter to approve):") + + reply = await asyncio.get_event_loop().run_in_executor(None, input, "> ") + if reply.strip() == "": + print("Plan approved.\n") + pending_responses = {pending_request.request_id: event_data.approve()} + else: + print("Plan revised by human.\n") + pending_responses = {pending_request.request_id: event_data.revise(reply)} + pending_request = None ``` ## Key Concepts @@ -355,8 +258,7 @@ async for event in workflow.run_stream(task): - **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 +- **Human Oversight**: Optional human-in-the-loop mechanisms for plan review ## Workflow Execution Flow @@ -367,166 +269,13 @@ The Magentic orchestration follows this execution pattern: 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 +6. **Stall Detection**: If progress stalls, auto-replan with an optional human review process 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: - -```python -import asyncio -import logging -from typing import cast - -from agent_framework import ( - MAGENTIC_EVENT_TYPE_AGENT_DELTA, - MAGENTIC_EVENT_TYPE_ORCHESTRATOR, - AgentRunUpdateEvent, - ChatAgent, - ChatMessage, - HostedCodeInterpreterTool, - MagenticBuilder, - WorkflowOutputEvent, -) -from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient - -logging.basicConfig(level=logging.WARNING) -logger = logging.getLogger(__name__) - -async def main() -> None: - # Define specialized agents - researcher_agent = ChatAgent( - name="ResearcherAgent", - description="Specialist in research and information gathering", - instructions=( - "You are a Researcher. You find information without additional " - "computation or quantitative analysis." - ), - chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"), - ) - - coder_agent = ChatAgent( - name="CoderAgent", - description="A helpful assistant that writes and executes code to process and analyze data.", - instructions="You solve questions using code. Please provide detailed analysis and computation process.", - chat_client=OpenAIResponsesClient(), - tools=HostedCodeInterpreterTool(), - ) - - # 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 - - # Build the workflow - print("\nBuilding Magentic Workflow...") - - workflow = ( - MagenticBuilder() - .participants(researcher=researcher_agent, coder=coder_agent) - .with_standard_manager( - agent=manager_agent, - max_round_count=10, - max_stall_count=3, - max_reset_count=2, - ) - .build() - ) - - # Define the task - task = ( - "I am preparing a report on the energy efficiency of different machine learning model architectures. " - "Compare the estimated training and inference energy consumption of ResNet-50, BERT-base, and GPT-2 " - "on standard datasets (for example, ImageNet for ResNet, GLUE for BERT, WebText for GPT-2). " - "Then, estimate the CO2 emissions associated with each, assuming training on an Azure Standard_NC6s_v3 " - "VM for 24 hours. Provide tables for clarity, and recommend the most energy-efficient model " - "per task type (image classification, text classification, and text generation)." - ) - - print(f"\nTask: {task}") - print("\nStarting workflow execution...") - - # Run the workflow - try: - output: str | None = None - async for event in workflow.run_stream(task): - 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}") - logger.exception("Workflow exception", exc_info=e) - -if __name__ == "__main__": - asyncio.run(main()) -``` - -## Configuration Options - -### Manager Parameters -- `max_round_count`: Maximum number of collaboration rounds (default: 10) -- `max_stall_count`: Maximum rounds without progress before triggering stall handling (default: 3) -- `max_reset_count`: Maximum number of plan resets allowed (default: 2) - -### 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 - -Coming soon... +See complete samples in the [Agent Framework Samples repository](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/workflows/orchestration). ::: zone-end diff --git a/agent-framework/user-guide/workflows/orchestrations/overview.md b/agent-framework/user-guide/workflows/orchestrations/overview.md index 99a377d7f..873ba19a4 100644 --- a/agent-framework/user-guide/workflows/orchestrations/overview.md +++ b/agent-framework/user-guide/workflows/orchestrations/overview.md @@ -10,7 +10,7 @@ ms.service: agent-framework # Microsoft Agent Framework Workflows Orchestrations -Orchestrations are pre-built workflow patterns that allow developers to quickly create complex workflows by simply plugging in their own AI agents. +Orchestrations are pre-built workflow patterns often with specially-built executors that allow developers to quickly create complex workflows by simply plugging in their own AI agents. ## Why Multi-Agent? @@ -18,13 +18,13 @@ Traditional single-agent systems are limited in their ability to handle complex, ## Supported Orchestrations -| Pattern | Description | Typical Use Case | -| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -| [Concurrent](./concurrent.md) | Broadcasts a task to all agents, collects results independently. | Parallel analysis, independent subtasks, ensemble decision making. | -| [Sequential](./sequential.md) | Passes the result from one agent to the next in a defined order. | Step-by-step workflows, pipelines, multi-stage processing. | -| [Group Chat](./group-chat.md) | Coordinates multiple agents in a collaborative conversation with a manager controlling speaker selection and flow. | Iterative refinement, collaborative problem-solving, content review. | -| [Handoff](./handoff.md) | Dynamically passes control between agents based on context or rules. | Dynamic workflows, escalation, fallback, or expert handoff scenarios. | -| [Magentic](./magentic.md) | Inspired by [MagenticOne](https://www.microsoft.com/en-us/research/articles/magentic-one-a-generalist-multi-agent-system-for-solving-complex-tasks/). | Complex, generalist multi-agent collaboration. | +| Pattern | Description | Typical Use Case | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | +| [Concurrent](./concurrent.md) | A task is broadcast to all agents and processed concurrently. | Parallel analysis, independent subtasks, ensemble decision making. | +| [Sequential](./sequential.md) | Passes the result from one agent to the next in a defined order. | Step-by-step workflows, pipelines, multi-stage processing. | +| [Group Chat](./group-chat.md) | Assembles agents in a star topology with a manager controlling the flow of conversation. | Iterative refinement, collaborative problem-solving, content review. | +| [Magentic](./magentic.md) | A variant of group chat with a planner-based manager. Inspired by [MagenticOne](https://www.microsoft.com/en-us/research/articles/magentic-one-a-generalist-multi-agent-system-for-solving-complex-tasks/). | Complex, generalist multi-agent collaboration. | +| [Handoff](./handoff.md) | Assembles agents in a mesh topology where agents can dynamically pass control based on context without a central manager. | Dynamic workflows, escalation, fallback, or expert handoff scenarios. | ## Next Steps diff --git a/agent-framework/user-guide/workflows/orchestrations/sequential.md b/agent-framework/user-guide/workflows/orchestrations/sequential.md index c0db09aff..f385aa76a 100644 --- a/agent-framework/user-guide/workflows/orchestrations/sequential.md +++ b/agent-framework/user-guide/workflows/orchestrations/sequential.md @@ -13,7 +13,12 @@ ms.service: agent-framework In sequential orchestration, agents are organized in a pipeline. Each agent processes the task in turn, passing its output to the next agent in the sequence. This is ideal for workflows where each step builds upon the previous one, such as document review, data processing pipelines, or multi-stage reasoning. -![Sequential Orchestration](../resources/images/orchestration-sequential.png) +

+ Sequential Orchestration +

+ +> [!IMPORTANT] +> The full conversation history from previous agents is passed to the next agent in the sequence. Each agent can see all prior messages, allowing for context-aware processing. ## What You'll Learn @@ -87,7 +92,7 @@ await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); List result = new(); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - if (evt is AgentRunUpdateEvent e) + if (evt is AgentResponseUpdateEvent e) { Console.WriteLine($"{e.ExecutorId}: {e.Data}"); } @@ -120,7 +125,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 `WorkflowOutputEvent` +- **Event Handling**: Monitor agent progress through `AgentResponseUpdateEvent` and completion through `WorkflowOutputEvent` ::: zone-end diff --git a/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat-synchronization.gif b/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat-synchronization.gif new file mode 100644 index 000000000..ad2371e59 Binary files /dev/null and b/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat-synchronization.gif differ diff --git a/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat.png b/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat.png new file mode 100644 index 000000000..10080aa7c Binary files /dev/null and b/agent-framework/user-guide/workflows/resources/images/orchestration-groupchat.png differ diff --git a/agent-framework/user-guide/workflows/resources/images/orchestration-handoff-synchronization.gif b/agent-framework/user-guide/workflows/resources/images/orchestration-handoff-synchronization.gif new file mode 100644 index 000000000..cc50203ae Binary files /dev/null and b/agent-framework/user-guide/workflows/resources/images/orchestration-handoff-synchronization.gif differ diff --git a/agent-framework/user-guide/workflows/resources/images/orchestration-handoff.png b/agent-framework/user-guide/workflows/resources/images/orchestration-handoff.png index 1eb76648e..19fe2827d 100644 Binary files a/agent-framework/user-guide/workflows/resources/images/orchestration-handoff.png and b/agent-framework/user-guide/workflows/resources/images/orchestration-handoff.png differ diff --git a/agent-framework/user-guide/workflows/resources/images/orchestration-sequential-hitl.png b/agent-framework/user-guide/workflows/resources/images/orchestration-sequential-hitl.png new file mode 100644 index 000000000..a5df8ef36 Binary files /dev/null and b/agent-framework/user-guide/workflows/resources/images/orchestration-sequential-hitl.png differ diff --git a/agent-framework/user-guide/workflows/using-agents.md b/agent-framework/user-guide/workflows/using-agents.md index 5e93e346d..d5e9aa9f1 100644 --- a/agent-framework/user-guide/workflows/using-agents.md +++ b/agent-framework/user-guide/workflows/using-agents.md @@ -56,9 +56,9 @@ StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessag await run.TrySendMessageAsync(new TurnToken(emitEvents: true)); await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false)) { - // The agents will run in streaming mode and an AgentRunUpdateEvent + // The agents will run in streaming mode and an AgentResponseUpdateEvent // will be emitted as new chunks are generated. - if (evt is AgentRunUpdateEvent agentRunUpdate) + if (evt is AgentResponseUpdateEvent agentRunUpdate) { Console.WriteLine($"{agentRunUpdate.ExecutorId}: {agentRunUpdate.Data}"); } @@ -118,7 +118,7 @@ Whenever the executor receives a message of one of these types, it will trigger Two possible event type related to the agents' responses can be emitted when running the workflow: -- `AgentRunUpdateEvent` containing chunks of the agent's response as they are generated in streaming mode. +- `AgentResponseUpdateEvent` containing chunks of the agent's response as they are generated in streaming mode. - `AgentRunEvent` containing the full response from the agent in non-streaming mode. > By default, agents are wrapped in executors that run in streaming mode. You can customize this behavior by creating a custom executor. See the next section for more details. @@ -126,7 +126,7 @@ Two possible event type related to the agents' responses can be emitted when run ```python last_executor_id = None async for event in workflow.run_streaming("Write a short blog post about AI agents."): - if isinstance(event, AgentRunUpdateEvent): + if isinstance(event, AgentResponseUpdateEvent): if event.executor_id != last_executor_id: if last_executor_id is not None: print()