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="