diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index bf3ff94eba3..3a85c615b7a 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -7,33 +7,33 @@
- 9.5.2
+ 13.0.0
-
+
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -46,27 +46,27 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -101,7 +101,7 @@
-
+
@@ -115,7 +115,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 71c79efc443..25030bedccb 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -63,6 +63,7 @@
+
@@ -161,7 +162,7 @@
-
+
diff --git a/dotnet/samples/HostedAgents/DeepResearchAgent/DeepResearchAgent.csproj b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj
similarity index 75%
rename from dotnet/samples/HostedAgents/DeepResearchAgent/DeepResearchAgent.csproj
rename to dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj
index 7ae71d83de6..11c7beb3bf7 100644
--- a/dotnet/samples/HostedAgents/DeepResearchAgent/DeepResearchAgent.csproj
+++ b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Agent_Step18_DeepResearch.csproj
@@ -14,7 +14,7 @@
-
+
diff --git a/dotnet/samples/HostedAgents/DeepResearchAgent/Program.cs b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs
similarity index 100%
rename from dotnet/samples/HostedAgents/DeepResearchAgent/Program.cs
rename to dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/Program.cs
diff --git a/dotnet/samples/HostedAgents/DeepResearchAgent/README.md b/dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/README.md
similarity index 100%
rename from dotnet/samples/HostedAgents/DeepResearchAgent/README.md
rename to dotnet/samples/GettingStarted/Agents/Agent_Step18_DeepResearch/README.md
diff --git a/dotnet/samples/GettingStarted/Agents/README.md b/dotnet/samples/GettingStarted/Agents/README.md
index b93e9ceb723..f510b03fafe 100644
--- a/dotnet/samples/GettingStarted/Agents/README.md
+++ b/dotnet/samples/GettingStarted/Agents/README.md
@@ -44,6 +44,7 @@ Before you begin, ensure you have the following prerequisites:
|[Using plugins with an agent](./Agent_Step15_Plugins/)|This sample demonstrates how to use plugins with an agent|
|[Reducing chat history size](./Agent_Step16_ChatReduction/)|This sample demonstrates how to reduce the chat history to constrain its size, where chat history is maintained locally|
|[Background responses](./Agent_Step17_BackgroundResponses/)|This sample demonstrates how to use background responses for long-running operations with polling and resumption support|
+|[Deep research with an agent](./Agent_Step18_DeepResearch/)|This sample demonstrates how to use the Deep Research Tool to perform comprehensive research on complex topics|
## Running the samples from the console
diff --git a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
index 0415f0e0e08..7fded8c55bb 100644
--- a/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
+++ b/dotnet/samples/GettingStarted/DevUI/DevUI_Step01_BasicUsage/Program.cs
@@ -2,6 +2,7 @@
// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
+using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -18,10 +19,11 @@ namespace DevUI_Step01_BasicUsage;
///
/// This sample shows how to:
/// 1. Set up Azure OpenAI as the chat client
-/// 2. Register agents and workflows using the hosting packages
-/// 3. Map the DevUI endpoint which automatically configures the middleware
-/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
-/// 5. Access the DevUI in a web browser
+/// 2. Create function tools for agents to use
+/// 3. Register agents and workflows using the hosting packages with tools
+/// 4. Map the DevUI endpoint which automatically configures the middleware
+/// 5. Map the dynamic OpenAI Responses API for Python DevUI compatibility
+/// 6. Access the DevUI in a web browser
///
/// The DevUI provides an interactive web interface for testing and debugging AI agents.
/// DevUI assets are served from embedded resources within the assembly.
@@ -50,10 +52,30 @@ private static void Main(string[] args)
builder.Services.AddChatClient(chatClient);
- // Register sample agents
- builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
+ // Define some example tools
+ [Description("Get the weather for a given location.")]
+ static string GetWeather([Description("The location to get the weather for.")] string location)
+ => $"The weather in {location} is cloudy with a high of 15°C.";
+
+ [Description("Calculate the sum of two numbers.")]
+ static double Add([Description("The first number.")] double a, [Description("The second number.")] double b)
+ => a + b;
+
+ [Description("Get the current time.")]
+ static string GetCurrentTime()
+ => DateTime.Now.ToString("HH:mm:ss");
+
+ // Register sample agents with tools
+ builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.")
+ .WithAITools(
+ AIFunctionFactory.Create(GetWeather, name: "get_weather"),
+ AIFunctionFactory.Create(GetCurrentTime, name: "get_current_time")
+ );
+
builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
- builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
+
+ builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.")
+ .WithAITool(AIFunctionFactory.Create(Add, name: "add"));
// Register sample workflows
var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj b/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj
new file mode 100644
index 00000000000..3130cda647d
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/AgentWithHostedMCP.csproj
@@ -0,0 +1,69 @@
+
+
+
+ Exe
+ net9.0
+
+ enable
+ enable
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile
new file mode 100644
index 00000000000..776f81041e5
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Dockerfile
@@ -0,0 +1,20 @@
+# Build the application
+FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+WORKDIR /src
+
+# Copy files from the current directory on the host to the working directory in the container
+COPY . .
+
+RUN dotnet restore
+RUN dotnet build -c Release --no-restore
+RUN dotnet publish -c Release --no-build -o /app
+
+# Run the application
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+WORKDIR /app
+
+# Copy everything needed to run the app from the "build" stage.
+COPY --from=build /app .
+
+EXPOSE 8088
+ENTRYPOINT ["dotnet", "AgentWithHostedMCP.dll"]
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/Program.cs b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Program.cs
new file mode 100644
index 00000000000..d6bfca7bf74
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/Program.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to create and use a simple AI agent with OpenAI Responses as the backend, that uses a Hosted MCP Tool.
+// In this case the OpenAI responses service will invoke any MCP tools as required. MCP tools are not invoked by the Agent Framework.
+// The sample demonstrates how to use MCP tools with auto approval by setting ApprovalMode to NeverRequire.
+
+using Azure.AI.AgentServer.AgentFramework.Extensions;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
+
+// Create an MCP tool that can be called without approval.
+AITool mcpTool = new HostedMcpServerTool(serverName: "microsoft_learn", serverAddress: "https://learn.microsoft.com/api/mcp")
+{
+ AllowedTools = ["microsoft_docs_search"],
+ ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire
+};
+
+// Create an agent with the MCP tool using Azure OpenAI Responses.
+AIAgent agent = new AzureOpenAIClient(
+ new Uri(endpoint),
+ new AzureCliCredential())
+ .GetOpenAIResponseClient(deploymentName)
+ .CreateAIAgent(
+ instructions: "You answer questions by searching the Microsoft Learn content only.",
+ name: "MicrosoftLearnAgent",
+ tools: [mcpTool]);
+
+await agent.RunAIAgentAsync();
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/README.md b/dotnet/samples/HostedAgents/AgentWithHostedMCP/README.md
new file mode 100644
index 00000000000..a5648d7ac97
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/README.md
@@ -0,0 +1,43 @@
+# What this sample demonstrates
+
+This sample demonstrates how to use a Hosted Model Context Protocol (MCP) server with an AI agent.
+The agent connects to the Microsoft Learn MCP server to search documentation and answer questions using official Microsoft content.
+
+Key features:
+- Configuring MCP tools with automatic approval (no user confirmation required)
+- Filtering available tools from an MCP server
+- Using Azure OpenAI Responses with MCP tools
+
+## Prerequisites
+
+Before running this sample, ensure you have:
+
+1. An Azure OpenAI endpoint configured
+2. A deployment of a chat model (e.g., gpt-4o-mini)
+3. Azure CLI installed and authenticated
+
+**Note**: This sample uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure OpenAI resource.
+
+## Environment Variables
+
+Set the following environment variables:
+
+```powershell
+# Replace with your Azure OpenAI endpoint
+$env:AZURE_OPENAI_ENDPOINT="https://your-openai-resource.openai.azure.com/"
+
+# Optional, defaults to gpt-4o-mini
+$env:AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o-mini"
+```
+
+## How It Works
+
+The sample connects to the Microsoft Learn MCP server and uses its documentation search capabilities:
+
+1. The agent is configured with a HostedMcpServerTool pointing to `https://learn.microsoft.com/api/mcp`
+2. Only the `microsoft_docs_search` tool is enabled from the available MCP tools
+3. Approval mode is set to `NeverRequire`, allowing automatic tool execution
+4. When you ask questions, Azure OpenAI Responses automatically invokes the MCP tool to search documentation
+5. The agent returns answers based on the Microsoft Learn content
+
+In this configuration, the OpenAI Responses service manages tool invocation directly - the Agent Framework does not handle MCP tool calls.
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/agent.yaml b/dotnet/samples/HostedAgents/AgentWithHostedMCP/agent.yaml
new file mode 100644
index 00000000000..1a484bfade8
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/agent.yaml
@@ -0,0 +1,29 @@
+name: AgentWithHostedMCP
+displayName: "Microsoft Learn Response Agent with MCP"
+description: >
+ An AI agent that uses Azure OpenAI Responses with a Hosted Model Context Protocol (MCP) server.
+ The agent answers questions by searching Microsoft Learn documentation using MCP tools.
+ This demonstrates how MCP tools can be integrated with Azure OpenAI Responses where the service
+ itself handles tool invocation.
+metadata:
+ authors:
+ - Microsoft Agent Framework Team
+ tags:
+ - Microsoft Agent Framework
+ - Model Context Protocol
+ - MCP
+template:
+ kind: hosted
+ name: AgentWithHostedMCP
+ protocols:
+ - protocol: responses
+ version: v1
+ environment_variables:
+ - name: AZURE_OPENAI_ENDPOINT
+ value: ${AZURE_OPENAI_ENDPOINT}
+ - name: AZURE_OPENAI_DEPLOYMENT_NAME
+ value: gpt-4o-mini
+resources:
+ - name: "gpt-4o-mini"
+ kind: model
+ id: gpt-4o-mini
diff --git a/dotnet/samples/HostedAgents/AgentWithHostedMCP/run-requests.http b/dotnet/samples/HostedAgents/AgentWithHostedMCP/run-requests.http
new file mode 100644
index 00000000000..cc26f43b904
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithHostedMCP/run-requests.http
@@ -0,0 +1,30 @@
+@host = http://localhost:8088
+@endpoint = {{host}}/responses
+
+### Health Check
+GET {{host}}/readiness
+
+### Simple string input - Ask about MCP Tools
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": "Please summarize the Azure AI Agent documentation related to MCP Tool calling?"
+}
+
+### Explicit input - Ask about Agent Framework
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "What is the Microsoft Agent Framework?"
+ }
+ ]
+ }
+ ]
+}
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
index c6bab8327e1..4aafb7582a3 100644
--- a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj
@@ -6,16 +6,64 @@
enable
enable
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile
new file mode 100644
index 00000000000..b494ad22549
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Dockerfile
@@ -0,0 +1,20 @@
+# Build the application
+FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+WORKDIR /src
+
+# Copy files from the current directory on the host to the working directory in the container
+COPY . .
+
+RUN dotnet restore
+RUN dotnet build -c Release --no-restore
+RUN dotnet publish -c Release --no-build -o /app
+
+# Run the application
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+WORKDIR /app
+
+# Copy everything needed to run the app from the "build" stage.
+COPY --from=build /app .
+
+EXPOSE 8088
+ENTRYPOINT ["dotnet", "AgentWithTextSearchRag.dll"]
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Program.cs b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Program.cs
index 65f3a9e98f8..94552a80141 100644
--- a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Program.cs
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/Program.cs
@@ -4,6 +4,7 @@
// capabilities to an AI agent. The provider runs a search against an external knowledge base
// before each model invocation and injects the results into the model context.
+using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -23,7 +24,7 @@
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
- new AzureCliCredential())
+ new DefaultAzureCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(new ChatClientAgentOptions
{
@@ -31,16 +32,7 @@
AIContextProviderFactory = ctx => new TextSearchProvider(MockSearchAsync, ctx.SerializedState, ctx.JsonSerializerOptions, textSearchOptions)
});
-AgentThread thread = agent.GetNewThread();
-
-Console.WriteLine(">> Asking about returns\n");
-Console.WriteLine(await agent.RunAsync("Hi! I need help understanding the return policy.", thread));
-
-Console.WriteLine("\n>> Asking about shipping\n");
-Console.WriteLine(await agent.RunAsync("How long does standard shipping usually take?", thread));
-
-Console.WriteLine("\n>> Asking about product care\n");
-Console.WriteLine(await agent.RunAsync("What is the best way to maintain the TrailRunner tent fabric?", thread));
+await agent.RunAIAgentAsync();
static Task> MockSearchAsync(string query, CancellationToken cancellationToken)
{
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/agent.yaml b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/agent.yaml
new file mode 100644
index 00000000000..78e45e9c565
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/agent.yaml
@@ -0,0 +1,30 @@
+name: AgentWithTextSearchRag
+displayName: "Text Search RAG Agent"
+description: >
+ An AI agent that uses TextSearchProvider for retrieval augmented generation (RAG) capabilities.
+ The agent runs searches against an external knowledge base before each model invocation and
+ injects the results into the model context. It can answer questions about Contoso Outdoors
+ policies and products, including return policies, refunds, shipping options, and product care
+ instructions such as tent maintenance.
+metadata:
+ authors:
+ - Microsoft Agent Framework Team
+ tags:
+ - Microsoft Agent Framework
+ - Retrieval-Augmented Generation
+ - RAG
+template:
+ kind: hosted
+ name: AgentWithTextSearchRag
+ protocols:
+ - protocol: responses
+ version: v1
+ environment_variables:
+ - name: AZURE_OPENAI_ENDPOINT
+ value: ${AZURE_OPENAI_ENDPOINT}
+ - name: AZURE_OPENAI_DEPLOYMENT_NAME
+ value: gpt-4o-mini
+resources:
+ - name: "gpt-4o-mini"
+ kind: model
+ id: gpt-4o-mini
diff --git a/dotnet/samples/HostedAgents/AgentWithTextSearchRag/run-requests.http b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/run-requests.http
new file mode 100644
index 00000000000..4bfb02d8f8b
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentWithTextSearchRag/run-requests.http
@@ -0,0 +1,30 @@
+@host = http://localhost:8088
+@endpoint = {{host}}/responses
+
+### Health Check
+GET {{host}}/readiness
+
+### Simple string input
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": "Hi! I need help understanding the return policy."
+}
+
+### Explicit input
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "How long does standard shipping usually take?"
+ }
+ ]
+ }
+ ]
+}
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
index f192c199013..7e70caabdaf 100644
--- a/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/AgentsInWorkflows.csproj
@@ -6,18 +6,64 @@
enable
enable
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
-
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile b/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile
new file mode 100644
index 00000000000..0d3e5757cde
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/Dockerfile
@@ -0,0 +1,20 @@
+# Build the application
+FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
+WORKDIR /src
+
+# Copy files from the current directory on the host to the working directory in the container
+COPY . .
+
+RUN dotnet restore
+RUN dotnet build -c Release --no-restore
+RUN dotnet publish -c Release --no-build -o /app
+
+# Run the application
+FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final
+WORKDIR /app
+
+# Copy everything needed to run the app from the "build" stage.
+COPY --from=build /app .
+
+EXPOSE 8088
+ENTRYPOINT ["dotnet", "AgentsInWorkflows.dll"]
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/Program.cs b/dotnet/samples/HostedAgents/AgentsInWorkflows/Program.cs
index 3e01f6e7177..b1d8a922fd4 100644
--- a/dotnet/samples/HostedAgents/AgentsInWorkflows/Program.cs
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/Program.cs
@@ -4,6 +4,7 @@
// Three translation agents are connected sequentially to create a translation chain:
// English → French → Spanish → English, showing how agents can be composed as workflow executors.
+using Azure.AI.AgentServer.AgentFramework.Extensions;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -14,7 +15,7 @@
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
-IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
+IChatClient chatClient = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deploymentName)
.AsIChatClient();
@@ -23,26 +24,14 @@
AIAgent spanishAgent = GetTranslationAgent("Spanish", chatClient);
AIAgent englishAgent = GetTranslationAgent("English", chatClient);
-// Build the workflow by adding executors and connecting them
-Workflow workflow = new WorkflowBuilder(frenchAgent)
+// Build the workflow and turn it into an agent
+AIAgent agent = new WorkflowBuilder(frenchAgent)
.AddEdge(frenchAgent, spanishAgent)
.AddEdge(spanishAgent, englishAgent)
-.Build();
-
-// Execute the workflow
-await using StreamingRun run = await InProcessExecution.StreamAsync(workflow, new ChatMessage(ChatRole.User, "Hello World!"));
-
-// Must send the turn token to trigger the agents.
-// The agents are wrapped as executors. When they receive messages,
-// they will cache the messages and only start processing when they receive a TurnToken.
-await run.TrySendMessageAsync(new TurnToken(emitEvents: true));
-await foreach (WorkflowEvent evt in run.WatchStreamAsync())
-{
- if (evt is AgentRunUpdateEvent executorComplete)
- {
- Console.WriteLine($"{executorComplete.ExecutorId}: {executorComplete.Data}");
- }
-}
+ .Build()
+ .AsAgent();
+
+await agent.RunAIAgentAsync();
static ChatClientAgent GetTranslationAgent(string targetLanguage, IChatClient chatClient) =>
new(chatClient, $"You are a translation assistant that translates the provided text to {targetLanguage}.");
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/agent.yaml b/dotnet/samples/HostedAgents/AgentsInWorkflows/agent.yaml
new file mode 100644
index 00000000000..5e9360efd91
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/agent.yaml
@@ -0,0 +1,27 @@
+name: AgentsInWorkflows
+displayName: "Translation Chain Workflow Agent"
+description: >
+ A workflow agent that performs sequential translation through multiple languages.
+ The agent translates text from English to French, then to Spanish, and finally back
+ to English, leveraging AI-powered translation capabilities in a pipeline workflow.
+metadata:
+ authors:
+ - Microsoft Agent Framework Team
+ tags:
+ - Microsoft Agent Framework
+ - Workflows
+template:
+ kind: hosted
+ name: AgentsInWorkflows
+ protocols:
+ - protocol: responses
+ version: v1
+ environment_variables:
+ - name: AZURE_OPENAI_ENDPOINT
+ value: ${AZURE_OPENAI_ENDPOINT}
+ - name: AZURE_OPENAI_DEPLOYMENT_NAME
+ value: gpt-4o-mini
+resources:
+ - name: "gpt-4o-mini"
+ kind: model
+ id: gpt-4o-mini
diff --git a/dotnet/samples/HostedAgents/AgentsInWorkflows/run-requests.http b/dotnet/samples/HostedAgents/AgentsInWorkflows/run-requests.http
new file mode 100644
index 00000000000..5c33700a936
--- /dev/null
+++ b/dotnet/samples/HostedAgents/AgentsInWorkflows/run-requests.http
@@ -0,0 +1,30 @@
+@host = http://localhost:8088
+@endpoint = {{host}}/responses
+
+### Health Check
+GET {{host}}/readiness
+
+### Simple string input
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": "Hello, how are you today?"
+}
+
+### Explicit input
+POST {{endpoint}}
+Content-Type: application/json
+{
+ "input": [
+ {
+ "type": "message",
+ "role": "user",
+ "content": [
+ {
+ "type": "input_text",
+ "text": "Hello, how are you today?"
+ }
+ ]
+ }
+ ]
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
index 3acc8d48d3a..09b95769a9a 100644
--- a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntitiesJsonContext.cs
@@ -18,9 +18,13 @@ namespace Microsoft.Agents.AI.DevUI.Entities;
[JsonSerializable(typeof(MetaResponse))]
[JsonSerializable(typeof(EnvVarRequirement))]
[JsonSerializable(typeof(List))]
-[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List>))]
+[JsonSerializable(typeof(List>))]
[JsonSerializable(typeof(Dictionary))]
-[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(Dictionary>))]
+[JsonSerializable(typeof(Dictionary))]
[JsonSerializable(typeof(JsonElement))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(int))]
[ExcludeFromCodeCoverage]
internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
index 8b5e4e54929..7b711b36c26 100644
--- a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/EntityInfo.cs
@@ -36,16 +36,16 @@ internal sealed record EntityInfo(
string Name,
[property: JsonPropertyName("description")]
- string? Description = null,
+ string? Description,
[property: JsonPropertyName("framework")]
- string Framework = "dotnet",
+ string Framework,
[property: JsonPropertyName("tools")]
- List? Tools = null,
+ List Tools,
[property: JsonPropertyName("metadata")]
- Dictionary? Metadata = null
+ Dictionary Metadata
)
{
[JsonPropertyName("source")]
@@ -54,6 +54,32 @@ internal sealed record EntityInfo(
[JsonPropertyName("original_url")]
public string? OriginalUrl { get; init; }
+ // Deployment support
+ [JsonPropertyName("deployment_supported")]
+ public bool DeploymentSupported { get; init; }
+
+ [JsonPropertyName("deployment_reason")]
+ public string? DeploymentReason { get; init; }
+
+ // Agent-specific fields
+ [JsonPropertyName("instructions")]
+ public string? Instructions { get; init; }
+
+ [JsonPropertyName("model_id")]
+ public string? ModelId { get; init; }
+
+ [JsonPropertyName("chat_client_type")]
+ public string? ChatClientType { get; init; }
+
+ [JsonPropertyName("context_providers")]
+ public List? ContextProviders { get; init; }
+
+ [JsonPropertyName("middleware")]
+ public List? Middleware { get; init; }
+
+ [JsonPropertyName("module_path")]
+ public string? ModulePath { get; init; }
+
// Workflow-specific fields
[JsonPropertyName("required_env_vars")]
public List? RequiredEnvVars { get; init; }
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
index 81ce6182d10..44fc8b1eb46 100644
--- a/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Entities/WorkflowSerializationExtensions.cs
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -17,31 +19,37 @@ internal static class WorkflowSerializationExtensions
/// Converts a workflow to a dictionary representation compatible with DevUI frontend.
/// This matches the Python workflow.to_dict() format expected by the UI.
///
- public static Dictionary ToDevUIDict(this Workflow workflow)
+ /// The workflow to convert.
+ /// A dictionary with string keys and JsonElement values containing the workflow data.
+ public static Dictionary ToDevUIDict(this Workflow workflow)
{
- var result = new Dictionary
+ var result = new Dictionary
{
- ["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
- ["start_executor_id"] = workflow.StartExecutorId,
- ["max_iterations"] = MaxIterationsDefault
+ ["id"] = Serialize(workflow.Name ?? Guid.NewGuid().ToString(), EntitiesJsonContext.Default.String),
+ ["start_executor_id"] = Serialize(workflow.StartExecutorId, EntitiesJsonContext.Default.String),
+ ["max_iterations"] = Serialize(MaxIterationsDefault, EntitiesJsonContext.Default.Int32)
};
// Add optional fields
if (!string.IsNullOrEmpty(workflow.Name))
{
- result["name"] = workflow.Name;
+ result["name"] = Serialize(workflow.Name, EntitiesJsonContext.Default.String);
}
if (!string.IsNullOrEmpty(workflow.Description))
{
- result["description"] = workflow.Description;
+ result["description"] = Serialize(workflow.Description, EntitiesJsonContext.Default.String);
}
// Convert executors to Python-compatible format
- result["executors"] = ConvertExecutorsToDict(workflow);
+ result["executors"] = Serialize(
+ ConvertExecutorsToDict(workflow),
+ EntitiesJsonContext.Default.DictionaryStringDictionaryStringString);
// Convert edges to edge_groups format
- result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
+ result["edge_groups"] = Serialize(
+ ConvertEdgesToEdgeGroups(workflow),
+ EntitiesJsonContext.Default.ListDictionaryStringJsonElement);
return result;
}
@@ -49,9 +57,9 @@ public static Dictionary ToDevUIDict(this Workflow workflow)
///
/// Converts workflow executors to a dictionary format compatible with Python
///
- private static Dictionary ConvertExecutorsToDict(Workflow workflow)
+ private static Dictionary> ConvertExecutorsToDict(Workflow workflow)
{
- var executors = new Dictionary();
+ var executors = new Dictionary>();
// Extract executor IDs from edges and start executor
// (Registrations is internal, so we infer executors from the graph structure)
@@ -73,7 +81,7 @@ private static Dictionary ConvertExecutorsToDict(Workflow workfl
// Create executor entries (we can't access internal Registrations for type info)
foreach (var executorId in executorIds)
{
- executors[executorId] = new Dictionary
+ executors[executorId] = new Dictionary
{
["id"] = executorId,
["type"] = "Executor"
@@ -86,9 +94,9 @@ private static Dictionary ConvertExecutorsToDict(Workflow workfl
///
/// Converts workflow edges to edge_groups format expected by the UI
///
- private static List ConvertEdgesToEdgeGroups(Workflow workflow)
+ private static List> ConvertEdgesToEdgeGroups(Workflow workflow)
{
- var edgeGroups = new List();
+ var edgeGroups = new List>();
var edgeGroupId = 0;
// Get edges using the public ReflectEdges method
@@ -101,13 +109,13 @@ private static List ConvertEdgesToEdgeGroups(Workflow workflow)
if (edgeInfo is DirectEdgeInfo directEdge)
{
// Single edge group for direct edges
- var edges = new List();
+ var edges = new List>();
foreach (var source in directEdge.Connection.SourceIds)
{
foreach (var sink in directEdge.Connection.SinkIds)
{
- var edge = new Dictionary
+ var edge = new Dictionary
{
["source_id"] = source,
["target_id"] = sink
@@ -123,23 +131,25 @@ private static List ConvertEdgesToEdgeGroups(Workflow workflow)
}
}
- edgeGroups.Add(new Dictionary
+ var edgeGroup = new Dictionary
{
- ["id"] = $"edge_group_{edgeGroupId++}",
- ["type"] = "SingleEdgeGroup",
- ["edges"] = edges
- });
+ ["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
+ ["type"] = Serialize("SingleEdgeGroup", EntitiesJsonContext.Default.String),
+ ["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
+ };
+
+ edgeGroups.Add(edgeGroup);
}
else if (edgeInfo is FanOutEdgeInfo fanOutEdge)
{
// FanOut edge group
- var edges = new List();
+ var edges = new List>();
foreach (var source in fanOutEdge.Connection.SourceIds)
{
foreach (var sink in fanOutEdge.Connection.SinkIds)
{
- edges.Add(new Dictionary
+ edges.Add(new Dictionary
{
["source_id"] = source,
["target_id"] = sink
@@ -147,16 +157,16 @@ private static List ConvertEdgesToEdgeGroups(Workflow workflow)
}
}
- var fanOutGroup = new Dictionary
+ var fanOutGroup = new Dictionary
{
- ["id"] = $"edge_group_{edgeGroupId++}",
- ["type"] = "FanOutEdgeGroup",
- ["edges"] = edges
+ ["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
+ ["type"] = Serialize("FanOutEdgeGroup", EntitiesJsonContext.Default.String),
+ ["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
};
if (fanOutEdge.HasAssigner)
{
- fanOutGroup["selection_func_name"] = "selector";
+ fanOutGroup["selection_func_name"] = Serialize("selector", EntitiesJsonContext.Default.String);
}
edgeGroups.Add(fanOutGroup);
@@ -164,13 +174,13 @@ private static List ConvertEdgesToEdgeGroups(Workflow workflow)
else if (edgeInfo is FanInEdgeInfo fanInEdge)
{
// FanIn edge group
- var edges = new List();
+ var edges = new List>();
foreach (var source in fanInEdge.Connection.SourceIds)
{
foreach (var sink in fanInEdge.Connection.SinkIds)
{
- edges.Add(new Dictionary
+ edges.Add(new Dictionary
{
["source_id"] = source,
["target_id"] = sink
@@ -178,16 +188,20 @@ private static List ConvertEdgesToEdgeGroups(Workflow workflow)
}
}
- edgeGroups.Add(new Dictionary
+ var edgeGroup = new Dictionary
{
- ["id"] = $"edge_group_{edgeGroupId++}",
- ["type"] = "FanInEdgeGroup",
- ["edges"] = edges
- });
+ ["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
+ ["type"] = Serialize("FanInEdgeGroup", EntitiesJsonContext.Default.String),
+ ["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
+ };
+
+ edgeGroups.Add(edgeGroup);
}
}
}
return edgeGroups;
}
+
+ private static JsonElement Serialize(T value, JsonTypeInfo typeInfo) => JsonSerializer.SerializeToElement(value, typeInfo);
}
diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
index eb41fe90b89..29b7dc588a5 100644
--- a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs
@@ -6,6 +6,7 @@
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
+using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DevUI;
@@ -56,21 +57,21 @@ private static async Task ListEntitiesAsync(
{
try
{
- var entities = new List();
+ var entities = new Dictionary();
// Discover agents
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
- entities.Add(agentInfo);
+ entities[agentInfo.Id] = agentInfo;
}
// Discover workflows
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
- entities.Add(workflowInfo);
+ entities[workflowInfo.Id] = workflowInfo;
}
- return Results.Json(new DiscoveryResponse([.. entities]), EntitiesJsonContext.Default.DiscoveryResponse);
+ return Results.Json(new DiscoveryResponse([.. entities.Values.OrderBy(e => e.Id)]), EntitiesJsonContext.Default.DiscoveryResponse);
}
catch (Exception ex)
{
@@ -90,19 +91,19 @@ private static async Task GetEntityInfoAsync(
{
try
{
- if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
+ if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
{
- await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
+ await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
- return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
+ return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
}
}
- if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
+ if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
- await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
+ await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
- return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo);
+ return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
}
}
@@ -180,17 +181,82 @@ private static async IAsyncEnumerable DiscoverWorkflowsAsync(
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
{
var entityId = agent.Name ?? agent.Id;
+
+ // Extract tools and other metadata using GetService
+ List tools = [];
+ var metadata = new Dictionary();
+
+ // Try to get ChatOptions from the agent which may contain tools
+ if (agent.GetService() is { Tools: { Count: > 0 } agentTools })
+ {
+ tools = agentTools
+ .Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
+ .Select(tool => tool.Name!)
+ .Distinct()
+ .ToList();
+ }
+
+ // Extract agent-specific fields (top-level properties for compatibility with Python)
+ string? instructions = null;
+ string? modelId = null;
+ string? chatClientType = null;
+
+ // Get instructions from ChatClientAgent
+ if (agent is ChatClientAgent chatAgent && !string.IsNullOrWhiteSpace(chatAgent.Instructions))
+ {
+ instructions = chatAgent.Instructions;
+ }
+
+ // Get IChatClient to extract metadata
+ IChatClient? chatClient = agent.GetService();
+ if (chatClient != null)
+ {
+ // Get chat client type
+ chatClientType = chatClient.GetType().Name;
+
+ // Get model ID from ChatClientMetadata
+ if (chatClient.GetService() is { } chatClientMetadata)
+ {
+ modelId = chatClientMetadata.DefaultModelId;
+
+ // Add additional metadata for compatibility
+ if (!string.IsNullOrWhiteSpace(chatClientMetadata.ProviderName))
+ {
+ metadata["chat_client_provider"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderName, EntitiesJsonContext.Default.String);
+ }
+
+ if (chatClientMetadata.ProviderUri is not null)
+ {
+ metadata["provider_uri"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderUri.ToString(), EntitiesJsonContext.Default.String);
+ }
+ }
+ }
+
+ // Add provider name from AIAgentMetadata if available
+ if (agent.GetService() is { } agentMetadata && !string.IsNullOrWhiteSpace(agentMetadata.ProviderName))
+ {
+ metadata["provider_name"] = JsonSerializer.SerializeToElement(agentMetadata.ProviderName, EntitiesJsonContext.Default.String);
+ }
+
+ // Add agent type information to metadata (in addition to chat_client_type)
+ var agentTypeName = agent.GetType().Name;
+ metadata["agent_type"] = JsonSerializer.SerializeToElement(agentTypeName, EntitiesJsonContext.Default.String);
+
return new EntityInfo(
Id: entityId,
Type: "agent",
- Name: entityId,
+ Name: agent.DisplayName,
Description: agent.Description,
- Framework: "agent-framework",
- Tools: null,
- Metadata: []
+ Framework: "agent_framework",
+ Tools: tools,
+ Metadata: metadata
)
{
- Source = "in_memory"
+ Source = "in_memory",
+ Instructions = instructions,
+ ModelId = modelId,
+ ChatClientType = chatClientType,
+ Executors = [], // Agents have empty executors list (workflows use this field)
};
}
@@ -212,7 +278,7 @@ private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
}
// Create a default input schema (string type)
- var defaultInputSchema = new Dictionary
+ var defaultInputSchema = new Dictionary
{
["type"] = "string"
};
@@ -223,14 +289,17 @@ private static EntityInfo CreateWorkflowEntityInfo(Workflow workflow)
Type: "workflow",
Name: workflowId,
Description: workflow.Description,
- Framework: "agent-framework",
- Tools: [.. executorIds],
+ Framework: "agent_framework",
+ Tools: [],
Metadata: []
)
{
Source = "in_memory",
- WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
- InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
+ Executors = [.. executorIds], // Workflows use Executors instead of Tools
+ WorkflowDump = JsonSerializer.SerializeToElement(
+ workflow.ToDevUIDict(),
+ EntitiesJsonContext.Default.DictionaryStringJsonElement),
+ InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema, EntitiesJsonContext.Default.DictionaryStringString),
InputTypeName = "string",
StartExecutorId = workflow.StartExecutorId
};
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs
index 87b0637b9b3..412494eeaaf 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs
@@ -160,5 +160,5 @@ internal sealed record CustomToolFormat
/// Additional format properties (schema definition).
///
[JsonExtensionData]
- public Dictionary? AdditionalProperties { get; init; }
+ public Dictionary? AdditionalProperties { get; set; }
}
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
index 46f893e531a..d04d9bb9fb4 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs
@@ -281,6 +281,8 @@ public override async IAsyncEnumerable RunStreamingAsync
base.GetService(serviceType, serviceKey) ??
(serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
: serviceType == typeof(IChatClient) ? this.ChatClient
+ : serviceType == typeof(ChatOptions) ? this._agentOptions?.ChatOptions
+ : serviceType == typeof(ChatClientAgentOptions) ? this._agentOptions
: this.ChatClient.GetService(serviceType, serviceKey));
///
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
index d223a65e28c..fbb087a1530 100644
--- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
@@ -68,7 +68,7 @@ public async Task CreateChatClientAgentAsync(
string name = "HelpfulAssistant",
string instructions = "You are a helpful assistant.",
IList? aiTools = null) =>
- new ChatClientAgent(
+ new(
this._openAIResponseClient.AsIChatClient(),
options: new()
{
diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md
index 500c0b45cda..e198323555b 100644
--- a/python/CHANGELOG.md
+++ b/python/CHANGELOG.md
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.0.0b251112] - 2025-11-12
+
+### Added
+
+- **agent-framework-azure-ai**: Azure AI client based on new `azure-ai-projects` package ([#1910](https://github.com/microsoft/agent-framework/pull/1910))
+- **agent-framework-anthropic**: Add convenience method on data content ([#2083](https://github.com/microsoft/agent-framework/pull/2083))
+
+### Changed
+
+- **agent-framework-core**: Update OpenAI samples to use agents ([#2012](https://github.com/microsoft/agent-framework/pull/2012))
+
+### Fixed
+
+- **agent-framework-anthropic**: Fixed image handling in Anthropic client ([#2083](https://github.com/microsoft/agent-framework/pull/2083))
+
## [1.0.0b251111] - 2025-11-11
### Added
@@ -204,7 +219,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
-[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...HEAD
+[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251112...HEAD
+[1.0.0b251112]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...python-1.0.0b251112
[1.0.0b251111]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251108...python-1.0.0b251111
[1.0.0b251108]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106.post1...python-1.0.0b251108
[1.0.0b251106.post1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106...python-1.0.0b251106.post1
diff --git a/python/packages/a2a/pyproject.toml b/python/packages/a2a/pyproject.toml
index 2780bdd481a..7b8cfedee33 100644
--- a/python/packages/a2a/pyproject.toml
+++ b/python/packages/a2a/pyproject.toml
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml
index 9216a17e242..63a4c60fdb9 100644
--- a/python/packages/ag-ui/pyproject.toml
+++ b/python/packages/ag-ui/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py
index d7b0334934e..303ea0ee20d 100644
--- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py
+++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py
@@ -330,11 +330,19 @@ def _convert_message_to_anthropic_format(self, message: ChatMessage) -> dict[str
if content.has_top_level_media_type("image"):
a_content.append({
"type": "image",
- "source": {"data": content.uri, "media_type": content.media_type},
+ "source": {
+ "data": content.get_data_bytes_as_str(),
+ "media_type": content.media_type,
+ "type": "base64",
+ },
})
+ else:
+ logger.debug(f"Ignoring unsupported data content media type: {content.media_type} for now")
case "uri":
if content.has_top_level_media_type("image"):
a_content.append({"type": "image", "source": {"type": "url", "url": content.uri}})
+ else:
+ logger.debug(f"Ignoring unsupported data content media type: {content.media_type} for now")
case "function_call":
a_content.append({
"type": "tool_use",
diff --git a/python/packages/anthropic/pyproject.toml b/python/packages/anthropic/pyproject.toml
index cacd760e764..8f52f43a1f8 100644
--- a/python/packages/anthropic/pyproject.toml
+++ b/python/packages/anthropic/pyproject.toml
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
diff --git a/python/packages/anthropic/tests/assets/sample_image.jpg b/python/packages/anthropic/tests/assets/sample_image.jpg
new file mode 100644
index 00000000000..ea6486656fd
Binary files /dev/null and b/python/packages/anthropic/tests/assets/sample_image.jpg differ
diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py
index deff5195941..677cc1e166d 100644
--- a/python/packages/anthropic/tests/test_anthropic_client.py
+++ b/python/packages/anthropic/tests/test_anthropic_client.py
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
import os
+from pathlib import Path
from typing import Annotated
from unittest.mock import MagicMock, patch
@@ -9,6 +10,7 @@
ChatMessage,
ChatOptions,
ChatResponseUpdate,
+ DataContent,
FinishReason,
FunctionCallContent,
FunctionResultContent,
@@ -775,3 +777,31 @@ async def test_anthropic_client_integration_ordering() -> None:
assert response is not None
assert response.messages[0].text is not None
+
+
+@pytest.mark.flaky
+@skip_if_anthropic_integration_tests_disabled
+async def test_anthropic_client_integration_images() -> None:
+ """Integration test with images."""
+ client = AnthropicClient()
+
+ # get a image from the assets folder
+ image_path = Path(__file__).parent / "assets" / "sample_image.jpg"
+ with open(image_path, "rb") as img_file: # noqa [ASYNC230]
+ image_bytes = img_file.read()
+
+ messages = [
+ ChatMessage(
+ role=Role.USER,
+ contents=[
+ TextContent(text="Describe this image"),
+ DataContent(media_type="image/jpeg", data=image_bytes),
+ ],
+ ),
+ ]
+
+ response = await client.get_response(messages=messages)
+
+ assert response is not None
+ assert response.messages[0].text is not None
+ assert "house" in response.messages[0].text.lower()
diff --git a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py
index 6e6ac7a5e5a..cf2423693d7 100644
--- a/python/packages/azure-ai/agent_framework_azure_ai/__init__.py
+++ b/python/packages/azure-ai/agent_framework_azure_ai/__init__.py
@@ -3,6 +3,7 @@
import importlib.metadata
from ._chat_client import AzureAIAgentClient
+from ._client import AzureAIClient
from ._shared import AzureAISettings
try:
@@ -12,6 +13,7 @@
__all__ = [
"AzureAIAgentClient",
+ "AzureAIClient",
"AzureAISettings",
"__version__",
]
diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py
new file mode 100644
index 00000000000..774349a85d3
--- /dev/null
+++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py
@@ -0,0 +1,354 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+import sys
+from collections.abc import MutableSequence
+from typing import Any, ClassVar, TypeVar
+
+from agent_framework import (
+ AGENT_FRAMEWORK_USER_AGENT,
+ ChatMessage,
+ ChatOptions,
+ HostedMCPTool,
+ TextContent,
+ get_logger,
+ use_chat_middleware,
+ use_function_invocation,
+)
+from agent_framework.exceptions import ServiceInitializationError
+from agent_framework.observability import use_observability
+from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
+from azure.ai.projects.aio import AIProjectClient
+from azure.ai.projects.models import (
+ MCPTool,
+ PromptAgentDefinition,
+ PromptAgentDefinitionText,
+ ResponseTextFormatConfigurationJsonSchema,
+)
+from azure.core.credentials_async import AsyncTokenCredential
+from azure.core.exceptions import ResourceNotFoundError
+from openai.types.responses.parsed_response import (
+ ParsedResponse,
+)
+from openai.types.responses.response import Response as OpenAIResponse
+from pydantic import BaseModel, ValidationError
+
+from ._shared import AzureAISettings
+
+if sys.version_info >= (3, 11):
+ from typing import Self # pragma: no cover
+else:
+ from typing_extensions import Self # pragma: no cover
+
+
+logger = get_logger("agent_framework.azure")
+
+
+TAzureAIClient = TypeVar("TAzureAIClient", bound="AzureAIClient")
+
+
+@use_function_invocation
+@use_observability
+@use_chat_middleware
+class AzureAIClient(OpenAIBaseResponsesClient):
+ """Azure AI Agent client."""
+
+ OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
+
+ def __init__(
+ self,
+ *,
+ project_client: AIProjectClient | None = None,
+ agent_name: str | None = None,
+ agent_version: str | None = None,
+ conversation_id: str | None = None,
+ project_endpoint: str | None = None,
+ model_deployment_name: str | None = None,
+ async_credential: AsyncTokenCredential | None = None,
+ use_latest_version: bool | None = None,
+ env_file_path: str | None = None,
+ env_file_encoding: str | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Initialize an Azure AI Agent client.
+
+ Keyword Args:
+ project_client: An existing AIProjectClient to use. If not provided, one will be created.
+ agent_name: The name to use when creating new agents.
+ agent_version: The version of the agent to use.
+ conversation_id: Default conversation ID to use for conversations. Can be overridden by
+ conversation_id property when making a request.
+ project_endpoint: The Azure AI Project endpoint URL.
+ Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
+ Ignored when a project_client is passed.
+ model_deployment_name: The model deployment name to use for agent creation.
+ Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
+ async_credential: Azure async credential to use for authentication.
+ use_latest_version: Boolean flag that indicates whether to use latest agent version
+ if it exists in the service.
+ env_file_path: Path to environment file for loading settings.
+ env_file_encoding: Encoding of the environment file.
+ kwargs: Additional keyword arguments passed to the parent class.
+
+ Examples:
+ .. code-block:: python
+
+ from agent_framework.azure import AzureAIClient
+ from azure.identity.aio import DefaultAzureCredential
+
+ # Using environment variables
+ # Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
+ # Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
+ credential = DefaultAzureCredential()
+ client = AzureAIClient(async_credential=credential)
+
+ # Or passing parameters directly
+ client = AzureAIClient(
+ project_endpoint="https://your-project.cognitiveservices.azure.com",
+ model_deployment_name="gpt-4",
+ async_credential=credential,
+ )
+
+ # Or loading from a .env file
+ client = AzureAIClient(async_credential=credential, env_file_path="path/to/.env")
+ """
+ try:
+ azure_ai_settings = AzureAISettings(
+ project_endpoint=project_endpoint,
+ model_deployment_name=model_deployment_name,
+ env_file_path=env_file_path,
+ env_file_encoding=env_file_encoding,
+ )
+ except ValidationError as ex:
+ raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex
+
+ # If no project_client is provided, create one
+ should_close_client = False
+ if project_client is None:
+ if not azure_ai_settings.project_endpoint:
+ raise ServiceInitializationError(
+ "Azure AI project endpoint is required. Set via 'project_endpoint' parameter "
+ "or 'AZURE_AI_PROJECT_ENDPOINT' environment variable."
+ )
+
+ # Use provided credential
+ if not async_credential:
+ raise ServiceInitializationError("Azure credential is required when project_client is not provided.")
+ project_client = AIProjectClient(
+ endpoint=azure_ai_settings.project_endpoint,
+ credential=async_credential,
+ user_agent=AGENT_FRAMEWORK_USER_AGENT,
+ )
+ should_close_client = True
+
+ # Initialize parent
+ super().__init__(
+ **kwargs,
+ )
+
+ # Initialize instance variables
+ self.agent_name = agent_name
+ self.agent_version = agent_version
+ self.use_latest_version = use_latest_version
+ self.project_client = project_client
+ self.credential = async_credential
+ self.model_id = azure_ai_settings.model_deployment_name
+ self.conversation_id = conversation_id
+ self._should_close_client = should_close_client # Track whether we should close client connection
+
+ async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
+ """Use this method to setup tracing in your Azure AI Project.
+
+ This will take the connection string from the project project_client.
+ It will override any connection string that is set in the environment variables.
+ It will disable any OTLP endpoint that might have been set.
+ """
+ try:
+ conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
+ except ResourceNotFoundError:
+ logger.warning(
+ "No Application Insights connection string found for the Azure AI Project, "
+ "please call setup_observability() manually."
+ )
+ return
+ from agent_framework.observability import setup_observability
+
+ setup_observability(
+ applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
+ )
+
+ async def __aenter__(self) -> "Self":
+ """Async context manager entry."""
+ return self
+
+ async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
+ """Async context manager exit."""
+ await self.close()
+
+ async def close(self) -> None:
+ """Close the project_client."""
+ await self._close_client_if_needed()
+
+ async def _get_agent_reference_or_create(
+ self, run_options: dict[str, Any], messages_instructions: str | None
+ ) -> dict[str, str]:
+ """Determine which agent to use and create if needed.
+
+ Returns:
+ str: The agent_name to use
+ """
+ agent_name = self.agent_name or "UnnamedAgent"
+
+ # If no agent_version is provided, either use latest version or create a new agent:
+ if self.agent_version is None:
+ # Try to use latest version if requested and agent exists
+ if self.use_latest_version:
+ try:
+ existing_agent = await self.project_client.agents.get(agent_name)
+ self.agent_name = existing_agent.name
+ self.agent_version = existing_agent.versions.latest.version
+ return {"name": self.agent_name, "version": self.agent_version, "type": "agent_reference"}
+ except ResourceNotFoundError:
+ # Agent doesn't exist, fall through to creation logic
+ pass
+
+ if "model" not in run_options or not run_options["model"]:
+ raise ServiceInitializationError(
+ "Model deployment name is required for agent creation, "
+ "can also be passed to the get_response methods."
+ )
+
+ args: dict[str, Any] = {"model": run_options["model"]}
+
+ if "tools" in run_options:
+ args["tools"] = run_options["tools"]
+
+ if "response_format" in run_options:
+ response_format = run_options["response_format"]
+ args["text"] = PromptAgentDefinitionText(
+ format=ResponseTextFormatConfigurationJsonSchema(
+ name=response_format.__name__,
+ schema=response_format.model_json_schema(),
+ )
+ )
+
+ # Combine instructions from messages and options
+ combined_instructions = [
+ instructions
+ for instructions in [messages_instructions, run_options.get("instructions")]
+ if instructions
+ ]
+ if combined_instructions:
+ args["instructions"] = "".join(combined_instructions)
+
+ created_agent = await self.project_client.agents.create_version(
+ agent_name=agent_name, definition=PromptAgentDefinition(**args)
+ )
+
+ self.agent_name = created_agent.name
+ self.agent_version = created_agent.version
+
+ return {"name": agent_name, "version": self.agent_version, "type": "agent_reference"}
+
+ async def _close_client_if_needed(self) -> None:
+ """Close project_client session if we created it."""
+ if self._should_close_client:
+ await self.project_client.close()
+
+ def _prepare_input(self, messages: MutableSequence[ChatMessage]) -> tuple[list[ChatMessage], str | None]:
+ """Prepare input from messages and convert system/developer messages to instructions."""
+ result: list[ChatMessage] = []
+ instructions_list: list[str] = []
+ instructions: str | None = None
+
+ # System/developer messages are turned into instructions, since there is no such message roles in Azure AI.
+ for message in messages:
+ if message.role.value in ["system", "developer"]:
+ for text_content in [content for content in message.contents if isinstance(content, TextContent)]:
+ instructions_list.append(text_content.text)
+ else:
+ result.append(message)
+
+ if len(instructions_list) > 0:
+ instructions = "".join(instructions_list)
+
+ return result, instructions
+
+ async def prepare_options(
+ self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions
+ ) -> dict[str, Any]:
+ chat_options.store = bool(chat_options.store or chat_options.store is None)
+ prepared_messages, instructions = self._prepare_input(messages)
+ run_options = await super().prepare_options(prepared_messages, chat_options)
+ agent_reference = await self._get_agent_reference_or_create(run_options, instructions)
+
+ run_options["extra_body"] = {"agent": agent_reference}
+
+ conversation_id = chat_options.conversation_id or self.conversation_id
+
+ # Handle different conversation ID formats
+ if conversation_id:
+ if conversation_id.startswith("resp_"):
+ # For response IDs, set previous_response_id and remove conversation property
+ run_options.pop("conversation", None)
+ run_options["previous_response_id"] = conversation_id
+ elif conversation_id.startswith("conv_"):
+ # For conversation IDs, set conversation and remove previous_response_id property
+ run_options.pop("previous_response_id", None)
+ run_options["conversation"] = conversation_id
+
+ # Remove properties that are not supported on request level
+ # but were configured on agent level
+ exclude = ["model", "tools", "response_format"]
+
+ for property in exclude:
+ run_options.pop(property, None)
+
+ return run_options
+
+ async def initialize_client(self) -> None:
+ """Initialize OpenAI client asynchronously."""
+ self.client = await self.project_client.get_openai_client() # type: ignore
+
+ def _update_agent_name(self, agent_name: str | None) -> None:
+ """Update the agent name in the chat client.
+
+ Args:
+ agent_name: The new name for the agent.
+ """
+ # This is a no-op in the base class, but can be overridden by subclasses
+ # to update the agent name in the client.
+ if agent_name and not self.agent_name:
+ self.agent_name = agent_name
+
+ def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
+ """Get MCP tool from HostedMCPTool."""
+ mcp = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url))
+
+ if tool.allowed_tools:
+ mcp["allowed_tools"] = list(tool.allowed_tools)
+
+ if tool.approval_mode:
+ match tool.approval_mode:
+ case str():
+ mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
+ case _:
+ if always_require_approvals := tool.approval_mode.get("always_require_approval"):
+ mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
+ if never_require_approvals := tool.approval_mode.get("never_require_approval"):
+ mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
+
+ return mcp
+
+ def get_conversation_id(
+ self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
+ ) -> str | None:
+ """Get the conversation ID from the response if store is True."""
+ if store:
+ # If conversation ID exists, it means that we operate with conversation
+ # so we use conversation ID as input and output.
+ if response.conversation and response.conversation.id:
+ return response.conversation.id
+ # If conversation ID doesn't exist, we operate with responses
+ # so we use response ID as input and output.
+ return response.id
+ return None
diff --git a/python/packages/azure-ai/pyproject.toml b/python/packages/azure-ai/pyproject.toml
index fa15e4c0745..1b2ff49ed94 100644
--- a/python/packages/azure-ai/pyproject.toml
+++ b/python/packages/azure-ai/pyproject.toml
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core",
- "azure-ai-projects >= 1.0.0b11",
+ "azure-ai-projects >= 2.0.0b1",
"azure-ai-agents == 1.2.0b5",
"aiohttp",
]
diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py
new file mode 100644
index 00000000000..576218f2706
--- /dev/null
+++ b/python/packages/azure-ai/tests/test_azure_ai_client.py
@@ -0,0 +1,743 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from agent_framework import (
+ ChatClientProtocol,
+ ChatMessage,
+ ChatOptions,
+ Role,
+ TextContent,
+)
+from agent_framework.exceptions import ServiceInitializationError
+from azure.ai.projects.models import (
+ ResponseTextFormatConfigurationJsonSchema,
+)
+from openai.types.responses.parsed_response import ParsedResponse
+from openai.types.responses.response import Response as OpenAIResponse
+from pydantic import BaseModel, ConfigDict, ValidationError
+
+from agent_framework_azure_ai import AzureAIClient, AzureAISettings
+
+
+def create_test_azure_ai_client(
+ mock_project_client: MagicMock,
+ agent_name: str | None = None,
+ agent_version: str | None = None,
+ conversation_id: str | None = None,
+ azure_ai_settings: AzureAISettings | None = None,
+ should_close_client: bool = False,
+ use_latest_version: bool | None = None,
+) -> AzureAIClient:
+ """Helper function to create AzureAIClient instances for testing, bypassing normal validation."""
+ if azure_ai_settings is None:
+ azure_ai_settings = AzureAISettings(env_file_path="test.env")
+
+ # Create client instance directly
+ client = object.__new__(AzureAIClient)
+
+ # Set attributes directly
+ client.project_client = mock_project_client
+ client.credential = None
+ client.agent_name = agent_name
+ client.agent_version = agent_version
+ client.use_latest_version = use_latest_version
+ client.model_id = azure_ai_settings.model_deployment_name
+ client.conversation_id = conversation_id
+ client._should_close_client = should_close_client # type: ignore
+ client.additional_properties = {}
+ client.middleware = None
+
+ # Mock the OpenAI client attribute
+ mock_openai_client = MagicMock()
+ mock_openai_client.conversations = MagicMock()
+ mock_openai_client.conversations.create = AsyncMock()
+ client.client = mock_openai_client
+
+ return client
+
+
+def test_azure_ai_settings_init(azure_ai_unit_test_env: dict[str, str]) -> None:
+ """Test AzureAISettings initialization."""
+ settings = AzureAISettings()
+
+ assert settings.project_endpoint == azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"]
+ assert settings.model_deployment_name == azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
+
+
+def test_azure_ai_settings_init_with_explicit_values() -> None:
+ """Test AzureAISettings initialization with explicit values."""
+ settings = AzureAISettings(
+ project_endpoint="https://custom-endpoint.com/",
+ model_deployment_name="custom-model",
+ )
+
+ assert settings.project_endpoint == "https://custom-endpoint.com/"
+ assert settings.model_deployment_name == "custom-model"
+
+
+def test_azure_ai_client_init_with_project_client(mock_project_client: MagicMock) -> None:
+ """Test AzureAIClient initialization with existing project_client."""
+ with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
+ mock_settings.return_value.project_endpoint = None
+ mock_settings.return_value.model_deployment_name = "test-model"
+
+ client = AzureAIClient(
+ project_client=mock_project_client,
+ agent_name="test-agent",
+ agent_version="1.0",
+ )
+
+ assert client.project_client is mock_project_client
+ assert client.agent_name == "test-agent"
+ assert client.agent_version == "1.0"
+ assert not client._should_close_client # type: ignore
+ assert isinstance(client, ChatClientProtocol)
+
+
+def test_azure_ai_client_init_auto_create_client(
+ azure_ai_unit_test_env: dict[str, str],
+ mock_azure_credential: MagicMock,
+) -> None:
+ """Test AzureAIClient initialization with auto-created project_client."""
+ with patch("agent_framework_azure_ai._client.AIProjectClient") as mock_ai_project_client:
+ mock_project_client = MagicMock()
+ mock_ai_project_client.return_value = mock_project_client
+
+ client = AzureAIClient(
+ project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
+ model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ async_credential=mock_azure_credential,
+ agent_name="test-agent",
+ )
+
+ assert client.project_client is mock_project_client
+ assert client.agent_name == "test-agent"
+ assert client._should_close_client # type: ignore
+
+ # Verify AIProjectClient was called with correct parameters
+ mock_ai_project_client.assert_called_once()
+
+
+def test_azure_ai_client_init_missing_project_endpoint() -> None:
+ """Test AzureAIClient initialization when project_endpoint is missing and no project_client provided."""
+ with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
+ mock_settings.return_value.project_endpoint = None
+ mock_settings.return_value.model_deployment_name = "test-model"
+
+ with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"):
+ AzureAIClient(async_credential=MagicMock())
+
+
+def test_azure_ai_client_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None:
+ """Test AzureAIClient.__init__ when async_credential is missing and no project_client provided."""
+ with pytest.raises(
+ ServiceInitializationError, match="Azure credential is required when project_client is not provided"
+ ):
+ AzureAIClient(
+ project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
+ model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
+ )
+
+
+def test_azure_ai_client_init_validation_error(mock_azure_credential: MagicMock) -> None:
+ """Test that ValidationError in AzureAISettings is properly handled."""
+ with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
+ mock_settings.side_effect = ValidationError.from_exception_data("test", [])
+
+ with pytest.raises(ServiceInitializationError, match="Failed to create Azure AI settings"):
+ AzureAIClient(async_credential=mock_azure_credential)
+
+
+async def test_azure_ai_client_get_agent_reference_or_create_existing_version(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _get_agent_reference_or_create when agent_version is already provided."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", agent_version="1.0")
+
+ agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
+
+ assert agent_ref == {"name": "existing-agent", "version": "1.0", "type": "agent_reference"}
+
+
+async def test_azure_ai_client_get_agent_reference_or_create_new_agent(
+ mock_project_client: MagicMock,
+ azure_ai_unit_test_env: dict[str, str],
+) -> None:
+ """Test _get_agent_reference_or_create when creating a new agent."""
+ azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
+ client = create_test_azure_ai_client(
+ mock_project_client, agent_name="new-agent", azure_ai_settings=azure_ai_settings
+ )
+
+ # Mock agent creation response
+ mock_agent = MagicMock()
+ mock_agent.name = "new-agent"
+ mock_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
+
+ run_options = {"model": azure_ai_settings.model_deployment_name}
+ agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ assert agent_ref == {"name": "new-agent", "version": "1.0", "type": "agent_reference"}
+ assert client.agent_name == "new-agent"
+ assert client.agent_version == "1.0"
+
+
+async def test_azure_ai_client_get_agent_reference_missing_model(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _get_agent_reference_or_create when model is missing for agent creation."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
+
+ with pytest.raises(ServiceInitializationError, match="Model deployment name is required for agent creation"):
+ await client._get_agent_reference_or_create({}, None) # type: ignore
+
+
+async def test_azure_ai_client_prepare_input_with_system_messages(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _prepare_input converts system/developer messages to instructions."""
+ client = create_test_azure_ai_client(mock_project_client)
+
+ messages = [
+ ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are a helpful assistant.")]),
+ ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
+ ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="System response")]),
+ ]
+
+ result_messages, instructions = client._prepare_input(messages) # type: ignore
+
+ assert len(result_messages) == 2
+ assert result_messages[0].role == Role.USER
+ assert result_messages[1].role == Role.ASSISTANT
+ assert instructions == "You are a helpful assistant."
+
+
+async def test_azure_ai_client_prepare_input_no_system_messages(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _prepare_input with no system/developer messages."""
+ client = create_test_azure_ai_client(mock_project_client)
+
+ messages = [
+ ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
+ ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Hi there!")]),
+ ]
+
+ result_messages, instructions = client._prepare_input(messages) # type: ignore
+
+ assert len(result_messages) == 2
+ assert instructions is None
+
+
+async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicMock) -> None:
+ """Test prepare_options basic functionality."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
+
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
+ chat_options = ChatOptions()
+
+ with (
+ patch.object(client.__class__.__bases__[0], "prepare_options", return_value={"model": "test-model"}),
+ patch.object(
+ client,
+ "_get_agent_reference_or_create",
+ return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
+ ),
+ ):
+ run_options = await client.prepare_options(messages, chat_options)
+
+ assert "extra_body" in run_options
+ assert run_options["extra_body"]["agent"]["name"] == "test-agent"
+
+
+async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock) -> None:
+ """Test initialize_client method."""
+ client = create_test_azure_ai_client(mock_project_client)
+
+ mock_openai_client = MagicMock()
+ mock_project_client.get_openai_client = AsyncMock(return_value=mock_openai_client)
+
+ await client.initialize_client()
+
+ assert client.client is mock_openai_client
+ mock_project_client.get_openai_client.assert_called_once()
+
+
+def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None:
+ """Test _update_agent_name method."""
+ client = create_test_azure_ai_client(mock_project_client)
+
+ # Test updating agent name when current is None
+ with patch.object(client, "_update_agent_name") as mock_update:
+ mock_update.return_value = None
+ client._update_agent_name("new-agent") # type: ignore
+ mock_update.assert_called_once_with("new-agent")
+
+ # Test behavior when agent name is updated
+ assert client.agent_name is None # Should remain None since we didn't actually update
+ client.agent_name = "test-agent" # Manually set for the test
+
+ # Test with None input
+ with patch.object(client, "_update_agent_name") as mock_update:
+ mock_update.return_value = None
+ client._update_agent_name(None) # type: ignore
+ mock_update.assert_called_once_with(None)
+
+
+async def test_azure_ai_client_async_context_manager(mock_project_client: MagicMock) -> None:
+ """Test async context manager functionality."""
+ client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
+
+ mock_project_client.close = AsyncMock()
+
+ async with client as ctx_client:
+ assert ctx_client is client
+
+ # Should call close after exiting context
+ mock_project_client.close.assert_called_once()
+
+
+async def test_azure_ai_client_close_method(mock_project_client: MagicMock) -> None:
+ """Test close method."""
+ client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
+
+ mock_project_client.close = AsyncMock()
+
+ await client.close()
+
+ mock_project_client.close.assert_called_once()
+
+
+async def test_azure_ai_client_close_client_when_should_close_false(mock_project_client: MagicMock) -> None:
+ """Test _close_client_if_needed when should_close_client is False."""
+ client = create_test_azure_ai_client(mock_project_client, should_close_client=False)
+
+ mock_project_client.close = AsyncMock()
+
+ await client._close_client_if_needed() # type: ignore
+
+ # Should not call close when should_close_client is False
+ mock_project_client.close.assert_not_called()
+
+
+async def test_azure_ai_client_agent_creation_with_instructions(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test agent creation with combined instructions."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
+
+ # Mock agent creation response
+ mock_agent = MagicMock()
+ mock_agent.name = "test-agent"
+ mock_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
+
+ run_options = {"model": "test-model", "instructions": "Option instructions. "}
+ messages_instructions = "Message instructions. "
+
+ await client._get_agent_reference_or_create(run_options, messages_instructions) # type: ignore
+
+ # Verify agent was created with combined instructions
+ call_args = mock_project_client.agents.create_version.call_args
+ assert call_args[1]["definition"].instructions == "Message instructions. Option instructions. "
+
+
+async def test_azure_ai_client_agent_creation_with_tools(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test agent creation with tools."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
+
+ # Mock agent creation response
+ mock_agent = MagicMock()
+ mock_agent.name = "test-agent"
+ mock_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
+
+ test_tools = [{"type": "function", "function": {"name": "test_tool"}}]
+ run_options = {"model": "test-model", "tools": test_tools}
+
+ await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ # Verify agent was created with tools
+ call_args = mock_project_client.agents.create_version.call_args
+ assert call_args[1]["definition"].tools == test_tools
+
+
+async def test_azure_ai_client_use_latest_version_existing_agent(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _get_agent_reference_or_create when use_latest_version=True and agent exists."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", use_latest_version=True)
+
+ # Mock existing agent response
+ mock_existing_agent = MagicMock()
+ mock_existing_agent.name = "existing-agent"
+ mock_existing_agent.versions.latest.version = "2.5"
+ mock_project_client.agents.get = AsyncMock(return_value=mock_existing_agent)
+
+ run_options = {"model": "test-model"}
+ agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ # Verify existing agent was retrieved and used
+ mock_project_client.agents.get.assert_called_once_with("existing-agent")
+ mock_project_client.agents.create_version.assert_not_called()
+
+ assert agent_ref == {"name": "existing-agent", "version": "2.5", "type": "agent_reference"}
+ assert client.agent_name == "existing-agent"
+ assert client.agent_version == "2.5"
+
+
+async def test_azure_ai_client_use_latest_version_agent_not_found(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _get_agent_reference_or_create when use_latest_version=True but agent doesn't exist."""
+ from azure.core.exceptions import ResourceNotFoundError
+
+ client = create_test_azure_ai_client(mock_project_client, agent_name="non-existing-agent", use_latest_version=True)
+
+ # Mock ResourceNotFoundError when trying to retrieve agent
+ mock_project_client.agents.get = AsyncMock(side_effect=ResourceNotFoundError("Agent not found"))
+
+ # Mock agent creation response for fallback
+ mock_created_agent = MagicMock()
+ mock_created_agent.name = "non-existing-agent"
+ mock_created_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
+
+ run_options = {"model": "test-model"}
+ agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ # Verify retrieval was attempted and creation was used as fallback
+ mock_project_client.agents.get.assert_called_once_with("non-existing-agent")
+ mock_project_client.agents.create_version.assert_called_once()
+
+ assert agent_ref == {"name": "non-existing-agent", "version": "1.0", "type": "agent_reference"}
+ assert client.agent_name == "non-existing-agent"
+ assert client.agent_version == "1.0"
+
+
+async def test_azure_ai_client_use_latest_version_false(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test _get_agent_reference_or_create when use_latest_version=False (default behavior)."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", use_latest_version=False)
+
+ # Mock agent creation response
+ mock_created_agent = MagicMock()
+ mock_created_agent.name = "test-agent"
+ mock_created_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
+
+ run_options = {"model": "test-model"}
+ agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ # Verify retrieval was not attempted and creation was used directly
+ mock_project_client.agents.get.assert_not_called()
+ mock_project_client.agents.create_version.assert_called_once()
+
+ assert agent_ref == {"name": "test-agent", "version": "1.0", "type": "agent_reference"}
+
+
+async def test_azure_ai_client_use_latest_version_with_existing_agent_version(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test that use_latest_version is ignored when agent_version is already provided."""
+ client = create_test_azure_ai_client(
+ mock_project_client, agent_name="test-agent", agent_version="3.0", use_latest_version=True
+ )
+
+ agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
+
+ # Verify neither retrieval nor creation was attempted since version is already set
+ mock_project_client.agents.get.assert_not_called()
+ mock_project_client.agents.create_version.assert_not_called()
+
+ assert agent_ref == {"name": "test-agent", "version": "3.0", "type": "agent_reference"}
+
+
+class ResponseFormatModel(BaseModel):
+ """Test Pydantic model for response format testing."""
+
+ name: str
+ value: int
+ description: str
+ model_config = ConfigDict(extra="forbid")
+
+
+async def test_azure_ai_client_agent_creation_with_response_format(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test agent creation with response_format configuration."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
+
+ # Mock agent creation response
+ mock_agent = MagicMock()
+ mock_agent.name = "test-agent"
+ mock_agent.version = "1.0"
+ mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
+
+ run_options = {"model": "test-model", "response_format": ResponseFormatModel}
+
+ await client._get_agent_reference_or_create(run_options, None) # type: ignore
+
+ # Verify agent was created with response format configuration
+ call_args = mock_project_client.agents.create_version.call_args
+ created_definition = call_args[1]["definition"]
+
+ # Check that text format configuration was set
+ assert hasattr(created_definition, "text")
+ assert created_definition.text is not None
+
+ # Check that the format is a ResponseTextFormatConfigurationJsonSchema
+ assert hasattr(created_definition.text, "format")
+ format_config = created_definition.text.format
+ assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
+
+ # Check the schema name matches the model class name
+ assert format_config.name == "ResponseFormatModel"
+
+ # Check that schema was generated correctly
+ assert format_config.schema is not None
+ schema = format_config.schema
+ assert "properties" in schema
+ assert "name" in schema["properties"]
+ assert "value" in schema["properties"]
+ assert "description" in schema["properties"]
+
+
+async def test_azure_ai_client_prepare_options_excludes_response_format(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test that prepare_options excludes response_format from final run options."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
+
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
+ chat_options = ChatOptions()
+
+ with (
+ patch.object(
+ client.__class__.__bases__[0],
+ "prepare_options",
+ return_value={"model": "test-model", "response_format": ResponseFormatModel},
+ ),
+ patch.object(
+ client,
+ "_get_agent_reference_or_create",
+ return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
+ ),
+ ):
+ run_options = await client.prepare_options(messages, chat_options)
+
+ # response_format should be excluded from final run options
+ assert "response_format" not in run_options
+ # But extra_body should contain agent reference
+ assert "extra_body" in run_options
+ assert run_options["extra_body"]["agent"]["name"] == "test-agent"
+
+
+async def test_azure_ai_client_prepare_options_with_resp_conversation_id(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test prepare_options with conversation ID starting with 'resp_'."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
+
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
+ chat_options = ChatOptions(conversation_id="resp_12345")
+
+ with (
+ patch.object(
+ client.__class__.__bases__[0],
+ "prepare_options",
+ return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
+ ),
+ patch.object(
+ client,
+ "_get_agent_reference_or_create",
+ return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
+ ),
+ ):
+ run_options = await client.prepare_options(messages, chat_options)
+
+ # Should set previous_response_id and remove conversation property
+ assert run_options["previous_response_id"] == "resp_12345"
+ assert "conversation" not in run_options
+
+
+async def test_azure_ai_client_prepare_options_with_conv_conversation_id(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test prepare_options with conversation ID starting with 'conv_'."""
+ client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
+
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
+ chat_options = ChatOptions(conversation_id="conv_67890")
+
+ with (
+ patch.object(
+ client.__class__.__bases__[0],
+ "prepare_options",
+ return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
+ ),
+ patch.object(
+ client,
+ "_get_agent_reference_or_create",
+ return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
+ ),
+ ):
+ run_options = await client.prepare_options(messages, chat_options)
+
+ # Should set conversation and remove previous_response_id property
+ assert run_options["conversation"] == "conv_67890"
+ assert "previous_response_id" not in run_options
+
+
+async def test_azure_ai_client_prepare_options_with_client_conversation_id(
+ mock_project_client: MagicMock,
+) -> None:
+ """Test prepare_options using client's default conversation ID when chat options don't have one."""
+ client = create_test_azure_ai_client(
+ mock_project_client, agent_name="test-agent", agent_version="1.0", conversation_id="resp_client_default"
+ )
+
+ messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
+ chat_options = ChatOptions() # No conversation_id specified
+
+ with (
+ patch.object(
+ client.__class__.__bases__[0],
+ "prepare_options",
+ return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
+ ),
+ patch.object(
+ client,
+ "_get_agent_reference_or_create",
+ return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
+ ),
+ ):
+ run_options = await client.prepare_options(messages, chat_options)
+
+ # Should use client's default conversation_id and set previous_response_id
+ assert run_options["previous_response_id"] == "resp_client_default"
+ assert "conversation" not in run_options
+
+
+def test_get_conversation_id_with_store_true_and_conversation_id() -> None:
+ """Test get_conversation_id returns conversation ID when store is True and conversation exists."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock OpenAI response with conversation
+ mock_response = MagicMock(spec=OpenAIResponse)
+ mock_response.id = "resp_12345"
+ mock_conversation = MagicMock()
+ mock_conversation.id = "conv_67890"
+ mock_response.conversation = mock_conversation
+
+ result = client.get_conversation_id(mock_response, store=True)
+
+ assert result == "conv_67890"
+
+
+def test_get_conversation_id_with_store_true_and_no_conversation() -> None:
+ """Test get_conversation_id returns response ID when store is True and no conversation exists."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock OpenAI response without conversation
+ mock_response = MagicMock(spec=OpenAIResponse)
+ mock_response.id = "resp_12345"
+ mock_response.conversation = None
+
+ result = client.get_conversation_id(mock_response, store=True)
+
+ assert result == "resp_12345"
+
+
+def test_get_conversation_id_with_store_true_and_empty_conversation_id() -> None:
+ """Test get_conversation_id returns response ID when store is True and conversation ID is empty."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock OpenAI response with conversation but empty ID
+ mock_response = MagicMock(spec=OpenAIResponse)
+ mock_response.id = "resp_12345"
+ mock_conversation = MagicMock()
+ mock_conversation.id = ""
+ mock_response.conversation = mock_conversation
+
+ result = client.get_conversation_id(mock_response, store=True)
+
+ assert result == "resp_12345"
+
+
+def test_get_conversation_id_with_store_false() -> None:
+ """Test get_conversation_id returns None when store is False."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock OpenAI response with conversation
+ mock_response = MagicMock(spec=OpenAIResponse)
+ mock_response.id = "resp_12345"
+ mock_conversation = MagicMock()
+ mock_conversation.id = "conv_67890"
+ mock_response.conversation = mock_conversation
+
+ result = client.get_conversation_id(mock_response, store=False)
+
+ assert result is None
+
+
+def test_get_conversation_id_with_parsed_response_and_store_true() -> None:
+ """Test get_conversation_id works with ParsedResponse when store is True."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock ParsedResponse with conversation
+ mock_response = MagicMock(spec=ParsedResponse[BaseModel])
+ mock_response.id = "resp_parsed_12345"
+ mock_conversation = MagicMock()
+ mock_conversation.id = "conv_parsed_67890"
+ mock_response.conversation = mock_conversation
+
+ result = client.get_conversation_id(mock_response, store=True)
+
+ assert result == "conv_parsed_67890"
+
+
+def test_get_conversation_id_with_parsed_response_no_conversation() -> None:
+ """Test get_conversation_id returns response ID with ParsedResponse when no conversation exists."""
+ client = create_test_azure_ai_client(MagicMock())
+
+ # Mock ParsedResponse without conversation
+ mock_response = MagicMock(spec=ParsedResponse[BaseModel])
+ mock_response.id = "resp_parsed_12345"
+ mock_response.conversation = None
+
+ result = client.get_conversation_id(mock_response, store=True)
+
+ assert result == "resp_parsed_12345"
+
+
+@pytest.fixture
+def mock_project_client() -> MagicMock:
+ """Fixture that provides a mock AIProjectClient."""
+ mock_client = MagicMock()
+
+ # Mock agents property
+ mock_client.agents = MagicMock()
+ mock_client.agents.create_version = AsyncMock()
+
+ # Mock conversations property
+ mock_client.conversations = MagicMock()
+ mock_client.conversations.create = AsyncMock()
+
+ # Mock telemetry property
+ mock_client.telemetry = MagicMock()
+ mock_client.telemetry.get_application_insights_connection_string = AsyncMock()
+
+ # Mock get_openai_client method
+ mock_client.get_openai_client = AsyncMock()
+
+ # Mock close method
+ mock_client.close = AsyncMock()
+
+ return mock_client
diff --git a/python/packages/chatkit/pyproject.toml b/python/packages/chatkit/pyproject.toml
index 8c0a5047e44..b4ecefa2a61 100644
--- a/python/packages/chatkit/pyproject.toml
+++ b/python/packages/chatkit/pyproject.toml
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
diff --git a/python/packages/copilotstudio/pyproject.toml b/python/packages/copilotstudio/pyproject.toml
index 9872355b4ec..bedaa1c6ce0 100644
--- a/python/packages/copilotstudio/pyproject.toml
+++ b/python/packages/copilotstudio/pyproject.toml
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
diff --git a/python/packages/core/agent_framework/_clients.py b/python/packages/core/agent_framework/_clients.py
index 116148b80f5..630e7f8709a 100644
--- a/python/packages/core/agent_framework/_clients.py
+++ b/python/packages/core/agent_framework/_clients.py
@@ -564,10 +564,6 @@ async def get_response(
# Validate that store is True when conversation_id is set
if chat_options.conversation_id is not None and chat_options.store is not True:
- logger.warning(
- "When conversation_id is set, store must be True for service-managed threads. "
- "Automatically setting store=True."
- )
chat_options.store = True
if chat_options.instructions:
@@ -663,10 +659,6 @@ async def get_streaming_response(
# Validate that store is True when conversation_id is set
if chat_options.conversation_id is not None and chat_options.store is not True:
- logger.warning(
- "When conversation_id is set, store must be True for service-managed threads. "
- "Automatically setting store=True."
- )
chat_options.store = True
if chat_options.instructions:
diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py
index 117c9efe525..6edd258e15b 100644
--- a/python/packages/core/agent_framework/_tools.py
+++ b/python/packages/core/agent_framework/_tools.py
@@ -1636,7 +1636,7 @@ async def function_invocation_wrapper(
# this runs in every but the first run
# we need to keep track of all function call messages
fcc_messages.extend(response.messages)
- if getattr(kwargs.get("chat_options"), "store", False):
+ if response.conversation_id is not None:
prepped_messages.clear()
prepped_messages.append(result_message)
else:
@@ -1839,7 +1839,7 @@ async def streaming_function_invocation_wrapper(
# this runs in every but the first run
# we need to keep track of all function call messages
fcc_messages.extend(response.messages)
- if getattr(kwargs.get("chat_options"), "store", False):
+ if response.conversation_id is not None:
prepped_messages.clear()
prepped_messages.append(result_message)
else:
diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py
index 8dc7c006559..34ade839bf5 100644
--- a/python/packages/core/agent_framework/_types.py
+++ b/python/packages/core/agent_framework/_types.py
@@ -897,6 +897,9 @@ def __iadd__(self, other: "TextReasoningContent") -> Self:
return self
+TDataContent = TypeVar("TDataContent", bound="DataContent")
+
+
class DataContent(BaseContent):
"""Represents binary data content with an associated media type (also known as a MIME type).
@@ -1079,8 +1082,8 @@ def detect_image_format_from_base64(image_base64: str) -> str:
except Exception:
return "png" # Fallback if decoding fails
- @classmethod
- def create_data_uri_from_base64(cls, image_base64: str) -> tuple[str, str]:
+ @staticmethod
+ def create_data_uri_from_base64(image_base64: str) -> tuple[str, str]:
"""Create a data URI and media type from base64 image data.
Args:
@@ -1089,11 +1092,31 @@ def create_data_uri_from_base64(cls, image_base64: str) -> tuple[str, str]:
Returns:
Tuple of (data_uri, media_type)
"""
- format_type = cls.detect_image_format_from_base64(image_base64)
+ format_type = DataContent.detect_image_format_from_base64(image_base64)
uri = f"data:image/{format_type};base64,{image_base64}"
media_type = f"image/{format_type}"
return uri, media_type
+ def get_data_bytes_as_str(self) -> str:
+ """Extracts and returns the base64-encoded data from the data URI.
+
+ Returns:
+ The binary data as str.
+ """
+ match = URI_PATTERN.match(self.uri)
+ if not match:
+ raise ValueError(f"Invalid data URI format: {self.uri}")
+ return match.group("base64_data")
+
+ def get_data_bytes(self) -> bytes:
+ """Extracts and returns the binary data from the data URI.
+
+ Returns:
+ The binary data as bytes.
+ """
+ base64_data = self.get_data_bytes_as_str()
+ return base64.b64decode(base64_data)
+
class UriContent(BaseContent):
"""Represents a URI content.
diff --git a/python/packages/core/agent_framework/azure/__init__.py b/python/packages/core/agent_framework/azure/__init__.py
index 5dfab603cb4..23b2085cbc4 100644
--- a/python/packages/core/agent_framework/azure/__init__.py
+++ b/python/packages/core/agent_framework/azure/__init__.py
@@ -6,6 +6,7 @@
_IMPORTS: dict[str, tuple[str, str]] = {
"AzureAIAgentClient": ("agent_framework_azure_ai", "azure-ai"),
+ "AzureAIClient": ("agent_framework_azure_ai", "azure-ai"),
"AzureOpenAIAssistantsClient": ("agent_framework.azure._assistants_client", "core"),
"AzureOpenAIChatClient": ("agent_framework.azure._chat_client", "core"),
"AzureAISettings": ("agent_framework_azure_ai", "azure-ai"),
diff --git a/python/packages/core/agent_framework/azure/__init__.pyi b/python/packages/core/agent_framework/azure/__init__.pyi
index 742325a7366..582c7a05bed 100644
--- a/python/packages/core/agent_framework/azure/__init__.pyi
+++ b/python/packages/core/agent_framework/azure/__init__.pyi
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
-from agent_framework_azure_ai import AzureAIAgentClient, AzureAISettings
+from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings
from agent_framework.azure._assistants_client import AzureOpenAIAssistantsClient
from agent_framework.azure._chat_client import AzureOpenAIChatClient
@@ -10,6 +10,7 @@ from agent_framework.azure._shared import AzureOpenAISettings
__all__ = [
"AzureAIAgentClient",
+ "AzureAIClient",
"AzureAISettings",
"AzureOpenAIAssistantsClient",
"AzureOpenAIChatClient",
diff --git a/python/packages/core/agent_framework/openai/_assistants_client.py b/python/packages/core/agent_framework/openai/_assistants_client.py
index 8a28075e624..6255a6b8db7 100644
--- a/python/packages/core/agent_framework/openai/_assistants_client.py
+++ b/python/packages/core/agent_framework/openai/_assistants_client.py
@@ -161,7 +161,8 @@ async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseExc
async def close(self) -> None:
"""Clean up any assistants we created."""
if self._should_delete_assistant and self.assistant_id is not None:
- await self.client.beta.assistants.delete(self.assistant_id)
+ client = await self.ensure_client()
+ await client.beta.assistants.delete(self.assistant_id)
object.__setattr__(self, "assistant_id", None)
object.__setattr__(self, "_should_delete_assistant", False)
@@ -215,7 +216,11 @@ async def _get_assistant_id_or_create(self) -> str:
"""
# If no assistant is provided, create a temporary assistant
if self.assistant_id is None:
- created_assistant = await self.client.beta.assistants.create(name=self.assistant_name, model=self.model_id)
+ if not self.model_id:
+ raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.")
+
+ client = await self.ensure_client()
+ created_assistant = await client.beta.assistants.create(name=self.assistant_name, model=self.model_id)
self.assistant_id = created_assistant.id
self._should_delete_assistant = True
@@ -233,6 +238,7 @@ async def _create_assistant_stream(
Returns:
tuple: (stream, final_thread_id)
"""
+ client = await self.ensure_client()
# Get any active run for this thread
thread_run = await self._get_active_thread_run(thread_id)
@@ -240,7 +246,7 @@ async def _create_assistant_stream(
if thread_run is not None and tool_run_id is not None and tool_run_id == thread_run.id and tool_outputs:
# There's an active run and we have tool results to submit, so submit the results.
- stream = self.client.beta.threads.runs.submit_tool_outputs_stream( # type: ignore[reportDeprecated]
+ stream = client.beta.threads.runs.submit_tool_outputs_stream( # type: ignore[reportDeprecated]
run_id=tool_run_id, thread_id=thread_run.thread_id, tool_outputs=tool_outputs
)
final_thread_id = thread_run.thread_id
@@ -249,7 +255,7 @@ async def _create_assistant_stream(
final_thread_id = await self._prepare_thread(thread_id, thread_run, run_options)
# Now create a new run and stream the results.
- stream = self.client.beta.threads.runs.stream( # type: ignore[reportDeprecated]
+ stream = client.beta.threads.runs.stream( # type: ignore[reportDeprecated]
assistant_id=assistant_id, thread_id=final_thread_id, **run_options
)
@@ -257,19 +263,21 @@ async def _create_assistant_stream(
async def _get_active_thread_run(self, thread_id: str | None) -> Run | None:
"""Get any active run for the given thread."""
+ client = await self.ensure_client()
if thread_id is None:
return None
- async for run in self.client.beta.threads.runs.list(thread_id=thread_id, limit=1, order="desc"): # type: ignore[reportDeprecated]
+ async for run in client.beta.threads.runs.list(thread_id=thread_id, limit=1, order="desc"): # type: ignore[reportDeprecated]
if run.status not in ["completed", "cancelled", "failed", "expired"]:
return run
return None
async def _prepare_thread(self, thread_id: str | None, thread_run: Run | None, run_options: dict[str, Any]) -> str:
"""Prepare the thread for a new run, creating or cleaning up as needed."""
+ client = await self.ensure_client()
if thread_id is None:
# No thread ID was provided, so create a new thread.
- thread = await self.client.beta.threads.create( # type: ignore[reportDeprecated]
+ thread = await client.beta.threads.create( # type: ignore[reportDeprecated]
messages=run_options["additional_messages"],
tool_resources=run_options.get("tool_resources"),
metadata=run_options.get("metadata"),
@@ -280,7 +288,7 @@ async def _prepare_thread(self, thread_id: str | None, thread_run: Run | None, r
if thread_run is not None:
# There was an active run; we need to cancel it before starting a new run.
- await self.client.beta.threads.runs.cancel(run_id=thread_run.id, thread_id=thread_id) # type: ignore[reportDeprecated]
+ await client.beta.threads.runs.cancel(run_id=thread_run.id, thread_id=thread_id) # type: ignore[reportDeprecated]
return thread_id
diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py
index e6a4087508e..02e0743e1b2 100644
--- a/python/packages/core/agent_framework/openai/_chat_client.py
+++ b/python/packages/core/agent_framework/openai/_chat_client.py
@@ -69,10 +69,11 @@ async def _inner_get_response(
chat_options: ChatOptions,
**kwargs: Any,
) -> ChatResponse:
+ client = await self.ensure_client()
options_dict = self._prepare_options(messages, chat_options)
try:
return self._create_chat_response(
- await self.client.chat.completions.create(stream=False, **options_dict), chat_options
+ await client.chat.completions.create(stream=False, **options_dict), chat_options
)
except BadRequestError as ex:
if ex.code == "content_filter":
@@ -97,10 +98,11 @@ async def _inner_get_streaming_response(
chat_options: ChatOptions,
**kwargs: Any,
) -> AsyncIterable[ChatResponseUpdate]:
+ client = await self.ensure_client()
options_dict = self._prepare_options(messages, chat_options)
options_dict["stream_options"] = {"include_usage": True}
try:
- async for chunk in await self.client.chat.completions.create(stream=True, **options_dict):
+ async for chunk in await client.chat.completions.create(stream=True, **options_dict):
if len(chunk.choices) == 0 and chunk.usage is None:
continue
yield self._create_chat_response_update(chunk)
diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py
index 149fe4bfac4..447333447ab 100644
--- a/python/packages/core/agent_framework/openai/_responses_client.py
+++ b/python/packages/core/agent_framework/openai/_responses_client.py
@@ -89,23 +89,24 @@ async def _inner_get_response(
chat_options: ChatOptions,
**kwargs: Any,
) -> ChatResponse:
- options_dict = self._prepare_options(messages, chat_options)
+ client = await self.ensure_client()
+ run_options = await self.prepare_options(messages, chat_options)
try:
- if not chat_options.response_format:
- response = await self.client.responses.create(
+ response_format = run_options.pop("response_format", None)
+ if not response_format:
+ response = await client.responses.create(
stream=False,
- **options_dict,
+ **run_options,
)
- chat_options.conversation_id = response.id if chat_options.store is True else None
+ chat_options.conversation_id = self.get_conversation_id(response, chat_options.store)
return self._create_response_content(response, chat_options=chat_options)
# create call does not support response_format, so we need to handle it via parse call
- resp_format = chat_options.response_format
- parsed_response: ParsedResponse[BaseModel] = await self.client.responses.parse(
- text_format=resp_format,
+ parsed_response: ParsedResponse[BaseModel] = await client.responses.parse(
+ text_format=response_format,
stream=False,
- **options_dict,
+ **run_options,
)
- chat_options.conversation_id = parsed_response.id if chat_options.store is True else None
+ chat_options.conversation_id = self.get_conversation_id(parsed_response, chat_options.store)
return self._create_response_content(parsed_response, chat_options=chat_options)
except BadRequestError as ex:
if ex.code == "content_filter":
@@ -130,13 +131,15 @@ async def _inner_get_streaming_response(
chat_options: ChatOptions,
**kwargs: Any,
) -> AsyncIterable[ChatResponseUpdate]:
- options_dict = self._prepare_options(messages, chat_options)
+ client = await self.ensure_client()
+ run_options = await self.prepare_options(messages, chat_options)
function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name)
try:
- if not chat_options.response_format:
- response = await self.client.responses.create(
+ response_format = run_options.pop("response_format", None)
+ if not response_format:
+ response = await client.responses.create(
stream=True,
- **options_dict,
+ **run_options,
)
async for chunk in response:
update = self._create_streaming_response_content(
@@ -145,9 +148,9 @@ async def _inner_get_streaming_response(
yield update
return
# create call does not support response_format, so we need to handle it via stream call
- async with self.client.responses.stream(
- text_format=chat_options.response_format,
- **options_dict,
+ async with client.responses.stream(
+ text_format=response_format,
+ **run_options,
) as response:
async for chunk in response:
update = self._create_streaming_response_content(
@@ -170,6 +173,12 @@ async def _inner_get_streaming_response(
inner_exception=ex,
) from ex
+ def get_conversation_id(
+ self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
+ ) -> str | None:
+ """Get the conversation ID from the response if store is True."""
+ return response.id if store else None
+
# region Prep methods
def _tools_to_response_tools(
@@ -180,31 +189,7 @@ def _tools_to_response_tools(
if isinstance(tool, ToolProtocol):
match tool:
case HostedMCPTool():
- mcp: Mcp = {
- "type": "mcp",
- "server_label": tool.name.replace(" ", "_"),
- "server_url": str(tool.url),
- "server_description": tool.description,
- "headers": tool.headers,
- }
- if tool.allowed_tools:
- mcp["allowed_tools"] = list(tool.allowed_tools)
- if tool.approval_mode:
- match tool.approval_mode:
- case str():
- mcp["require_approval"] = (
- "always" if tool.approval_mode == "always_require" else "never"
- )
- case _:
- if always_require_approvals := tool.approval_mode.get("always_require_approval"):
- mcp["require_approval"] = {
- "always": {"tool_names": list(always_require_approvals)}
- }
- if never_require_approvals := tool.approval_mode.get("never_require_approval"):
- mcp["require_approval"] = {
- "never": {"tool_names": list(never_require_approvals)}
- }
- response_tools.append(mcp)
+ response_tools.append(self.get_mcp_tool(tool))
case HostedCodeInterpreterTool():
tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"}
if tool.inputs:
@@ -306,12 +291,36 @@ def _tools_to_response_tools(
response_tools.append(tool_dict)
return response_tools
- def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
+ def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
+ """Get MCP tool from HostedMCPTool."""
+ mcp: Mcp = {
+ "type": "mcp",
+ "server_label": tool.name.replace(" ", "_"),
+ "server_url": str(tool.url),
+ "server_description": tool.description,
+ "headers": tool.headers,
+ }
+ if tool.allowed_tools:
+ mcp["allowed_tools"] = list(tool.allowed_tools)
+ if tool.approval_mode:
+ match tool.approval_mode:
+ case str():
+ mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
+ case _:
+ if always_require_approvals := tool.approval_mode.get("always_require_approval"):
+ mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
+ if never_require_approvals := tool.approval_mode.get("never_require_approval"):
+ mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
+
+ return mcp
+
+ async def prepare_options(
+ self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions
+ ) -> dict[str, Any]:
"""Take ChatOptions and create the specific options for Responses API."""
- options_dict: dict[str, Any] = chat_options.to_dict(
+ run_options: dict[str, Any] = chat_options.to_dict(
exclude={
"type",
- "response_format", # handled in inner get methods
"presence_penalty", # not supported
"frequency_penalty", # not supported
"logit_bias", # not supported
@@ -320,6 +329,10 @@ def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options:
"instructions", # already added as system message
}
)
+
+ if chat_options.response_format:
+ run_options["response_format"] = chat_options.response_format
+
translations = {
"model_id": "model",
"allow_multiple_tool_calls": "parallel_tool_calls",
@@ -327,35 +340,37 @@ def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options:
"max_tokens": "max_output_tokens",
}
for old_key, new_key in translations.items():
- if old_key in options_dict and old_key != new_key:
- options_dict[new_key] = options_dict.pop(old_key)
+ if old_key in run_options and old_key != new_key:
+ run_options[new_key] = run_options.pop(old_key)
# tools
if chat_options.tools is None:
- options_dict.pop("parallel_tool_calls", None)
+ run_options.pop("parallel_tool_calls", None)
else:
- options_dict["tools"] = self._tools_to_response_tools(chat_options.tools)
+ run_options["tools"] = self._tools_to_response_tools(chat_options.tools)
# model id
- if not options_dict.get("model"):
- options_dict["model"] = self.model_id
+ if not run_options.get("model"):
+ if not self.model_id:
+ raise ValueError("model_id must be a non-empty string")
+ run_options["model"] = self.model_id
# messages
request_input = self._prepare_chat_messages_for_request(messages)
if not request_input:
raise ServiceInvalidRequestError("Messages are required for chat completions")
- options_dict["input"] = request_input
+ run_options["input"] = request_input
# additional provider specific settings
- if additional_properties := options_dict.pop("additional_properties", None):
+ if additional_properties := run_options.pop("additional_properties", None):
for key, value in additional_properties.items():
if value is not None:
- options_dict[key] = value
- if "store" not in options_dict:
- options_dict["store"] = False
- if (tool_choice := options_dict.get("tool_choice")) and len(tool_choice.keys()) == 1:
- options_dict["tool_choice"] = tool_choice["mode"]
- return options_dict
+ run_options[key] = value
+ if "store" not in run_options:
+ run_options["store"] = False
+ if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
+ run_options["tool_choice"] = tool_choice["mode"]
+ return run_options
def _prepare_chat_messages_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
"""Prepare the chat messages for a request.
@@ -504,7 +519,6 @@ def _openai_content_parser(
# call_id for the result needs to be the same as the call_id for the function call
args: dict[str, Any] = {
"call_id": content.call_id,
- "id": call_id_to_id.get(content.call_id),
"type": "function_call_output",
}
if content.result:
@@ -734,7 +748,7 @@ def _create_response_content(
"raw_representation": response,
}
if chat_options.store:
- args["conversation_id"] = response.id
+ args["conversation_id"] = self.get_conversation_id(response, chat_options.store)
if response.usage and (usage_details := self._usage_details_from_openai(response.usage)):
args["usage_details"] = usage_details
if structured_response:
@@ -834,7 +848,7 @@ def _create_streaming_response_content(
contents.append(TextReasoningContent(text=event.text, raw_representation=event))
metadata.update(self._get_metadata_from_response(event))
case "response.completed":
- conversation_id = event.response.id if chat_options.store is True else None
+ conversation_id = self.get_conversation_id(event.response, chat_options.store)
model = event.response.model
if event.response.usage:
usage = self._usage_details_from_openai(event.response.usage)
diff --git a/python/packages/core/agent_framework/openai/_shared.py b/python/packages/core/agent_framework/openai/_shared.py
index bea58786c87..20c719e09eb 100644
--- a/python/packages/core/agent_framework/openai/_shared.py
+++ b/python/packages/core/agent_framework/openai/_shared.py
@@ -127,18 +127,18 @@ class OpenAIBase(SerializationMixin):
INJECTABLE: ClassVar[set[str]] = {"client"}
- def __init__(self, *, client: AsyncOpenAI, model_id: str, **kwargs: Any) -> None:
+ def __init__(self, *, model_id: str | None = None, client: AsyncOpenAI | None = None, **kwargs: Any) -> None:
"""Initialize OpenAIBase.
Keyword Args:
client: The AsyncOpenAI client instance.
- model_id: The AI model ID to use (non-empty, whitespace stripped).
+ model_id: The AI model ID to use.
**kwargs: Additional keyword arguments.
"""
- if not model_id or not model_id.strip():
- raise ValueError("model_id must be a non-empty string")
self.client = client
- self.model_id = model_id.strip()
+ self.model_id = None
+ if model_id:
+ self.model_id = model_id.strip()
# Call super().__init__() to continue MRO chain (e.g., BaseChatClient)
# Extract known kwargs that belong to other base classes
@@ -162,6 +162,21 @@ def __init__(self, *, client: AsyncOpenAI, model_id: str, **kwargs: Any) -> None
for key, value in kwargs.items():
setattr(self, key, value)
+ async def initialize_client(self) -> None:
+ """Initialize OpenAI client asynchronously.
+
+ Override in subclasses to initialize the OpenAI client asynchronously.
+ """
+ pass
+
+ async def ensure_client(self) -> AsyncOpenAI:
+ """Ensure OpenAI client is initialized."""
+ await self.initialize_client()
+ if self.client is None:
+ raise ServiceInitializationError("OpenAI client is not initialized")
+
+ return self.client
+
def _get_api_key(
self, api_key: str | SecretStr | Callable[[], str | Awaitable[str]] | None
) -> str | Callable[[], str | Awaitable[str]] | None:
diff --git a/python/packages/core/pyproject.toml b/python/packages/core/pyproject.toml
index 0dc26386c2e..38aa9ba89bb 100644
--- a/python/packages/core/pyproject.toml
+++ b/python/packages/core/pyproject.toml
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
-version = "1.0.0b251111"
+version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py
index 5ff4bb3de3b..47009504395 100644
--- a/python/packages/core/tests/openai/test_openai_responses_client.py
+++ b/python/packages/core/tests/openai/test_openai_responses_client.py
@@ -1407,27 +1407,27 @@ def test_create_response_content_image_generation_fallback():
assert f"data:image/png;base64,{unrecognized_base64}" == content.uri
-def test_prepare_options_store_parameter_handling() -> None:
+async def test_prepare_options_store_parameter_handling() -> None:
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Test message")]
test_conversation_id = "test-conversation-123"
chat_options = ChatOptions(store=True, conversation_id=test_conversation_id)
- options = client._prepare_options(messages, chat_options) # type: ignore
+ options = await client.prepare_options(messages, chat_options)
assert options["store"] is True
assert options["previous_response_id"] == test_conversation_id
chat_options = ChatOptions(store=False, conversation_id="")
- options = client._prepare_options(messages, chat_options) # type: ignore
+ options = await client.prepare_options(messages, chat_options)
assert options["store"] is False
chat_options = ChatOptions(store=None, conversation_id=None)
- options = client._prepare_options(messages, chat_options) # type: ignore
+ options = await client.prepare_options(messages, chat_options)
assert options["store"] is False
assert "previous_response_id" not in options
chat_options = ChatOptions()
- options = client._prepare_options(messages, chat_options) # type: ignore
+ options = await client.prepare_options(messages, chat_options)
assert options["store"] is False
assert "previous_response_id" not in options
diff --git a/python/packages/devui/agent_framework_devui/ui/assets/index.js b/python/packages/devui/agent_framework_devui/ui/assets/index.js
index 3744c1e10dd..317e9e73493 100644
--- a/python/packages/devui/agent_framework_devui/ui/assets/index.js
+++ b/python/packages/devui/agent_framework_devui/ui/assets/index.js
@@ -14,7 +14,7 @@ function gE(e,n){for(var s=0;s>>1,T=A[P];if(0>>1;Pl(Z,$))rel(de,Z)?(A[P]=de,A[re]=$,P=re):(A[P]=Z,A[W]=$,P=W);else if(rel(de,$))A[P]=de,A[re]=$,P=re;else break e}}return I}function l(A,I){var $=A.sortIndex-I.sortIndex;return $!==0?$:A.id-I.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var d=Date,f=d.now();e.unstable_now=function(){return d.now()-f}}var m=[],p=[],g=1,v=null,y=3,b=!1,S=!1,N=!1,_=!1,E=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,j=typeof setImmediate<"u"?setImmediate:null;function k(A){for(var I=s(p);I!==null;){if(I.callback===null)o(p);else if(I.startTime<=A)o(p),I.sortIndex=I.expirationTime,n(m,I);else break;I=s(p)}}function R(A){if(N=!1,k(A),!S)if(s(m)!==null)S=!0,D||(D=!0,G());else{var I=s(p);I!==null&&V(R,I.startTime-A)}}var D=!1,z=-1,H=5,U=-1;function F(){return _?!0:!(e.unstable_now()-UA&&F());){var P=v.callback;if(typeof P=="function"){v.callback=null,y=v.priorityLevel;var T=P(v.expirationTime<=A);if(A=e.unstable_now(),typeof T=="function"){v.callback=T,k(A),I=!0;break t}v===s(m)&&o(m),k(A)}else o(m);v=s(m)}if(v!==null)I=!0;else{var B=s(p);B!==null&&V(R,B.startTime-A),I=!1}}break e}finally{v=null,y=$,b=!1}I=void 0}}finally{I?G():D=!1}}}var G;if(typeof j=="function")G=function(){j(K)};else if(typeof MessageChannel<"u"){var ne=new MessageChannel,L=ne.port2;ne.port1.onmessage=K,G=function(){L.postMessage(null)}}else G=function(){E(K,0)};function V(A,I){z=E(function(){A(e.unstable_now())},I)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(A){A.callback=null},e.unstable_forceFrameRate=function(A){0>A||125P?(A.sortIndex=$,n(p,A),s(m)===null&&A===s(p)&&(N?(M(z),z=-1):N=!0,V(R,$-P))):(A.sortIndex=T,n(m,A),S||b||(S=!0,D||(D=!0,G()))),A},e.unstable_shouldYield=F,e.unstable_wrapCallback=function(A){var I=y;return function(){var $=y;y=I;try{return A.apply(this,arguments)}finally{y=$}}}})(Vm)),Vm}var Wy;function wE(){return Wy||(Wy=1,Um.exports=bE()),Um.exports}var qm={exports:{}},Yt={};/**
+ */var Zy;function bE(){return Zy||(Zy=1,(function(e){function n(A,I){var $=A.length;A.push(I);e:for(;0<$;){var P=$-1>>>1,T=A[P];if(0>>1;Pl(Z,$))rel(de,Z)?(A[P]=de,A[re]=$,P=re):(A[P]=Z,A[W]=$,P=W);else if(rel(de,$))A[P]=de,A[re]=$,P=re;else break e}}return I}function l(A,I){var $=A.sortIndex-I.sortIndex;return $!==0?$:A.id-I.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var c=performance;e.unstable_now=function(){return c.now()}}else{var d=Date,f=d.now();e.unstable_now=function(){return d.now()-f}}var m=[],p=[],g=1,v=null,y=3,b=!1,S=!1,N=!1,j=!1,E=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,_=typeof setImmediate<"u"?setImmediate:null;function k(A){for(var I=s(p);I!==null;){if(I.callback===null)o(p);else if(I.startTime<=A)o(p),I.sortIndex=I.expirationTime,n(m,I);else break;I=s(p)}}function R(A){if(N=!1,k(A),!S)if(s(m)!==null)S=!0,D||(D=!0,G());else{var I=s(p);I!==null&&V(R,I.startTime-A)}}var D=!1,z=-1,H=5,U=-1;function F(){return j?!0:!(e.unstable_now()-UA&&F());){var P=v.callback;if(typeof P=="function"){v.callback=null,y=v.priorityLevel;var T=P(v.expirationTime<=A);if(A=e.unstable_now(),typeof T=="function"){v.callback=T,k(A),I=!0;break t}v===s(m)&&o(m),k(A)}else o(m);v=s(m)}if(v!==null)I=!0;else{var B=s(p);B!==null&&V(R,B.startTime-A),I=!1}}break e}finally{v=null,y=$,b=!1}I=void 0}}finally{I?G():D=!1}}}var G;if(typeof _=="function")G=function(){_(K)};else if(typeof MessageChannel<"u"){var ne=new MessageChannel,L=ne.port2;ne.port1.onmessage=K,G=function(){L.postMessage(null)}}else G=function(){E(K,0)};function V(A,I){z=E(function(){A(e.unstable_now())},I)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(A){A.callback=null},e.unstable_forceFrameRate=function(A){0>A||125P?(A.sortIndex=$,n(p,A),s(m)===null&&A===s(p)&&(N?(M(z),z=-1):N=!0,V(R,$-P))):(A.sortIndex=T,n(m,A),S||b||(S=!0,D||(D=!0,G()))),A},e.unstable_shouldYield=F,e.unstable_wrapCallback=function(A){var I=y;return function(){var $=y;y=I;try{return A.apply(this,arguments)}finally{y=$}}}})(Vm)),Vm}var Wy;function wE(){return Wy||(Wy=1,Um.exports=bE()),Um.exports}var qm={exports:{}},Yt={};/**
* @license React
* react-dom.production.js
*
@@ -38,15 +38,15 @@ function gE(e,n){for(var s=0;sT||(t.current=P[T],P[T]=null,T--)}function Z(t,r){T++,P[T]=t.current,t.current=r}var re=B(null),de=B(null),ge=B(null),J=B(null);function le(t,r){switch(Z(ge,r),Z(de,t),Z(re,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?vy(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=vy(r),t=by(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}W(re),Z(re,t)}function ve(){W(re),W(de),W(ge)}function Ne(t){t.memoizedState!==null&&Z(J,t);var r=re.current,i=by(r,t.type);r!==i&&(Z(de,t),Z(re,i))}function _e(t){de.current===t&&(W(re),W(de)),J.current===t&&(W(J),Ai._currentValue=$)}var be=Object.prototype.hasOwnProperty,Re=e.unstable_scheduleCallback,te=e.unstable_cancelCallback,Ee=e.unstable_shouldYield,Ve=e.unstable_requestPaint,Qe=e.unstable_now,It=e.unstable_getCurrentPriorityLevel,Zt=e.unstable_ImmediatePriority,ht=e.unstable_UserBlockingPriority,We=e.unstable_NormalPriority,dt=e.unstable_LowPriority,wn=e.unstable_IdlePriority,ae=e.log,ie=e.unstable_setDisableYieldValue,ue=null,me=null;function ye(t){if(typeof ae=="function"&&ie(t),me&&typeof me.setStrictMode=="function")try{me.setStrictMode(ue,t)}catch{}}var ce=Math.clz32?Math.clz32:Ke,Se=Math.log,De=Math.LN2;function Ke(t){return t>>>=0,t===0?32:31-(Se(t)/De|0)|0}var Ut=256,we=4194304;function He(t){var r=t&42;if(r!==0)return r;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function je(t,r,i){var u=t.pendingLanes;if(u===0)return 0;var h=0,x=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var O=u&134217727;return O!==0?(u=O&~x,u!==0?h=He(u):(C&=O,C!==0?h=He(C):i||(i=O&~t,i!==0&&(h=He(i))))):(O=u&~x,O!==0?h=He(O):C!==0?h=He(C):i||(i=u&~t,i!==0&&(h=He(i)))),h===0?0:r!==0&&r!==h&&(r&x)===0&&(x=h&-h,i=r&-r,x>=i||x===32&&(i&4194048)!==0)?r:h}function rt(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function ft(t,r){switch(t){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Vt(){var t=Ut;return Ut<<=1,(Ut&4194048)===0&&(Ut=256),t}function Fn(){var t=we;return we<<=1,(we&62914560)===0&&(we=4194304),t}function Ma(t){for(var r=[],i=0;31>i;i++)r.push(t);return r}function Ms(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function kd(t,r,i,u,h,x){var C=t.pendingLanes;t.pendingLanes=i,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=i,t.entangledLanes&=i,t.errorRecoveryDisabledLanes&=i,t.shellSuspendCounter=0;var O=t.entanglements,q=t.expirationTimes,ee=t.hiddenUpdates;for(i=C&~i;0T||(t.current=P[T],P[T]=null,T--)}function Z(t,r){T++,P[T]=t.current,t.current=r}var re=B(null),de=B(null),ge=B(null),J=B(null);function le(t,r){switch(Z(ge,r),Z(de,t),Z(re,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?vy(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=vy(r),t=by(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}W(re),Z(re,t)}function ve(){W(re),W(de),W(ge)}function Ne(t){t.memoizedState!==null&&Z(J,t);var r=re.current,i=by(r,t.type);r!==i&&(Z(de,t),Z(re,i))}function je(t){de.current===t&&(W(re),W(de)),J.current===t&&(W(J),Ai._currentValue=$)}var be=Object.prototype.hasOwnProperty,Re=e.unstable_scheduleCallback,te=e.unstable_cancelCallback,Ee=e.unstable_shouldYield,Ve=e.unstable_requestPaint,Qe=e.unstable_now,It=e.unstable_getCurrentPriorityLevel,Zt=e.unstable_ImmediatePriority,ht=e.unstable_UserBlockingPriority,We=e.unstable_NormalPriority,dt=e.unstable_LowPriority,wn=e.unstable_IdlePriority,ae=e.log,ie=e.unstable_setDisableYieldValue,ue=null,me=null;function ye(t){if(typeof ae=="function"&&ie(t),me&&typeof me.setStrictMode=="function")try{me.setStrictMode(ue,t)}catch{}}var ce=Math.clz32?Math.clz32:Ke,Se=Math.log,De=Math.LN2;function Ke(t){return t>>>=0,t===0?32:31-(Se(t)/De|0)|0}var Ut=256,we=4194304;function He(t){var r=t&42;if(r!==0)return r;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function _e(t,r,i){var u=t.pendingLanes;if(u===0)return 0;var h=0,x=t.suspendedLanes,C=t.pingedLanes;t=t.warmLanes;var O=u&134217727;return O!==0?(u=O&~x,u!==0?h=He(u):(C&=O,C!==0?h=He(C):i||(i=O&~t,i!==0&&(h=He(i))))):(O=u&~x,O!==0?h=He(O):C!==0?h=He(C):i||(i=u&~t,i!==0&&(h=He(i)))),h===0?0:r!==0&&r!==h&&(r&x)===0&&(x=h&-h,i=r&-r,x>=i||x===32&&(i&4194048)!==0)?r:h}function rt(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function ft(t,r){switch(t){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Vt(){var t=Ut;return Ut<<=1,(Ut&4194048)===0&&(Ut=256),t}function Fn(){var t=we;return we<<=1,(we&62914560)===0&&(we=4194304),t}function Ma(t){for(var r=[],i=0;31>i;i++)r.push(t);return r}function Ms(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function kd(t,r,i,u,h,x){var C=t.pendingLanes;t.pendingLanes=i,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=i,t.entangledLanes&=i,t.errorRecoveryDisabledLanes&=i,t.shellSuspendCounter=0;var O=t.entanglements,q=t.expirationTimes,ee=t.hiddenUpdates;for(i=C&~i;0)":-1h||q[u]!==ee[h]){var fe=`
`+q[u].replace(" at new "," at ");return t.displayName&&fe.includes("")&&(fe=fe.replace("",t.displayName)),fe}while(1<=u&&0<=h);break}}}finally{Ha=!1,Error.prepareStackTrace=i}return(i=t?t.displayName||t.name:"")?hr(i):""}function Od(t){switch(t.tag){case 26:case 27:case 5:return hr(t.type);case 16:return hr("Lazy");case 13:return hr("Suspense");case 19:return hr("SuspenseList");case 0:case 15:return $a(t.type,!1);case 11:return $a(t.type.render,!1);case 1:return $a(t.type,!0);case 31:return hr("Activity");default:return""}}function Rl(t){try{var r="";do r+=Od(t),t=t.return;while(t);return r}catch(i){return`
Error generating stack: `+i.message+`
-`+i.stack}}function en(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function Dl(t){var r=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(r==="checkbox"||r==="radio")}function zd(t){var r=Dl(t)?"checked":"value",i=Object.getOwnPropertyDescriptor(t.constructor.prototype,r),u=""+t[r];if(!t.hasOwnProperty(r)&&typeof i<"u"&&typeof i.get=="function"&&typeof i.set=="function"){var h=i.get,x=i.set;return Object.defineProperty(t,r,{configurable:!0,get:function(){return h.call(this)},set:function(C){u=""+C,x.call(this,C)}}),Object.defineProperty(t,r,{enumerable:i.enumerable}),{getValue:function(){return u},setValue:function(C){u=""+C},stopTracking:function(){t._valueTracker=null,delete t[r]}}}}function vo(t){t._valueTracker||(t._valueTracker=zd(t))}function Ba(t){if(!t)return!1;var r=t._valueTracker;if(!r)return!0;var i=r.getValue(),u="";return t&&(u=Dl(t)?t.checked?"true":"false":t.value),t=u,t!==i?(r.setValue(t),!0):!1}function bo(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Id=/[\n"\\]/g;function tn(t){return t.replace(Id,function(r){return"\\"+r.charCodeAt(0).toString(16)+" "})}function Rs(t,r,i,u,h,x,C,O){t.name="",C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?t.type=C:t.removeAttribute("type"),r!=null?C==="number"?(r===0&&t.value===""||t.value!=r)&&(t.value=""+en(r)):t.value!==""+en(r)&&(t.value=""+en(r)):C!=="submit"&&C!=="reset"||t.removeAttribute("value"),r!=null?Pa(t,C,en(r)):i!=null?Pa(t,C,en(i)):u!=null&&t.removeAttribute("value"),h==null&&x!=null&&(t.defaultChecked=!!x),h!=null&&(t.checked=h&&typeof h!="function"&&typeof h!="symbol"),O!=null&&typeof O!="function"&&typeof O!="symbol"&&typeof O!="boolean"?t.name=""+en(O):t.removeAttribute("name")}function Ol(t,r,i,u,h,x,C,O){if(x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"&&(t.type=x),r!=null||i!=null){if(!(x!=="submit"&&x!=="reset"||r!=null))return;i=i!=null?""+en(i):"",r=r!=null?""+en(r):i,O||r===t.value||(t.value=r),t.defaultValue=r}u=u??h,u=typeof u!="function"&&typeof u!="symbol"&&!!u,t.checked=O?t.checked:!!u,t.defaultChecked=!!u,C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"&&(t.name=C)}function Pa(t,r,i){r==="number"&&bo(t.ownerDocument)===t||t.defaultValue===""+i||(t.defaultValue=""+i)}function pr(t,r,i,u){if(t=t.options,r){r={};for(var h=0;h"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Pd=!1;if(gr)try{var Va={};Object.defineProperty(Va,"passive",{get:function(){Pd=!0}}),window.addEventListener("test",Va,Va),window.removeEventListener("test",Va,Va)}catch{Pd=!1}var Vr=null,Ud=null,Il=null;function Sg(){if(Il)return Il;var t,r=Ud,i=r.length,u,h="value"in Vr?Vr.value:Vr.textContent,x=h.length;for(t=0;t=Ya),Ag=" ",Mg=!1;function Tg(t,r){switch(t){case"keyup":return Bj.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Rg(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var jo=!1;function Uj(t,r){switch(t){case"compositionend":return Rg(r);case"keypress":return r.which!==32?null:(Mg=!0,Ag);case"textInput":return t=r.data,t===Ag&&Mg?null:t;default:return null}}function Vj(t,r){if(jo)return t==="compositionend"||!Gd&&Tg(t,r)?(t=Sg(),Il=Ud=Vr=null,jo=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(r.ctrlKey||r.altKey||r.metaKey)||r.ctrlKey&&r.altKey){if(r.char&&1=r)return{node:i,offset:r-t};t=u}e:{for(;i;){if(i.nextSibling){i=i.nextSibling;break e}i=i.parentNode}i=void 0}i=Bg(i)}}function Ug(t,r){return t&&r?t===r?!0:t&&t.nodeType===3?!1:r&&r.nodeType===3?Ug(t,r.parentNode):"contains"in t?t.contains(r):t.compareDocumentPosition?!!(t.compareDocumentPosition(r)&16):!1:!1}function Vg(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var r=bo(t.document);r instanceof t.HTMLIFrameElement;){try{var i=typeof r.contentWindow.location.href=="string"}catch{i=!1}if(i)t=r.contentWindow;else break;r=bo(t.document)}return r}function Wd(t){var r=t&&t.nodeName&&t.nodeName.toLowerCase();return r&&(r==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||r==="textarea"||t.contentEditable==="true")}var Kj=gr&&"documentMode"in document&&11>=document.documentMode,_o=null,Kd=null,Wa=null,Qd=!1;function qg(t,r,i){var u=i.window===i?i.document:i.nodeType===9?i:i.ownerDocument;Qd||_o==null||_o!==bo(u)||(u=_o,"selectionStart"in u&&Wd(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),Wa&&Za(Wa,u)||(Wa=u,u=Ec(Kd,"onSelect"),0>=C,h-=C,yr=1<<32-ce(r)+h|i<x?x:8;var C=A.T,O={};A.T=O,Hf(t,!1,r,i);try{var q=h(),ee=A.S;if(ee!==null&&ee(O,q),q!==null&&typeof q=="object"&&typeof q.then=="function"){var fe=a_(q,u);di(t,r,fe,mn(t))}else di(t,r,u,mn(t))}catch(xe){di(t,r,{then:function(){},status:"rejected",reason:xe},mn())}finally{I.p=x,A.T=C}}function d_(){}function If(t,r,i,u){if(t.tag!==5)throw Error(o(476));var h=Fx(t).queue;qx(t,h,r,$,i===null?d_:function(){return Yx(t),i(u)})}function Fx(t){var r=t.memoizedState;if(r!==null)return r;r={memoizedState:$,baseState:$,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Nr,lastRenderedState:$},next:null};var i={};return r.next={memoizedState:i,baseState:i,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Nr,lastRenderedState:i},next:null},t.memoizedState=r,t=t.alternate,t!==null&&(t.memoizedState=r),r}function Yx(t){var r=Fx(t).next.queue;di(t,r,{},mn())}function Lf(){return Ft(Ai)}function Gx(){return Ct().memoizedState}function Xx(){return Ct().memoizedState}function f_(t){for(var r=t.return;r!==null;){switch(r.tag){case 24:case 3:var i=mn();t=Yr(i);var u=Gr(r,t,i);u!==null&&(hn(u,r,i),oi(u,r,i)),r={cache:mf()},t.payload=r;return}r=r.return}}function m_(t,r,i){var u=mn();i={lane:u,revertLane:0,action:i,hasEagerState:!1,eagerState:null,next:null},ac(t)?Wx(r,i):(i=nf(t,r,i,u),i!==null&&(hn(i,t,u),Kx(i,r,u)))}function Zx(t,r,i){var u=mn();di(t,r,i,u)}function di(t,r,i,u){var h={lane:u,revertLane:0,action:i,hasEagerState:!1,eagerState:null,next:null};if(ac(t))Wx(r,h);else{var x=t.alternate;if(t.lanes===0&&(x===null||x.lanes===0)&&(x=r.lastRenderedReducer,x!==null))try{var C=r.lastRenderedState,O=x(C,i);if(h.hasEagerState=!0,h.eagerState=O,ln(O,C))return Vl(t,r,h,0),pt===null&&Ul(),!1}catch{}finally{}if(i=nf(t,r,h,u),i!==null)return hn(i,t,u),Kx(i,r,u),!0}return!1}function Hf(t,r,i,u){if(u={lane:2,revertLane:gm(),action:u,hasEagerState:!1,eagerState:null,next:null},ac(t)){if(r)throw Error(o(479))}else r=nf(t,i,u,2),r!==null&&hn(r,t,2)}function ac(t){var r=t.alternate;return t===qe||r!==null&&r===qe}function Wx(t,r){zo=ec=!0;var i=t.pending;i===null?r.next=r:(r.next=i.next,i.next=r),t.pending=r}function Kx(t,r,i){if((i&4194048)!==0){var u=r.lanes;u&=t.pendingLanes,i|=u,r.lanes=i,Ta(t,i)}}var ic={readContext:Ft,use:nc,useCallback:St,useContext:St,useEffect:St,useImperativeHandle:St,useLayoutEffect:St,useInsertionEffect:St,useMemo:St,useReducer:St,useRef:St,useState:St,useDebugValue:St,useDeferredValue:St,useTransition:St,useSyncExternalStore:St,useId:St,useHostTransitionStatus:St,useFormState:St,useActionState:St,useOptimistic:St,useMemoCache:St,useCacheRefresh:St},Qx={readContext:Ft,use:nc,useCallback:function(t,r){return rn().memoizedState=[t,r===void 0?null:r],t},useContext:Ft,useEffect:zx,useImperativeHandle:function(t,r,i){i=i!=null?i.concat([t]):null,oc(4194308,4,$x.bind(null,r,t),i)},useLayoutEffect:function(t,r){return oc(4194308,4,t,r)},useInsertionEffect:function(t,r){oc(4,2,t,r)},useMemo:function(t,r){var i=rn();r=r===void 0?null:r;var u=t();if(qs){ye(!0);try{t()}finally{ye(!1)}}return i.memoizedState=[u,r],u},useReducer:function(t,r,i){var u=rn();if(i!==void 0){var h=i(r);if(qs){ye(!0);try{i(r)}finally{ye(!1)}}}else h=r;return u.memoizedState=u.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},u.queue=t,t=t.dispatch=m_.bind(null,qe,t),[u.memoizedState,t]},useRef:function(t){var r=rn();return t={current:t},r.memoizedState=t},useState:function(t){t=Rf(t);var r=t.queue,i=Zx.bind(null,qe,r);return r.dispatch=i,[t.memoizedState,i]},useDebugValue:Of,useDeferredValue:function(t,r){var i=rn();return zf(i,t,r)},useTransition:function(){var t=Rf(!1);return t=qx.bind(null,qe,t.queue,!0,!1),rn().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,r,i){var u=qe,h=rn();if(ot){if(i===void 0)throw Error(o(407));i=i()}else{if(i=r(),pt===null)throw Error(o(349));(et&124)!==0||vx(u,r,i)}h.memoizedState=i;var x={value:i,getSnapshot:r};return h.queue=x,zx(wx.bind(null,u,x,t),[t]),u.flags|=2048,Lo(9,sc(),bx.bind(null,u,x,i,r),null),i},useId:function(){var t=rn(),r=pt.identifierPrefix;if(ot){var i=vr,u=yr;i=(u&~(1<<32-ce(u)-1)).toString(32)+i,r="«"+r+"R"+i,i=tc++,0$e?(zt=ze,ze=null):zt=ze.sibling;var st=se(X,ze,Q[$e],he);if(st===null){ze===null&&(ze=zt);break}t&&ze&&st.alternate===null&&r(X,ze),Y=x(st,Y,$e),Ye===null?ke=st:Ye.sibling=st,Ye=st,ze=zt}if($e===Q.length)return i(X,ze),ot&&Hs(X,$e),ke;if(ze===null){for(;$e$e?(zt=ze,ze=null):zt=ze.sibling;var us=se(X,ze,st.value,he);if(us===null){ze===null&&(ze=zt);break}t&&ze&&us.alternate===null&&r(X,ze),Y=x(us,Y,$e),Ye===null?ke=us:Ye.sibling=us,Ye=us,ze=zt}if(st.done)return i(X,ze),ot&&Hs(X,$e),ke;if(ze===null){for(;!st.done;$e++,st=Q.next())st=xe(X,st.value,he),st!==null&&(Y=x(st,Y,$e),Ye===null?ke=st:Ye.sibling=st,Ye=st);return ot&&Hs(X,$e),ke}for(ze=u(ze);!st.done;$e++,st=Q.next())st=oe(ze,X,$e,st.value,he),st!==null&&(t&&st.alternate!==null&&ze.delete(st.key===null?$e:st.key),Y=x(st,Y,$e),Ye===null?ke=st:Ye.sibling=st,Ye=st);return t&&ze.forEach(function(pE){return r(X,pE)}),ot&&Hs(X,$e),ke}function ut(X,Y,Q,he){if(typeof Q=="object"&&Q!==null&&Q.type===S&&Q.key===null&&(Q=Q.props.children),typeof Q=="object"&&Q!==null){switch(Q.$$typeof){case y:e:{for(var ke=Q.key;Y!==null;){if(Y.key===ke){if(ke=Q.type,ke===S){if(Y.tag===7){i(X,Y.sibling),he=h(Y,Q.props.children),he.return=X,X=he;break e}}else if(Y.elementType===ke||typeof ke=="object"&&ke!==null&&ke.$$typeof===H&&e0(ke)===Y.type){i(X,Y.sibling),he=h(Y,Q.props),mi(he,Q),he.return=X,X=he;break e}i(X,Y);break}else r(X,Y);Y=Y.sibling}Q.type===S?(he=Is(Q.props.children,X.mode,he,Q.key),he.return=X,X=he):(he=Fl(Q.type,Q.key,Q.props,null,X.mode,he),mi(he,Q),he.return=X,X=he)}return C(X);case b:e:{for(ke=Q.key;Y!==null;){if(Y.key===ke)if(Y.tag===4&&Y.stateNode.containerInfo===Q.containerInfo&&Y.stateNode.implementation===Q.implementation){i(X,Y.sibling),he=h(Y,Q.children||[]),he.return=X,X=he;break e}else{i(X,Y);break}else r(X,Y);Y=Y.sibling}he=of(Q,X.mode,he),he.return=X,X=he}return C(X);case H:return ke=Q._init,Q=ke(Q._payload),ut(X,Y,Q,he)}if(V(Q))return Be(X,Y,Q,he);if(G(Q)){if(ke=G(Q),typeof ke!="function")throw Error(o(150));return Q=ke.call(Q),Le(X,Y,Q,he)}if(typeof Q.then=="function")return ut(X,Y,lc(Q),he);if(Q.$$typeof===j)return ut(X,Y,Zl(X,Q),he);cc(X,Q)}return typeof Q=="string"&&Q!==""||typeof Q=="number"||typeof Q=="bigint"?(Q=""+Q,Y!==null&&Y.tag===6?(i(X,Y.sibling),he=h(Y,Q),he.return=X,X=he):(i(X,Y),he=sf(Q,X.mode,he),he.return=X,X=he),C(X)):i(X,Y)}return function(X,Y,Q,he){try{fi=0;var ke=ut(X,Y,Q,he);return Ho=null,ke}catch(ze){if(ze===ri||ze===Kl)throw ze;var Ye=cn(29,ze,null,X.mode);return Ye.lanes=he,Ye.return=X,Ye}finally{}}}var $o=t0(!0),n0=t0(!1),En=B(null),Xn=null;function Zr(t){var r=t.alternate;Z(Mt,Mt.current&1),Z(En,t),Xn===null&&(r===null||Oo.current!==null||r.memoizedState!==null)&&(Xn=t)}function r0(t){if(t.tag===22){if(Z(Mt,Mt.current),Z(En,t),Xn===null){var r=t.alternate;r!==null&&r.memoizedState!==null&&(Xn=t)}}else Wr()}function Wr(){Z(Mt,Mt.current),Z(En,En.current)}function Sr(t){W(En),Xn===t&&(Xn=null),W(Mt)}var Mt=B(0);function uc(t){for(var r=t;r!==null;){if(r.tag===13){var i=r.memoizedState;if(i!==null&&(i=i.dehydrated,i===null||i.data==="$?"||km(i)))return r}else if(r.tag===19&&r.memoizedProps.revealOrder!==void 0){if((r.flags&128)!==0)return r}else if(r.child!==null){r.child.return=r,r=r.child;continue}if(r===t)break;for(;r.sibling===null;){if(r.return===null||r.return===t)return null;r=r.return}r.sibling.return=r.return,r=r.sibling}return null}function $f(t,r,i,u){r=t.memoizedState,i=i(u,r),i=i==null?r:g({},r,i),t.memoizedState=i,t.lanes===0&&(t.updateQueue.baseState=i)}var Bf={enqueueSetState:function(t,r,i){t=t._reactInternals;var u=mn(),h=Yr(u);h.payload=r,i!=null&&(h.callback=i),r=Gr(t,h,u),r!==null&&(hn(r,t,u),oi(r,t,u))},enqueueReplaceState:function(t,r,i){t=t._reactInternals;var u=mn(),h=Yr(u);h.tag=1,h.payload=r,i!=null&&(h.callback=i),r=Gr(t,h,u),r!==null&&(hn(r,t,u),oi(r,t,u))},enqueueForceUpdate:function(t,r){t=t._reactInternals;var i=mn(),u=Yr(i);u.tag=2,r!=null&&(u.callback=r),r=Gr(t,u,i),r!==null&&(hn(r,t,i),oi(r,t,i))}};function s0(t,r,i,u,h,x,C){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(u,x,C):r.prototype&&r.prototype.isPureReactComponent?!Za(i,u)||!Za(h,x):!0}function o0(t,r,i,u){t=r.state,typeof r.componentWillReceiveProps=="function"&&r.componentWillReceiveProps(i,u),typeof r.UNSAFE_componentWillReceiveProps=="function"&&r.UNSAFE_componentWillReceiveProps(i,u),r.state!==t&&Bf.enqueueReplaceState(r,r.state,null)}function Fs(t,r){var i=r;if("ref"in r){i={};for(var u in r)u!=="ref"&&(i[u]=r[u])}if(t=t.defaultProps){i===r&&(i=g({},i));for(var h in t)i[h]===void 0&&(i[h]=t[h])}return i}var dc=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var r=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(r))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)};function a0(t){dc(t)}function i0(t){console.error(t)}function l0(t){dc(t)}function fc(t,r){try{var i=t.onUncaughtError;i(r.value,{componentStack:r.stack})}catch(u){setTimeout(function(){throw u})}}function c0(t,r,i){try{var u=t.onCaughtError;u(i.value,{componentStack:i.stack,errorBoundary:r.tag===1?r.stateNode:null})}catch(h){setTimeout(function(){throw h})}}function Pf(t,r,i){return i=Yr(i),i.tag=3,i.payload={element:null},i.callback=function(){fc(t,r)},i}function u0(t){return t=Yr(t),t.tag=3,t}function d0(t,r,i,u){var h=i.type.getDerivedStateFromError;if(typeof h=="function"){var x=u.value;t.payload=function(){return h(x)},t.callback=function(){c0(r,i,u)}}var C=i.stateNode;C!==null&&typeof C.componentDidCatch=="function"&&(t.callback=function(){c0(r,i,u),typeof h!="function"&&(ns===null?ns=new Set([this]):ns.add(this));var O=u.stack;this.componentDidCatch(u.value,{componentStack:O!==null?O:""})})}function p_(t,r,i,u,h){if(i.flags|=32768,u!==null&&typeof u=="object"&&typeof u.then=="function"){if(r=i.alternate,r!==null&&ei(r,i,h,!0),i=En.current,i!==null){switch(i.tag){case 13:return Xn===null?dm():i.alternate===null&&Nt===0&&(Nt=3),i.flags&=-257,i.flags|=65536,i.lanes=h,u===gf?i.flags|=16384:(r=i.updateQueue,r===null?i.updateQueue=new Set([u]):r.add(u),mm(t,u,h)),!1;case 22:return i.flags|=65536,u===gf?i.flags|=16384:(r=i.updateQueue,r===null?(r={transitions:null,markerInstances:null,retryQueue:new Set([u])},i.updateQueue=r):(i=r.retryQueue,i===null?r.retryQueue=new Set([u]):i.add(u)),mm(t,u,h)),!1}throw Error(o(435,i.tag))}return mm(t,u,h),dm(),!1}if(ot)return r=En.current,r!==null?((r.flags&65536)===0&&(r.flags|=256),r.flags|=65536,r.lanes=h,u!==cf&&(t=Error(o(422),{cause:u}),Ja(Nn(t,i)))):(u!==cf&&(r=Error(o(423),{cause:u}),Ja(Nn(r,i))),t=t.current.alternate,t.flags|=65536,h&=-h,t.lanes|=h,u=Nn(u,i),h=Pf(t.stateNode,u,h),vf(t,h),Nt!==4&&(Nt=2)),!1;var x=Error(o(520),{cause:u});if(x=Nn(x,i),bi===null?bi=[x]:bi.push(x),Nt!==4&&(Nt=2),r===null)return!0;u=Nn(u,i),i=r;do{switch(i.tag){case 3:return i.flags|=65536,t=h&-h,i.lanes|=t,t=Pf(i.stateNode,u,t),vf(i,t),!1;case 1:if(r=i.type,x=i.stateNode,(i.flags&128)===0&&(typeof r.getDerivedStateFromError=="function"||x!==null&&typeof x.componentDidCatch=="function"&&(ns===null||!ns.has(x))))return i.flags|=65536,h&=-h,i.lanes|=h,h=u0(h),d0(h,t,i,u),vf(i,h),!1}i=i.return}while(i!==null);return!1}var f0=Error(o(461)),Dt=!1;function Lt(t,r,i,u){r.child=t===null?n0(r,null,i,u):$o(r,t.child,i,u)}function m0(t,r,i,u,h){i=i.render;var x=r.ref;if("ref"in u){var C={};for(var O in u)O!=="ref"&&(C[O]=u[O])}else C=u;return Us(r),u=jf(t,r,i,C,x,h),O=_f(),t!==null&&!Dt?(Ef(t,r,h),jr(t,r,h)):(ot&&O&&af(r),r.flags|=1,Lt(t,r,u,h),r.child)}function h0(t,r,i,u,h){if(t===null){var x=i.type;return typeof x=="function"&&!rf(x)&&x.defaultProps===void 0&&i.compare===null?(r.tag=15,r.type=x,p0(t,r,x,u,h)):(t=Fl(i.type,null,u,r,r.mode,h),t.ref=r.ref,t.return=r,r.child=t)}if(x=t.child,!Zf(t,h)){var C=x.memoizedProps;if(i=i.compare,i=i!==null?i:Za,i(C,u)&&t.ref===r.ref)return jr(t,r,h)}return r.flags|=1,t=xr(x,u),t.ref=r.ref,t.return=r,r.child=t}function p0(t,r,i,u,h){if(t!==null){var x=t.memoizedProps;if(Za(x,u)&&t.ref===r.ref)if(Dt=!1,r.pendingProps=u=x,Zf(t,h))(t.flags&131072)!==0&&(Dt=!0);else return r.lanes=t.lanes,jr(t,r,h)}return Uf(t,r,i,u,h)}function g0(t,r,i){var u=r.pendingProps,h=u.children,x=t!==null?t.memoizedState:null;if(u.mode==="hidden"){if((r.flags&128)!==0){if(u=x!==null?x.baseLanes|i:i,t!==null){for(h=r.child=t.child,x=0;h!==null;)x=x|h.lanes|h.childLanes,h=h.sibling;r.childLanes=x&~u}else r.childLanes=0,r.child=null;return x0(t,r,u,i)}if((i&536870912)!==0)r.memoizedState={baseLanes:0,cachePool:null},t!==null&&Wl(r,x!==null?x.cachePool:null),x!==null?px(r,x):wf(),r0(r);else return r.lanes=r.childLanes=536870912,x0(t,r,x!==null?x.baseLanes|i:i,i)}else x!==null?(Wl(r,x.cachePool),px(r,x),Wr(),r.memoizedState=null):(t!==null&&Wl(r,null),wf(),Wr());return Lt(t,r,h,i),r.child}function x0(t,r,i,u){var h=pf();return h=h===null?null:{parent:At._currentValue,pool:h},r.memoizedState={baseLanes:i,cachePool:h},t!==null&&Wl(r,null),wf(),r0(r),t!==null&&ei(t,r,u,!0),null}function mc(t,r){var i=r.ref;if(i===null)t!==null&&t.ref!==null&&(r.flags|=4194816);else{if(typeof i!="function"&&typeof i!="object")throw Error(o(284));(t===null||t.ref!==i)&&(r.flags|=4194816)}}function Uf(t,r,i,u,h){return Us(r),i=jf(t,r,i,u,void 0,h),u=_f(),t!==null&&!Dt?(Ef(t,r,h),jr(t,r,h)):(ot&&u&&af(r),r.flags|=1,Lt(t,r,i,h),r.child)}function y0(t,r,i,u,h,x){return Us(r),r.updateQueue=null,i=xx(r,u,i,h),gx(t),u=_f(),t!==null&&!Dt?(Ef(t,r,x),jr(t,r,x)):(ot&&u&&af(r),r.flags|=1,Lt(t,r,i,x),r.child)}function v0(t,r,i,u,h){if(Us(r),r.stateNode===null){var x=Ao,C=i.contextType;typeof C=="object"&&C!==null&&(x=Ft(C)),x=new i(u,x),r.memoizedState=x.state!==null&&x.state!==void 0?x.state:null,x.updater=Bf,r.stateNode=x,x._reactInternals=r,x=r.stateNode,x.props=u,x.state=r.memoizedState,x.refs={},xf(r),C=i.contextType,x.context=typeof C=="object"&&C!==null?Ft(C):Ao,x.state=r.memoizedState,C=i.getDerivedStateFromProps,typeof C=="function"&&($f(r,i,C,u),x.state=r.memoizedState),typeof i.getDerivedStateFromProps=="function"||typeof x.getSnapshotBeforeUpdate=="function"||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(C=x.state,typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount(),C!==x.state&&Bf.enqueueReplaceState(x,x.state,null),ii(r,u,x,h),ai(),x.state=r.memoizedState),typeof x.componentDidMount=="function"&&(r.flags|=4194308),u=!0}else if(t===null){x=r.stateNode;var O=r.memoizedProps,q=Fs(i,O);x.props=q;var ee=x.context,fe=i.contextType;C=Ao,typeof fe=="object"&&fe!==null&&(C=Ft(fe));var xe=i.getDerivedStateFromProps;fe=typeof xe=="function"||typeof x.getSnapshotBeforeUpdate=="function",O=r.pendingProps!==O,fe||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(O||ee!==C)&&o0(r,x,u,C),Fr=!1;var se=r.memoizedState;x.state=se,ii(r,u,x,h),ai(),ee=r.memoizedState,O||se!==ee||Fr?(typeof xe=="function"&&($f(r,i,xe,u),ee=r.memoizedState),(q=Fr||s0(r,i,q,u,se,ee,C))?(fe||typeof x.UNSAFE_componentWillMount!="function"&&typeof x.componentWillMount!="function"||(typeof x.componentWillMount=="function"&&x.componentWillMount(),typeof x.UNSAFE_componentWillMount=="function"&&x.UNSAFE_componentWillMount()),typeof x.componentDidMount=="function"&&(r.flags|=4194308)):(typeof x.componentDidMount=="function"&&(r.flags|=4194308),r.memoizedProps=u,r.memoizedState=ee),x.props=u,x.state=ee,x.context=C,u=q):(typeof x.componentDidMount=="function"&&(r.flags|=4194308),u=!1)}else{x=r.stateNode,yf(t,r),C=r.memoizedProps,fe=Fs(i,C),x.props=fe,xe=r.pendingProps,se=x.context,ee=i.contextType,q=Ao,typeof ee=="object"&&ee!==null&&(q=Ft(ee)),O=i.getDerivedStateFromProps,(ee=typeof O=="function"||typeof x.getSnapshotBeforeUpdate=="function")||typeof x.UNSAFE_componentWillReceiveProps!="function"&&typeof x.componentWillReceiveProps!="function"||(C!==xe||se!==q)&&o0(r,x,u,q),Fr=!1,se=r.memoizedState,x.state=se,ii(r,u,x,h),ai();var oe=r.memoizedState;C!==xe||se!==oe||Fr||t!==null&&t.dependencies!==null&&Xl(t.dependencies)?(typeof O=="function"&&($f(r,i,O,u),oe=r.memoizedState),(fe=Fr||s0(r,i,fe,u,se,oe,q)||t!==null&&t.dependencies!==null&&Xl(t.dependencies))?(ee||typeof x.UNSAFE_componentWillUpdate!="function"&&typeof x.componentWillUpdate!="function"||(typeof x.componentWillUpdate=="function"&&x.componentWillUpdate(u,oe,q),typeof x.UNSAFE_componentWillUpdate=="function"&&x.UNSAFE_componentWillUpdate(u,oe,q)),typeof x.componentDidUpdate=="function"&&(r.flags|=4),typeof x.getSnapshotBeforeUpdate=="function"&&(r.flags|=1024)):(typeof x.componentDidUpdate!="function"||C===t.memoizedProps&&se===t.memoizedState||(r.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&se===t.memoizedState||(r.flags|=1024),r.memoizedProps=u,r.memoizedState=oe),x.props=u,x.state=oe,x.context=q,u=fe):(typeof x.componentDidUpdate!="function"||C===t.memoizedProps&&se===t.memoizedState||(r.flags|=4),typeof x.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&se===t.memoizedState||(r.flags|=1024),u=!1)}return x=u,mc(t,r),u=(r.flags&128)!==0,x||u?(x=r.stateNode,i=u&&typeof i.getDerivedStateFromError!="function"?null:x.render(),r.flags|=1,t!==null&&u?(r.child=$o(r,t.child,null,h),r.child=$o(r,null,i,h)):Lt(t,r,i,h),r.memoizedState=x.state,t=r.child):t=jr(t,r,h),t}function b0(t,r,i,u){return Qa(),r.flags|=256,Lt(t,r,i,u),r.child}var Vf={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function qf(t){return{baseLanes:t,cachePool:ix()}}function Ff(t,r,i){return t=t!==null?t.childLanes&~i:0,r&&(t|=Cn),t}function w0(t,r,i){var u=r.pendingProps,h=!1,x=(r.flags&128)!==0,C;if((C=x)||(C=t!==null&&t.memoizedState===null?!1:(Mt.current&2)!==0),C&&(h=!0,r.flags&=-129),C=(r.flags&32)!==0,r.flags&=-33,t===null){if(ot){if(h?Zr(r):Wr(),ot){var O=wt,q;if(q=O){e:{for(q=O,O=Gn;q.nodeType!==8;){if(!O){O=null;break e}if(q=zn(q.nextSibling),q===null){O=null;break e}}O=q}O!==null?(r.memoizedState={dehydrated:O,treeContext:Ls!==null?{id:yr,overflow:vr}:null,retryLane:536870912,hydrationErrors:null},q=cn(18,null,null,0),q.stateNode=O,q.return=r,r.child=q,Wt=r,wt=null,q=!0):q=!1}q||Bs(r)}if(O=r.memoizedState,O!==null&&(O=O.dehydrated,O!==null))return km(O)?r.lanes=32:r.lanes=536870912,null;Sr(r)}return O=u.children,u=u.fallback,h?(Wr(),h=r.mode,O=hc({mode:"hidden",children:O},h),u=Is(u,h,i,null),O.return=r,u.return=r,O.sibling=u,r.child=O,h=r.child,h.memoizedState=qf(i),h.childLanes=Ff(t,C,i),r.memoizedState=Vf,u):(Zr(r),Yf(r,O))}if(q=t.memoizedState,q!==null&&(O=q.dehydrated,O!==null)){if(x)r.flags&256?(Zr(r),r.flags&=-257,r=Gf(t,r,i)):r.memoizedState!==null?(Wr(),r.child=t.child,r.flags|=128,r=null):(Wr(),h=u.fallback,O=r.mode,u=hc({mode:"visible",children:u.children},O),h=Is(h,O,i,null),h.flags|=2,u.return=r,h.return=r,u.sibling=h,r.child=u,$o(r,t.child,null,i),u=r.child,u.memoizedState=qf(i),u.childLanes=Ff(t,C,i),r.memoizedState=Vf,r=h);else if(Zr(r),km(O)){if(C=O.nextSibling&&O.nextSibling.dataset,C)var ee=C.dgst;C=ee,u=Error(o(419)),u.stack="",u.digest=C,Ja({value:u,source:null,stack:null}),r=Gf(t,r,i)}else if(Dt||ei(t,r,i,!1),C=(i&t.childLanes)!==0,Dt||C){if(C=pt,C!==null&&(u=i&-i,u=(u&42)!==0?1:Ra(u),u=(u&(C.suspendedLanes|i))!==0?0:u,u!==0&&u!==q.retryLane))throw q.retryLane=u,ko(t,u),hn(C,t,u),f0;O.data==="$?"||dm(),r=Gf(t,r,i)}else O.data==="$?"?(r.flags|=192,r.child=t.child,r=null):(t=q.treeContext,wt=zn(O.nextSibling),Wt=r,ot=!0,$s=null,Gn=!1,t!==null&&(jn[_n++]=yr,jn[_n++]=vr,jn[_n++]=Ls,yr=t.id,vr=t.overflow,Ls=r),r=Yf(r,u.children),r.flags|=4096);return r}return h?(Wr(),h=u.fallback,O=r.mode,q=t.child,ee=q.sibling,u=xr(q,{mode:"hidden",children:u.children}),u.subtreeFlags=q.subtreeFlags&65011712,ee!==null?h=xr(ee,h):(h=Is(h,O,i,null),h.flags|=2),h.return=r,u.return=r,u.sibling=h,r.child=u,u=h,h=r.child,O=t.child.memoizedState,O===null?O=qf(i):(q=O.cachePool,q!==null?(ee=At._currentValue,q=q.parent!==ee?{parent:ee,pool:ee}:q):q=ix(),O={baseLanes:O.baseLanes|i,cachePool:q}),h.memoizedState=O,h.childLanes=Ff(t,C,i),r.memoizedState=Vf,u):(Zr(r),i=t.child,t=i.sibling,i=xr(i,{mode:"visible",children:u.children}),i.return=r,i.sibling=null,t!==null&&(C=r.deletions,C===null?(r.deletions=[t],r.flags|=16):C.push(t)),r.child=i,r.memoizedState=null,i)}function Yf(t,r){return r=hc({mode:"visible",children:r},t.mode),r.return=t,t.child=r}function hc(t,r){return t=cn(22,t,null,r),t.lanes=0,t.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},t}function Gf(t,r,i){return $o(r,t.child,null,i),t=Yf(r,r.pendingProps.children),t.flags|=2,r.memoizedState=null,t}function N0(t,r,i){t.lanes|=r;var u=t.alternate;u!==null&&(u.lanes|=r),df(t.return,r,i)}function Xf(t,r,i,u,h){var x=t.memoizedState;x===null?t.memoizedState={isBackwards:r,rendering:null,renderingStartTime:0,last:u,tail:i,tailMode:h}:(x.isBackwards=r,x.rendering=null,x.renderingStartTime=0,x.last=u,x.tail=i,x.tailMode=h)}function S0(t,r,i){var u=r.pendingProps,h=u.revealOrder,x=u.tail;if(Lt(t,r,u.children,i),u=Mt.current,(u&2)!==0)u=u&1|2,r.flags|=128;else{if(t!==null&&(t.flags&128)!==0)e:for(t=r.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&N0(t,i,r);else if(t.tag===19)N0(t,i,r);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===r)break e;for(;t.sibling===null;){if(t.return===null||t.return===r)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}u&=1}switch(Z(Mt,u),h){case"forwards":for(i=r.child,h=null;i!==null;)t=i.alternate,t!==null&&uc(t)===null&&(h=i),i=i.sibling;i=h,i===null?(h=r.child,r.child=null):(h=i.sibling,i.sibling=null),Xf(r,!1,h,i,x);break;case"backwards":for(i=null,h=r.child,r.child=null;h!==null;){if(t=h.alternate,t!==null&&uc(t)===null){r.child=h;break}t=h.sibling,h.sibling=i,i=h,h=t}Xf(r,!0,i,null,x);break;case"together":Xf(r,!1,null,null,void 0);break;default:r.memoizedState=null}return r.child}function jr(t,r,i){if(t!==null&&(r.dependencies=t.dependencies),ts|=r.lanes,(i&r.childLanes)===0)if(t!==null){if(ei(t,r,i,!1),(i&r.childLanes)===0)return null}else return null;if(t!==null&&r.child!==t.child)throw Error(o(153));if(r.child!==null){for(t=r.child,i=xr(t,t.pendingProps),r.child=i,i.return=r;t.sibling!==null;)t=t.sibling,i=i.sibling=xr(t,t.pendingProps),i.return=r;i.sibling=null}return r.child}function Zf(t,r){return(t.lanes&r)!==0?!0:(t=t.dependencies,!!(t!==null&&Xl(t)))}function g_(t,r,i){switch(r.tag){case 3:le(r,r.stateNode.containerInfo),qr(r,At,t.memoizedState.cache),Qa();break;case 27:case 5:Ne(r);break;case 4:le(r,r.stateNode.containerInfo);break;case 10:qr(r,r.type,r.memoizedProps.value);break;case 13:var u=r.memoizedState;if(u!==null)return u.dehydrated!==null?(Zr(r),r.flags|=128,null):(i&r.child.childLanes)!==0?w0(t,r,i):(Zr(r),t=jr(t,r,i),t!==null?t.sibling:null);Zr(r);break;case 19:var h=(t.flags&128)!==0;if(u=(i&r.childLanes)!==0,u||(ei(t,r,i,!1),u=(i&r.childLanes)!==0),h){if(u)return S0(t,r,i);r.flags|=128}if(h=r.memoizedState,h!==null&&(h.rendering=null,h.tail=null,h.lastEffect=null),Z(Mt,Mt.current),u)break;return null;case 22:case 23:return r.lanes=0,g0(t,r,i);case 24:qr(r,At,t.memoizedState.cache)}return jr(t,r,i)}function j0(t,r,i){if(t!==null)if(t.memoizedProps!==r.pendingProps)Dt=!0;else{if(!Zf(t,i)&&(r.flags&128)===0)return Dt=!1,g_(t,r,i);Dt=(t.flags&131072)!==0}else Dt=!1,ot&&(r.flags&1048576)!==0&&ex(r,Gl,r.index);switch(r.lanes=0,r.tag){case 16:e:{t=r.pendingProps;var u=r.elementType,h=u._init;if(u=h(u._payload),r.type=u,typeof u=="function")rf(u)?(t=Fs(u,t),r.tag=1,r=v0(null,r,u,t,i)):(r.tag=0,r=Uf(null,r,u,t,i));else{if(u!=null){if(h=u.$$typeof,h===k){r.tag=11,r=m0(null,r,u,t,i);break e}else if(h===z){r.tag=14,r=h0(null,r,u,t,i);break e}}throw r=L(u)||u,Error(o(306,r,""))}}return r;case 0:return Uf(t,r,r.type,r.pendingProps,i);case 1:return u=r.type,h=Fs(u,r.pendingProps),v0(t,r,u,h,i);case 3:e:{if(le(r,r.stateNode.containerInfo),t===null)throw Error(o(387));u=r.pendingProps;var x=r.memoizedState;h=x.element,yf(t,r),ii(r,u,null,i);var C=r.memoizedState;if(u=C.cache,qr(r,At,u),u!==x.cache&&ff(r,[At],i,!0),ai(),u=C.element,x.isDehydrated)if(x={element:u,isDehydrated:!1,cache:C.cache},r.updateQueue.baseState=x,r.memoizedState=x,r.flags&256){r=b0(t,r,u,i);break e}else if(u!==h){h=Nn(Error(o(424)),r),Ja(h),r=b0(t,r,u,i);break e}else{switch(t=r.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(wt=zn(t.firstChild),Wt=r,ot=!0,$s=null,Gn=!0,i=n0(r,null,u,i),r.child=i;i;)i.flags=i.flags&-3|4096,i=i.sibling}else{if(Qa(),u===h){r=jr(t,r,i);break e}Lt(t,r,u,i)}r=r.child}return r;case 26:return mc(t,r),t===null?(i=ky(r.type,null,r.pendingProps,null))?r.memoizedState=i:ot||(i=r.type,t=r.pendingProps,u=kc(ge.current).createElement(i),u[Rt]=r,u[qt]=t,$t(u,i,t),_t(u),r.stateNode=u):r.memoizedState=ky(r.type,t.memoizedProps,r.pendingProps,t.memoizedState),null;case 27:return Ne(r),t===null&&ot&&(u=r.stateNode=_y(r.type,r.pendingProps,ge.current),Wt=r,Gn=!0,h=wt,os(r.type)?(Am=h,wt=zn(u.firstChild)):wt=h),Lt(t,r,r.pendingProps.children,i),mc(t,r),t===null&&(r.flags|=4194304),r.child;case 5:return t===null&&ot&&((h=u=wt)&&(u=q_(u,r.type,r.pendingProps,Gn),u!==null?(r.stateNode=u,Wt=r,wt=zn(u.firstChild),Gn=!1,h=!0):h=!1),h||Bs(r)),Ne(r),h=r.type,x=r.pendingProps,C=t!==null?t.memoizedProps:null,u=x.children,_m(h,x)?u=null:C!==null&&_m(h,C)&&(r.flags|=32),r.memoizedState!==null&&(h=jf(t,r,l_,null,null,i),Ai._currentValue=h),mc(t,r),Lt(t,r,u,i),r.child;case 6:return t===null&&ot&&((t=i=wt)&&(i=F_(i,r.pendingProps,Gn),i!==null?(r.stateNode=i,Wt=r,wt=null,t=!0):t=!1),t||Bs(r)),null;case 13:return w0(t,r,i);case 4:return le(r,r.stateNode.containerInfo),u=r.pendingProps,t===null?r.child=$o(r,null,u,i):Lt(t,r,u,i),r.child;case 11:return m0(t,r,r.type,r.pendingProps,i);case 7:return Lt(t,r,r.pendingProps,i),r.child;case 8:return Lt(t,r,r.pendingProps.children,i),r.child;case 12:return Lt(t,r,r.pendingProps.children,i),r.child;case 10:return u=r.pendingProps,qr(r,r.type,u.value),Lt(t,r,u.children,i),r.child;case 9:return h=r.type._context,u=r.pendingProps.children,Us(r),h=Ft(h),u=u(h),r.flags|=1,Lt(t,r,u,i),r.child;case 14:return h0(t,r,r.type,r.pendingProps,i);case 15:return p0(t,r,r.type,r.pendingProps,i);case 19:return S0(t,r,i);case 31:return u=r.pendingProps,i=r.mode,u={mode:u.mode,children:u.children},t===null?(i=hc(u,i),i.ref=r.ref,r.child=i,i.return=r,r=i):(i=xr(t.child,u),i.ref=r.ref,r.child=i,i.return=r,r=i),r;case 22:return g0(t,r,i);case 24:return Us(r),u=Ft(At),t===null?(h=pf(),h===null&&(h=pt,x=mf(),h.pooledCache=x,x.refCount++,x!==null&&(h.pooledCacheLanes|=i),h=x),r.memoizedState={parent:u,cache:h},xf(r),qr(r,At,h)):((t.lanes&i)!==0&&(yf(t,r),ii(r,null,null,i),ai()),h=t.memoizedState,x=r.memoizedState,h.parent!==u?(h={parent:u,cache:u},r.memoizedState=h,r.lanes===0&&(r.memoizedState=r.updateQueue.baseState=h),qr(r,At,u)):(u=x.cache,qr(r,At,u),u!==h.cache&&ff(r,[At],i,!0))),Lt(t,r,r.pendingProps.children,i),r.child;case 29:throw r.pendingProps}throw Error(o(156,r.tag))}function _r(t){t.flags|=4}function _0(t,r){if(r.type!=="stylesheet"||(r.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!Dy(r)){if(r=En.current,r!==null&&((et&4194048)===et?Xn!==null:(et&62914560)!==et&&(et&536870912)===0||r!==Xn))throw si=gf,lx;t.flags|=8192}}function pc(t,r){r!==null&&(t.flags|=4),t.flags&16384&&(r=t.tag!==22?Fn():536870912,t.lanes|=r,Vo|=r)}function hi(t,r){if(!ot)switch(t.tailMode){case"hidden":r=t.tail;for(var i=null;r!==null;)r.alternate!==null&&(i=r),r=r.sibling;i===null?t.tail=null:i.sibling=null;break;case"collapsed":i=t.tail;for(var u=null;i!==null;)i.alternate!==null&&(u=i),i=i.sibling;u===null?r||t.tail===null?t.tail=null:t.tail.sibling=null:u.sibling=null}}function vt(t){var r=t.alternate!==null&&t.alternate.child===t.child,i=0,u=0;if(r)for(var h=t.child;h!==null;)i|=h.lanes|h.childLanes,u|=h.subtreeFlags&65011712,u|=h.flags&65011712,h.return=t,h=h.sibling;else for(h=t.child;h!==null;)i|=h.lanes|h.childLanes,u|=h.subtreeFlags,u|=h.flags,h.return=t,h=h.sibling;return t.subtreeFlags|=u,t.childLanes=i,r}function x_(t,r,i){var u=r.pendingProps;switch(lf(r),r.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return vt(r),null;case 1:return vt(r),null;case 3:return i=r.stateNode,u=null,t!==null&&(u=t.memoizedState.cache),r.memoizedState.cache!==u&&(r.flags|=2048),wr(At),ve(),i.pendingContext&&(i.context=i.pendingContext,i.pendingContext=null),(t===null||t.child===null)&&(Ka(r)?_r(r):t===null||t.memoizedState.isDehydrated&&(r.flags&256)===0||(r.flags|=1024,rx())),vt(r),null;case 26:return i=r.memoizedState,t===null?(_r(r),i!==null?(vt(r),_0(r,i)):(vt(r),r.flags&=-16777217)):i?i!==t.memoizedState?(_r(r),vt(r),_0(r,i)):(vt(r),r.flags&=-16777217):(t.memoizedProps!==u&&_r(r),vt(r),r.flags&=-16777217),null;case 27:_e(r),i=ge.current;var h=r.type;if(t!==null&&r.stateNode!=null)t.memoizedProps!==u&&_r(r);else{if(!u){if(r.stateNode===null)throw Error(o(166));return vt(r),null}t=re.current,Ka(r)?tx(r):(t=_y(h,u,i),r.stateNode=t,_r(r))}return vt(r),null;case 5:if(_e(r),i=r.type,t!==null&&r.stateNode!=null)t.memoizedProps!==u&&_r(r);else{if(!u){if(r.stateNode===null)throw Error(o(166));return vt(r),null}if(t=re.current,Ka(r))tx(r);else{switch(h=kc(ge.current),t){case 1:t=h.createElementNS("http://www.w3.org/2000/svg",i);break;case 2:t=h.createElementNS("http://www.w3.org/1998/Math/MathML",i);break;default:switch(i){case"svg":t=h.createElementNS("http://www.w3.org/2000/svg",i);break;case"math":t=h.createElementNS("http://www.w3.org/1998/Math/MathML",i);break;case"script":t=h.createElement("div"),t.innerHTML="