diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 4215400e63..00f10230a5 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -15,7 +15,7 @@ - + @@ -81,6 +81,10 @@ + + + + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 908884476d..0cedcfbdde 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -103,11 +103,11 @@ + - - + @@ -191,7 +191,7 @@ - + @@ -208,6 +208,9 @@ + + + @@ -371,6 +374,7 @@ + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/ActorFrameworkWebApplicationExtensions.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/ActorFrameworkWebApplicationExtensions.cs index 5e997c4f58..09e19a82f5 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/ActorFrameworkWebApplicationExtensions.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/ActorFrameworkWebApplicationExtensions.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Agents.AI.Hosting; +using Microsoft.Agents.AI; namespace AgentWebChat.AgentHost; @@ -10,24 +10,24 @@ internal static class ActorFrameworkWebApplicationExtensions { public static void MapAgentDiscovery(this IEndpointRouteBuilder endpoints, [StringSyntax("Route")] string path) { - var routeGroup = endpoints.MapGroup(path); - routeGroup.MapGet("/", async ( - AgentCatalog agentCatalog, - CancellationToken cancellationToken) => - { - var results = new List(); - await foreach (var result in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false)) - { - results.Add(new AgentDiscoveryCard - { - Name = result.Name!, - Description = result.Description, - }); - } + var registeredAIAgents = endpoints.ServiceProvider.GetKeyedServices(KeyedService.AnyKey); - return Results.Ok(results); - }) - .WithName("GetAgents"); + var routeGroup = endpoints.MapGroup(path); + routeGroup.MapGet("/", async (CancellationToken cancellationToken) => + { + var results = new List(); + foreach (var result in registeredAIAgents) + { + results.Add(new AgentDiscoveryCard + { + Name = result.Name!, + Description = result.Description, + }); + } + + return Results.Ok(results); + }) + .WithName("GetAgents"); } internal sealed class AgentDiscoveryCard diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj index 53fd4757ee..b4141ba166 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/AgentWebChat.AgentHost.csproj @@ -8,6 +8,7 @@ + diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index 46af2a5b19..7447c54aa1 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -5,6 +5,7 @@ using AgentWebChat.AgentHost; using AgentWebChat.AgentHost.Custom; using AgentWebChat.AgentHost.Utilities; using Microsoft.Agents.AI; +using Microsoft.Agents.AI.DevUI; using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; @@ -21,6 +22,13 @@ builder.Services.AddProblemDetails(); // Configure the chat model and our agent. builder.AddKeyedChatClient("chat-model"); +// Add DevUI services +builder.AddDevUI(); + +// Add OpenAI services +builder.AddOpenAIChatCompletions(); +builder.AddOpenAIResponses(); + var pirateAgentBuilder = builder.AddAIAgent( "pirate", instructions: "You are a pirate. Speak like a pirate", @@ -95,8 +103,48 @@ var scienceConcurrentWorkflow = builder.AddWorkflow("science-concurrent-workflow return AgentWorkflowBuilder.BuildConcurrent(workflowName: key, agents: agents); }).AddAsAIAgent(); -builder.AddOpenAIChatCompletions(); -builder.AddOpenAIResponses(); +builder.AddWorkflow("nonAgentWorkflow", (sp, key) => +{ + List usedAgents = [pirateAgentBuilder, chemistryAgent]; + var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService(ab.Name)); + return AgentWorkflowBuilder.BuildSequential(workflowName: key, agents: agents); +}); + +builder.Services.AddKeyedSingleton("NonAgentAndNonmatchingDINameWorkflow", (sp, key) => +{ + List usedAgents = [pirateAgentBuilder, chemistryAgent]; + var agents = usedAgents.Select(ab => sp.GetRequiredKeyedService(ab.Name)); + return AgentWorkflowBuilder.BuildSequential(workflowName: "random-name", agents: agents); +}); + +builder.Services.AddSingleton(sp => +{ + var chatClient = sp.GetRequiredKeyedService("chat-model"); + return new ChatClientAgent(chatClient, name: "default-agent", instructions: "you are a default agent."); +}); + +builder.Services.AddKeyedSingleton("my-di-nonmatching-agent", (sp, name) => +{ + var chatClient = sp.GetRequiredKeyedService("chat-model"); + return new ChatClientAgent( + chatClient, + name: "some-random-name", // demonstrating registration can be different for DI and actual agent + instructions: "you are a dependency inject agent. Tell me all about dependency injection."); +}); + +builder.Services.AddKeyedSingleton("my-di-matchingname-agent", (sp, name) => +{ + if (name is not string nameStr) + { + throw new NotSupportedException("Name should be passed as a key"); + } + + var chatClient = sp.GetRequiredKeyedService("chat-model"); + return new ChatClientAgent( + chatClient, + name: nameStr, // demonstrating registration with the same name + instructions: "you are a dependency inject agent. Tell me all about dependency injection."); +}); var app = builder.Build(); @@ -118,7 +166,10 @@ app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard // Url = "http://localhost:5390/a2a/knights-and-knaves" }); +app.MapDevUI(); + app.MapOpenAIResponses(); +app.MapOpenAIConversations(); app.MapOpenAIChatCompletions(pirateAgentBuilder); app.MapOpenAIChatCompletions(knightsKnavesAgentBuilder); diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs index a28b3e1902..328e3f5e83 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AppHost/Program.cs @@ -9,7 +9,9 @@ var azOpenAiResourceGroup = builder.AddParameterFromConfiguration("AzureOpenAIRe var chatModel = builder.AddAIModel("chat-model").AsAzureOpenAI("gpt-4o", o => o.AsExisting(azOpenAiResource, azOpenAiResourceGroup)); var agentHost = builder.AddProject("agenthost") - .WithReference(chatModel); + .WithHttpEndpoint(name: "devui") + .WithUrlForEndpoint("devui", (url) => new() { Url = "/devui", DisplayText = "Dev UI" }) + .WithReference(chatModel); builder.AddProject("webfrontend") .WithExternalHttpEndpoints() diff --git a/dotnet/samples/AzureFunctions/01_SingleAgent/README.md b/dotnet/samples/AzureFunctions/01_SingleAgent/README.md index 5727f037c2..d4ac968978 100644 --- a/dotnet/samples/AzureFunctions/01_SingleAgent/README.md +++ b/dotnet/samples/AzureFunctions/01_SingleAgent/README.md @@ -47,7 +47,7 @@ curl -X POST http://localhost:7071/api/agents/Joker/run \ To continue a conversation, include the `thread_id` in the query string or JSON body: ```bash -curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=@dafx-joker@your-thread-id" \ +curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{"message": "Tell me another one."}' @@ -64,7 +64,7 @@ The expected `application/json` output will look something like: ```json { "status": 200, - "thread_id": "@dafx-joker@your-thread-id", + "thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40", "response": { "Messages": [ { diff --git a/dotnet/samples/AzureFunctions/06_LongRunningTools/README.md b/dotnet/samples/AzureFunctions/06_LongRunningTools/README.md index 9c538298e8..54ed85060b 100644 --- a/dotnet/samples/AzureFunctions/06_LongRunningTools/README.md +++ b/dotnet/samples/AzureFunctions/06_LongRunningTools/README.md @@ -52,7 +52,7 @@ The response will be a text string that looks something like the following, indi ```http HTTP/1.1 200 OK Content-Type: text/plain -x-ms-thread-id: @publisher@351ec855-7f4d-4527-a60d-498301ced36d +x-ms-thread-id: 351ec855-7f4d-4527-a60d-498301ced36d The content generation workflow for the topic "The Future of Artificial Intelligence" has been successfully started, and the instance ID is **6a04276e8d824d8d941e1dc4142cc254**. If you need any further assistance or updates on the workflow, feel free to ask! ``` diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/Program.cs index c51d345c11..3c374d799f 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.1_Basics/Program.cs @@ -11,14 +11,15 @@ using Microsoft.Extensions.AI; string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; -const string JokerInstructions = "You are good at telling jokes."; +const string JokerInstructionsV1 = "You are good at telling jokes."; +const string JokerInstructionsV2 = "You are extremely hilarious at telling jokes."; const string JokerName = "JokerAgent"; // Get a client to create/retrieve/delete server side agents with Azure Foundry Agents. AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); // Define the agent you want to create. (Prompt Agent in this case) -AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructions }); +AgentVersionCreationOptions options = new(new PromptAgentDefinition(model: deploymentName) { Instructions = JokerInstructionsV1 }); // Azure.AI.Agents SDK creates and manages agent by name and versions. // You can create a server side agent version with the Azure.AI.Agents SDK client below. @@ -32,8 +33,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: // You can retrieve an AIAgent for an already created server side agent version. AIAgent jokerAgentV1 = aiProjectClient.GetAIAgent(agentVersion); -// You can also create another AIAgent version (V2) by providing the same name with a different definition. -AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructions + "V2"); +// You can also create another AIAgent version (V2) by providing the same name with a different definition/instruction. +AIAgent jokerAgentV2 = aiProjectClient.CreateAIAgent(name: JokerName, model: deploymentName, instructions: JokerInstructionsV2); // You can also get the AIAgent latest version by just providing its name. AIAgent jokerAgentLatest = aiProjectClient.GetAIAgent(name: JokerName); @@ -43,11 +44,7 @@ AgentVersion latestVersion = jokerAgentLatest.GetService()!; Console.WriteLine($"Latest agent version id: {latestVersion.Id}"); // Once you have the AIAgent, you can invoke it like any other AIAgent. -AgentThread thread = jokerAgentLatest.GetNewThread(); -Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.", thread)); - -// This will use the same thread to continue the conversation. -Console.WriteLine(await jokerAgentLatest.RunAsync("Now tell me a joke about a cat and a dog using last joke as the anchor.", thread)); +Console.WriteLine(await jokerAgentLatest.RunAsync("Tell me a joke about a pirate.")); // Cleanup by agent name removes both agent versions created (jokerAgentV1 + jokerAgentV2). await aiProjectClient.Agents.DeleteAgentAsync(jokerAgentV1.Name); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/Program.cs index bd5620ff1f..4d840d54ff 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step01.2_Running/Program.cs @@ -26,13 +26,8 @@ AgentVersion agentVersion = aiProjectClient.Agents.CreateAgentVersion(agentName: // You can retrieve an AIAgent for a already created server side agent version. AIAgent jokerAgent = aiProjectClient.GetAIAgent(agentVersion); -// Invoke the agent and output the text result. -AgentThread thread = jokerAgent.GetNewThread(); -Console.WriteLine(await jokerAgent.RunAsync("Tell me a joke about a pirate.", thread)); - // Invoke the agent with streaming support. -thread = jokerAgent.GetNewThread(); -await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.", thread)) +await foreach (AgentRunResponseUpdate update in jokerAgent.RunStreamingAsync("Tell me a joke about a pirate.")) { Console.WriteLine(update); } diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI.csproj b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI.csproj deleted file mode 100644 index 90bc94a20f..0000000000 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - Exe - net9.0 - - enable - enable - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json deleted file mode 100644 index 84715914da..0000000000 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/OpenAPISpec.json +++ /dev/null @@ -1,354 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Github Versions API", - "version": "1.0.0" - }, - "servers": [ - { - "url": "https://api.github.com" - } - ], - "components": { - "schemas": { - "basic-error": { - "title": "Basic Error", - "description": "Basic Error", - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "documentation_url": { - "type": "string" - }, - "url": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "label": { - "title": "Label", - "description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).", - "type": "object", - "properties": { - "id": { - "description": "Unique identifier for the label.", - "type": "integer", - "format": "int64", - "example": 208045946 - }, - "node_id": { - "type": "string", - "example": "MDU6TGFiZWwyMDgwNDU5NDY=" - }, - "url": { - "description": "URL for the label", - "example": "https://api.github.com/repositories/42/labels/bug", - "type": "string", - "format": "uri" - }, - "name": { - "description": "The name of the label.", - "example": "bug", - "type": "string" - }, - "description": { - "description": "Optional description of the label, such as its purpose.", - "type": "string", - "example": "Something isn't working", - "nullable": true - }, - "color": { - "description": "6-character hex code, without the leading #, identifying the color", - "example": "FFFFFF", - "type": "string" - }, - "default": { - "description": "Whether this label comes by default in a new repository.", - "type": "boolean", - "example": true - } - }, - "required": [ - "id", - "node_id", - "url", - "name", - "description", - "color", - "default" - ] - }, - "tag": { - "title": "Tag", - "description": "Tag", - "type": "object", - "properties": { - "name": { - "type": "string", - "example": "v0.1" - }, - "commit": { - "type": "object", - "properties": { - "sha": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "required": [ - "sha", - "url" - ] - }, - "zipball_url": { - "type": "string", - "format": "uri", - "example": "https://github.com/octocat/Hello-World/zipball/v0.1" - }, - "tarball_url": { - "type": "string", - "format": "uri", - "example": "https://github.com/octocat/Hello-World/tarball/v0.1" - }, - "node_id": { - "type": "string" - } - }, - "required": [ - "name", - "node_id", - "commit", - "zipball_url", - "tarball_url" - ] - } - }, - "examples": { - "label-items": { - "value": [ - { - "id": 208045946, - "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", - "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", - "name": "bug", - "description": "Something isn't working", - "color": "f29513", - "default": true - }, - { - "id": 208045947, - "node_id": "MDU6TGFiZWwyMDgwNDU5NDc=", - "url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement", - "name": "enhancement", - "description": "New feature or request", - "color": "a2eeef", - "default": false - } - ] - }, - "tag-items": { - "value": [ - { - "name": "v0.1", - "commit": { - "sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc", - "url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc" - }, - "zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1", - "tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1", - "node_id": "MDQ6VXNlcjE=" - } - ] - } - }, - "parameters": { - "owner": { - "name": "owner", - "description": "The account owner of the repository. The name is not case sensitive.", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - "repo": { - "name": "repo", - "description": "The name of the repository without the `.git` extension. The name is not case sensitive.", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - "per-page": { - "name": "per_page", - "description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"", - "in": "query", - "schema": { - "type": "integer", - "default": 30 - } - }, - "page": { - "name": "page", - "description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - } - }, - "responses": { - "not_found": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/basic-error" - } - } - } - } - }, - "headers": { - "link": { - "example": "; rel=\"next\", ; rel=\"last\"", - "schema": { - "type": "string" - } - } - } - }, - "paths": { - "/repos/{owner}/{repo}/tags": { - "get": { - "summary": "List repository tags", - "description": "", - "tags": [ - "repos" - ], - "operationId": "repos/list-tags", - "externalDocs": { - "description": "API method documentation", - "url": "https://docs.github.com/rest/repos/repos#list-repository-tags" - }, - "parameters": [ - { - "$ref": "#/components/parameters/owner" - }, - { - "$ref": "#/components/parameters/repo" - }, - { - "$ref": "#/components/parameters/per-page" - }, - { - "$ref": "#/components/parameters/page" - } - ], - "responses": { - "200": { - "description": "Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/tag" - } - }, - "examples": { - "default": { - "$ref": "#/components/examples/tag-items" - } - } - } - }, - "headers": { - "Link": { - "$ref": "#/components/headers/link" - } - } - } - }, - "x-github": { - "githubCloudOnly": false, - "enabledForGitHubApps": true, - "category": "repos", - "subcategory": "repos" - } - } - }, - "/repos/{owner}/{repo}/labels": { - "get": { - "summary": "List labels for a repository", - "description": "Lists all labels for a repository.", - "tags": [ - "issues" - ], - "operationId": "issues/list-labels-for-repo", - "externalDocs": { - "description": "API method documentation", - "url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository" - }, - "parameters": [ - { - "$ref": "#/components/parameters/owner" - }, - { - "$ref": "#/components/parameters/repo" - }, - { - "$ref": "#/components/parameters/per-page" - }, - { - "$ref": "#/components/parameters/page" - } - ], - "responses": { - "200": { - "description": "Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/label" - } - }, - "examples": { - "default": { - "$ref": "#/components/examples/label-items" - } - } - } - }, - "headers": { - "Link": { - "$ref": "#/components/headers/link" - } - } - }, - "404": { - "$ref": "#/components/responses/not_found" - } - }, - "x-github": { - "githubCloudOnly": false, - "enabledForGitHubApps": true, - "category": "issues", - "subcategory": "labels" - } - } - } - } -} \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs deleted file mode 100644 index f9540d1725..0000000000 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/Program.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -// This sample demonstrates how to use an agent with function tools provided via an OpenAPI spec. -// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent. - -using Azure.AI.Projects; -using Azure.Identity; -using Microsoft.Agents.AI; -using Microsoft.Extensions.AI; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Plugins.OpenApi; - -string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini"; - -// Load the OpenAPI Spec from a file. -KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json"); - -// Convert the Semantic Kernel plugin to Agent Framework function tools. -// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one. -Kernel kernel = new(); -List tools = plugin.Select(x => x.WithKernel(kernel)).Cast().ToList(); - -const string AssistantInstructions = "You are a helpful assistant that can query GitHub repositories."; -const string AssistantName = "GitHubAssistant"; - -// Get a client to create/retrieve/delete server side agents with Azure Foundry Agents. -AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); - -// Create AIAgent directly -AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: tools); - -// Run the agent with the OpenAPI function tools. -AgentThread thread = agent.GetNewThread(); -Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github.", thread)); - -// Cleanup by agent name removes the agent version created. -await aiProjectClient.Agents.DeleteAgentAsync(agent.Name); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/README.md b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/README.md deleted file mode 100644 index 68096e495f..0000000000 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Using Function Tools from OpenAPI Specifications - -This sample demonstrates how to create function tools from an OpenAPI specification and use them with AI agents. - -## What this sample demonstrates - -- Loading OpenAPI specifications from files -- Converting OpenAPI specifications to Semantic Kernel plugins -- Converting Semantic Kernel plugins to AI function tools -- Using OpenAPI-based function tools with AI agents -- Managing agent lifecycle (creation and deletion) - -## Prerequisites - -Before you begin, ensure you have the following prerequisites: - -- .NET 8.0 SDK or later -- Azure Foundry service endpoint and deployment configured -- Azure CLI installed and authenticated (for Azure credential authentication) - -**Note**: This demo uses Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). - -Set the following environment variables: - -```powershell -$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint -$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini -``` - -## Run the sample - -Navigate to the FoundryAgents sample directory and run: - -```powershell -cd dotnet/samples/GettingStarted/FoundryAgents -dotnet run --project .\FoundryAgents_Step03.2_UsingFunctionTools_FromOpenAPI -``` - -## Expected behavior - -The sample will: - -1. Load the OpenAPI specification from OpenAPISpec.json (GitHub API) -2. Convert the OpenAPI spec to Semantic Kernel plugins -3. Create an agent named "GitHubAssistant" with the OpenAPI-based function tools -4. Run the agent with a prompt to query GitHub repositories -5. The agent will invoke the appropriate OpenAPI function tools to retrieve data -6. Clean up resources by deleting the agent - diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/FoundryAgents_Step03.1_UsingFunctionTools.csproj b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj similarity index 100% rename from dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/FoundryAgents_Step03.1_UsingFunctionTools.csproj rename to dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/FoundryAgents_Step03_UsingFunctionTools.csproj diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs similarity index 59% rename from dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/Program.cs rename to dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs index e2af503905..38c5a15d75 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/Program.cs @@ -26,18 +26,26 @@ AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential( AITool tool = AIFunctionFactory.Create(GetWeather); // Create AIAgent directly -AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]); +var newAgent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [tool]); + +// Getting an already existing agent by name with tools. +/* + * IMPORTANT: Since agents that are stored in the server only know the definition of the function tools (JSON Schema), + * you need to provided all invocable function tools when retrieving the agent so it can invoke them automatically. + * If no invocable tools are provided, the function calling needs to handled manually. + */ +var existingAgent = await aiProjectClient.GetAIAgentAsync(name: AssistantName, tools: [tool]); // Non-streaming agent interaction with function tools. -AgentThread thread = agent.GetNewThread(); -Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?", thread)); +AgentThread thread = existingAgent.GetNewThread(); +Console.WriteLine(await existingAgent.RunAsync("What is the weather like in Amsterdam?", thread)); // Streaming agent interaction with function tools. -thread = agent.GetNewThread(); -await foreach (AgentRunResponseUpdate update in agent.RunStreamingAsync("What is the weather like in Amsterdam?", thread)) +thread = existingAgent.GetNewThread(); +await foreach (AgentRunResponseUpdate update in existingAgent.RunStreamingAsync("What is the weather like in Amsterdam?", thread)) { Console.WriteLine(update); } // Cleanup by agent name removes the agent version created. -await aiProjectClient.Agents.DeleteAgentAsync(agent.Name); +await aiProjectClient.Agents.DeleteAgentAsync(existingAgent.Name); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/README.md b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/README.md similarity index 100% rename from dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03.1_UsingFunctionTools/README.md rename to dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step03_UsingFunctionTools/README.md diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs index bd72a2c940..1b51d210cf 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step04_UsingFunctionToolsWithApprovals/Program.cs @@ -25,7 +25,7 @@ const string AssistantName = "WeatherAssistant"; // Get a client to create/retrieve/delete server side agents with Azure Foundry Agents. AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); -ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather)); +ApprovalRequiredAIFunction approvalTool = new(AIFunctionFactory.Create(GetWeather, name: nameof(GetWeather))); // Create AIAgent directly AIAgent agent = await aiProjectClient.CreateAIAgentAsync(name: AssistantName, model: deploymentName, instructions: AssistantInstructions, tools: [approvalTool]); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Assets/walkway.jpg b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Assets/walkway.jpg new file mode 100644 index 0000000000..13ef1e1840 Binary files /dev/null and b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Assets/walkway.jpg differ diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj index d0cd81c352..1d423b22bc 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/FoundryAgents_Step10_UsingImages.csproj @@ -16,5 +16,11 @@ + + + + Always + + diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs index 52ee408a25..a799fe46fb 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step10_UsingImages/Program.cs @@ -8,7 +8,7 @@ using Microsoft.Agents.AI; using Microsoft.Extensions.AI; string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); -string deploymentName = System.Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o"; +string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o"; const string VisionInstructions = "You are a helpful agent that can analyze images"; const string VisionName = "VisionAgent"; @@ -21,7 +21,7 @@ AIAgent agent = aiProjectClient.CreateAIAgent(name: VisionName, model: deploymen ChatMessage message = new(ChatRole.User, [ new TextContent("What do you see in this image?"), - new UriContent("https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", "image/jpeg") + new DataContent(File.ReadAllBytes("assets/walkway.jpg"), "image/jpeg") ]); AgentThread thread = agent.GetNewThread(); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/Program.cs index 0a70ca76df..0f6f6ef2d9 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step14_CodeInterpreter/Program.cs @@ -55,7 +55,7 @@ AgentRunResponse response = await agentOption1.RunAsync("I need to solve the equ // AgentRunResponse response = await agentOption2.RunAsync("I need to solve the equation sin(x) + x^2 = 42"); // Get the CodeInterpreterToolCallContent -CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType().SingleOrDefault(); +CodeInterpreterToolCallContent? toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType().FirstOrDefault(); if (toolCallContent?.Inputs is not null) { DataContent? codeInput = toolCallContent.Inputs.OfType().FirstOrDefault(); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs index cb621275ea..05fb39bbf4 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/Program.cs @@ -15,8 +15,8 @@ internal sealed class Program { private static async Task Main(string[] args) { - string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); - string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "computer-use-preview"; + string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set."); + string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "computer-use-preview"; // Get a client to create/retrieve/delete server side agents with Azure Foundry Agents. AIProjectClient aiProjectClient = new(new Uri(endpoint), new AzureCliCredential()); diff --git a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/README.md b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/README.md index 6b7f27aaf3..94ddf6b269 100644 --- a/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/README.md +++ b/dotnet/samples/GettingStarted/FoundryAgents/FoundryAgents_Step15_ComputerUse/README.md @@ -24,8 +24,8 @@ Before you begin, ensure you have the following prerequisites: Set the following environment variables: ```powershell -$env:AZURE_OPENAI_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint -$env:AZURE_OPENAI_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint +$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="computer-use-preview" # Optional, defaults to computer-use-preview ``` ## Run the sample diff --git a/dotnet/samples/GettingStarted/FoundryAgents/README.md b/dotnet/samples/GettingStarted/FoundryAgents/README.md new file mode 100644 index 0000000000..c5d027d8d7 --- /dev/null +++ b/dotnet/samples/GettingStarted/FoundryAgents/README.md @@ -0,0 +1,82 @@ +# Getting started with Foundry Agents + +The getting started with Foundry Agents samples demonstrate the fundamental concepts and functionalities +of Azure Foundry Agents and can be used with Azure Foundry as the AI provider. + +These samples showcase how to work with agents managed through Azure Foundry, including agent creation, +versioning, multi-turn conversations, and advanced features like code interpretation and computer use. + +## Getting started with Foundry Agents prerequisites + +Before you begin, ensure you have the following prerequisites: + +- .NET 8.0 SDK or later +- Azure Foundry service endpoint and project configured +- Azure CLI installed and authenticated (for Azure credential authentication) + +**Note**: These samples use Azure Foundry Agents. For more information, see [Azure AI Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/). + +**Note**: These samples use Azure CLI credentials for authentication. Make sure you're logged in with `az login` and have access to the Azure Foundry resource. For more information, see the [Azure CLI documentation](https://learn.microsoft.com/cli/azure/authenticate-azure-cli-interactively). + +## Samples + +|Sample|Description| +|---|---| +|[Basics](./FoundryAgents_Step01.1_Basics/)|This sample demonstrates how to create and manage AI agents with versioning| +|[Running a simple agent](./FoundryAgents_Step01.2_Running/)|This sample demonstrates how to create and run a basic Foundry agent| +|[Multi-turn conversation](./FoundryAgents_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a Foundry agent| +|[Using function tools](./FoundryAgents_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a Foundry agent| +|[Using function tools with approvals](./FoundryAgents_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution| +|[Structured output](./FoundryAgents_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a Foundry agent| +|[Persisted conversations](./FoundryAgents_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later| +|[Observability](./FoundryAgents_Step07_Observability/)|This sample demonstrates how to add telemetry to a Foundry agent| +|[Dependency injection](./FoundryAgents_Step08_DependencyInjection/)|This sample demonstrates how to add and resolve a Foundry agent with a dependency injection container| +|[Using MCP client as tools](./FoundryAgents_Step09_UsingMcpClientAsTools/)|This sample demonstrates how to use MCP clients as tools with a Foundry agent| +|[Using images](./FoundryAgents_Step10_UsingImages/)|This sample demonstrates how to use image multi-modality with a Foundry agent| +|[Exposing as a function tool](./FoundryAgents_Step11_AsFunctionTool/)|This sample demonstrates how to expose a Foundry agent as a function tool| +|[Using middleware](./FoundryAgents_Step12_Middleware/)|This sample demonstrates how to use middleware with a Foundry agent| +|[Using plugins](./FoundryAgents_Step13_Plugins/)|This sample demonstrates how to use plugins with a Foundry agent| +|[Code interpreter](./FoundryAgents_Step14_CodeInterpreter/)|This sample demonstrates how to use the code interpreter tool with a Foundry agent| +|[Computer use](./FoundryAgents_Step15_ComputerUse/)|This sample demonstrates how to use computer use capabilities with a Foundry agent| + +## Running the samples from the console + +To run the samples, navigate to the desired sample directory, e.g. + +```powershell +cd FoundryAgents_Step01.2_Running +``` + +Set the following environment variables: + +```powershell +$env:AZURE_FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Azure Foundry resource endpoint +$env:AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME="gpt-4o-mini" # Optional, defaults to gpt-4o-mini +``` + +If the variables are not set, you will be prompted for the values when running the samples. + +Execute the following command to build the sample: + +```powershell +dotnet build +``` + +Execute the following command to run the sample: + +```powershell +dotnet run --no-build +``` + +Or just build and run in one step: + +```powershell +dotnet run +``` + +## Running the samples from Visual Studio + +Open the solution in Visual Studio and set the desired sample project as the startup project. Then, run the project using the built-in debugger or by pressing `F5`. + +You will be prompted for any required environment variables if they are not already set. + diff --git a/dotnet/samples/GettingStarted/README.md b/dotnet/samples/GettingStarted/README.md index 4fdf0a3d7f..4e349c7742 100644 --- a/dotnet/samples/GettingStarted/README.md +++ b/dotnet/samples/GettingStarted/README.md @@ -8,6 +8,7 @@ of the agent framework. |Sample|Description| |---|---| |[Agents](./Agents/README.md)|Step by step instructions for getting started with agents| +|[Foundry Agents](./FoundryAgents/README.md)|Getting started with Azure Foundry Agents| |[Agent Providers](./AgentProviders/README.md)|Getting started with creating agents using various providers| |[Agents With Retrieval Augmented Generation (RAG)](./AgentWithRAG/README.md)|Adding Retrieval Augmented Generation (RAG) capabilities to your agents.| |[Agents With Memory](./AgentWithMemory/README.md)|Adding Memory capabilities to your agents.| diff --git a/dotnet/samples/M365Agent/AFAgentApplication.cs b/dotnet/samples/M365Agent/AFAgentApplication.cs new file mode 100644 index 0000000000..04aabf96a8 --- /dev/null +++ b/dotnet/samples/M365Agent/AFAgentApplication.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json; +using AdaptiveCards; +using M365Agent.Agents; +using Microsoft.Agents.AI; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Builder.App; +using Microsoft.Agents.Builder.State; +using Microsoft.Agents.Core.Models; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; + +namespace M365Agent; + +/// +/// An adapter class that exposes a Microsoft Agent Framework as a M365 Agent SDK . +/// +internal sealed class AFAgentApplication : AgentApplication +{ + private readonly AIAgent _agent; + private readonly string? _welcomeMessage; + + public AFAgentApplication(AIAgent agent, AgentApplicationOptions options, [FromKeyedServices("AFAgentApplicationWelcomeMessage")] string? welcomeMessage = null) : base(options) + { + this._agent = agent; + this._welcomeMessage = welcomeMessage; + + this.OnConversationUpdate(ConversationUpdateEvents.MembersAdded, this.WelcomeMessageAsync); + this.OnActivity(ActivityTypes.Message, this.MessageActivityAsync, rank: RouteRank.Last); + } + + /// + /// The main agent invocation method, where each user message triggers a call to the underlying . + /// + private async Task MessageActivityAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + // Start a Streaming Process + await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Working on a response for you", cancellationToken); + + // Get the conversation history from turn state. + JsonElement threadElementStart = turnState.GetValue("conversation.chatHistory"); + + // Deserialize the conversation history into an AgentThread, or create a new one if none exists. + AgentThread agentThread = threadElementStart.ValueKind is not JsonValueKind.Undefined and not JsonValueKind.Null + ? this._agent.DeserializeThread(threadElementStart, JsonUtilities.DefaultOptions) + : this._agent.GetNewThread(); + + ChatMessage chatMessage = HandleUserInput(turnContext); + + // Invoke the WeatherForecastAgent to process the message + AgentRunResponse agentRunResponse = await this._agent.RunAsync(chatMessage, agentThread, cancellationToken: cancellationToken); + + // Check for any user input requests in the response + // and turn them into adaptive cards in the streaming response. + List? attachments = null; + HandleUserInputRequests(agentRunResponse, ref attachments); + + // Check for Adaptive Card content in the response messages + // and return them appropriately in the response. + var adaptiveCards = agentRunResponse.Messages.SelectMany(x => x.Contents).OfType().ToList(); + if (adaptiveCards.Count > 0) + { + attachments ??= []; + attachments.Add(new Attachment() + { + ContentType = "application/vnd.microsoft.card.adaptive", + Content = adaptiveCards.First().AdaptiveCardJson, + }); + } + else + { + turnContext.StreamingResponse.QueueTextChunk(agentRunResponse.Text); + } + + // If created any adaptive cards, add them to the final message. + if (attachments is not null) + { + turnContext.StreamingResponse.FinalMessage = MessageFactory.Attachment(attachments); + } + + // Serialize and save the updated conversation history back to turn state. + JsonElement threadElementEnd = agentThread.Serialize(JsonUtilities.DefaultOptions); + turnState.SetValue("conversation.chatHistory", threadElementEnd); + + // End the streaming response + await turnContext.StreamingResponse.EndStreamAsync(cancellationToken); + } + + /// + /// A method to show a welcome message when a new user joins the conversation. + /// + private async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(this._welcomeMessage)) + { + return; + } + + foreach (ChannelAccount member in turnContext.Activity.MembersAdded) + { + if (member.Id != turnContext.Activity.Recipient.Id) + { + await turnContext.SendActivityAsync(MessageFactory.Text(this._welcomeMessage), cancellationToken); + } + } + } + + /// + /// When a user responds to a function approval request by clicking on a card, this method converts the response + /// into the appropriate approval or rejection . + /// + /// The for the current turn. + /// The to pass to the . + private static ChatMessage HandleUserInput(ITurnContext turnContext) + { + // Check if this contains the function approval Adaptive Card response. + if (turnContext.Activity.Value is JsonElement valueElement + && valueElement.GetProperty("type").GetString() == "functionApproval" + && valueElement.GetProperty("approved") is JsonElement approvedJsonElement + && approvedJsonElement.ValueKind is JsonValueKind.True or JsonValueKind.False + && valueElement.GetProperty("requestJson") is JsonElement requestJsonElement + && requestJsonElement.ValueKind == JsonValueKind.String) + { + var requestContent = JsonSerializer.Deserialize(requestJsonElement.GetString()!, JsonUtilities.DefaultOptions); + + return new ChatMessage(ChatRole.User, [requestContent!.CreateResponse(approvedJsonElement.ValueKind == JsonValueKind.True)]); + } + + return new ChatMessage(ChatRole.User, turnContext.Activity.Text); + } + + /// + /// When the agent returns any user input requests, this method converts them into adaptive cards that + /// asks the user to approve or deny the requests. + /// + /// The that may contain the user input requests. + /// The list of to which the adaptive cards will be added. + private static void HandleUserInputRequests(AgentRunResponse response, ref List? attachments) + { + var userInputRequests = response.UserInputRequests.ToList(); + if (userInputRequests.Count > 0) + { + foreach (var functionApprovalRequest in userInputRequests.OfType()) + { + var functionApprovalRequestJson = JsonSerializer.Serialize(functionApprovalRequest, JsonUtilities.DefaultOptions); + + var card = new AdaptiveCard("1.5"); + card.Body.Add(new AdaptiveTextBlock + { + Text = "Function Call Approval Required", + Size = AdaptiveTextSize.Large, + Weight = AdaptiveTextWeight.Bolder, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center + }); + card.Body.Add(new AdaptiveTextBlock + { + Text = $"Function: {functionApprovalRequest.FunctionCall.Name}" + }); + card.Body.Add(new AdaptiveActionSet() + { + Actions = + [ + new AdaptiveSubmitAction + { + Id = "Approve", + Title = "Approve", + Data = new { type = "functionApproval", approved = true, requestJson = functionApprovalRequestJson } + }, + new AdaptiveSubmitAction + { + Id = "Deny", + Title = "Deny", + Data = new { type = "functionApproval", approved = false, requestJson = functionApprovalRequestJson } + } + ] + }); + + attachments ??= []; + attachments.Add(new Attachment() + { + ContentType = "application/vnd.microsoft.card.adaptive", + Content = card.ToJson(), + }); + } + } + } +} diff --git a/dotnet/samples/M365Agent/Agents/AdaptiveCardAIContent.cs b/dotnet/samples/M365Agent/Agents/AdaptiveCardAIContent.cs new file mode 100644 index 0000000000..9b1ebee662 --- /dev/null +++ b/dotnet/samples/M365Agent/Agents/AdaptiveCardAIContent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; +using AdaptiveCards; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace M365Agent.Agents; + +/// +/// An type allows an to return adaptive cards as part of its response messages. +/// +internal sealed class AdaptiveCardAIContent : AIContent +{ + public AdaptiveCardAIContent(AdaptiveCard adaptiveCard) + { + this.AdaptiveCard = adaptiveCard ?? throw new ArgumentNullException(nameof(adaptiveCard)); + } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + [JsonConstructor] + public AdaptiveCardAIContent(string adaptiveCardJson) + { + this.AdaptiveCardJson = adaptiveCardJson; + } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + + [JsonIgnore] + public AdaptiveCard AdaptiveCard { get; private set; } + + public string AdaptiveCardJson + { + get => this.AdaptiveCard.ToJson(); + set => this.AdaptiveCard = AdaptiveCard.FromJson(value).Card; + } +} diff --git a/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs new file mode 100644 index 0000000000..e133170b36 --- /dev/null +++ b/dotnet/samples/M365Agent/Agents/WeatherForecastAgent.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Text.Json; +using AdaptiveCards; +using Microsoft.Agents.AI; +using Microsoft.Extensions.AI; + +namespace M365Agent.Agents; + +/// +/// A weather forecasting agent. This agent wraps a and adds custom logic +/// to generate adaptive cards for weather forecasts and add these to the agent's response. +/// +public class WeatherForecastAgent : DelegatingAIAgent +{ + private const string AgentName = "WeatherForecastAgent"; + private const string AgentInstructions = """ + You are a friendly assistant that helps people find a weather forecast for a given location. + You may ask follow up questions until you have enough information to answer the customers question. + When answering with a weather forecast, fill out the weatherCard property with an adaptive card containing the weather information and + add some emojis to indicate the type of weather. + When answering with just text, fill out the context property with a friendly response. + """; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of for interacting with an LLM. + public WeatherForecastAgent(IChatClient chatClient) + : base(new ChatClientAgent( + chatClient: chatClient, + new ChatClientAgentOptions() + { + Name = AgentName, + Instructions = AgentInstructions, + ChatOptions = new ChatOptions() + { + Tools = [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(GetWeather))], + // We want the agent to return structured output in a known format + // so that we can easily create adaptive cards from the response. + ResponseFormat = ChatResponseFormat.ForJsonSchema( + schema: AIJsonUtilities.CreateJsonSchema(typeof(WeatherForecastAgentResponse)), + schemaName: "WeatherForecastAgentResponse", + schemaDescription: "Response to a query about the weather in a specified location"), + } + })) + { + } + + public override async Task RunAsync(IEnumerable messages, AgentThread? thread = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + { + var response = await base.RunAsync(messages, thread, options, cancellationToken); + + // If the agent returned a valid structured output response + // we might be able to enhance the response with an adaptive card. + if (response.TryDeserialize(JsonSerializerOptions.Web, out var structuredOutput)) + { + var textContentMessage = response.Messages.FirstOrDefault(x => x.Contents.OfType().Any()); + if (textContentMessage is not null) + { + // If the response contains weather information, create an adaptive card. + if (structuredOutput.ContentType == WeatherForecastAgentResponseContentType.WeatherForecastAgentResponse) + { + var card = CreateWeatherCard(structuredOutput.Location, structuredOutput.MeteorologicalCondition, structuredOutput.TemperatureInCelsius); + textContentMessage.Contents.Add(new AdaptiveCardAIContent(card)); + } + + // If the response is just text, replace the structured output with the text response. + if (structuredOutput.ContentType == WeatherForecastAgentResponseContentType.OtherAgentResponse) + { + var textContent = textContentMessage.Contents.OfType().First(); + textContent.Text = structuredOutput.OtherResponse; + } + } + } + + return response; + } + + /// + /// A mock weather tool, to get weather information for a given location. + /// + [Description("Get the weather for a given location.")] + private 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."; + + /// + /// Create an adaptive card to display weather information. + /// + private static AdaptiveCard CreateWeatherCard(string? location, string? condition, string? temperature) + { + var card = new AdaptiveCard("1.5"); + card.Body.Add(new AdaptiveTextBlock + { + Text = "🌤️ Weather Forecast 🌤️", + Size = AdaptiveTextSize.Large, + Weight = AdaptiveTextWeight.Bolder, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center + }); + card.Body.Add(new AdaptiveTextBlock + { + Text = "Location: " + location, + }); + card.Body.Add(new AdaptiveTextBlock + { + Text = "Condition: " + condition, + }); + card.Body.Add(new AdaptiveTextBlock + { + Text = "Temperature: " + temperature, + }); + return card; + } +} diff --git a/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponse.cs b/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponse.cs new file mode 100644 index 0000000000..e5e15dffd4 --- /dev/null +++ b/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponse.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace M365Agent.Agents; + +/// +/// The structured output type for the . +/// +internal sealed class WeatherForecastAgentResponse +{ + /// + /// A value indicating whether the response contains a weather forecast or some other type of response. + /// + [JsonPropertyName("contentType")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WeatherForecastAgentResponseContentType ContentType { get; set; } + + /// + /// If the agent could not provide a weather forecast this should contain a textual response. + /// + [Description("If the answer is other agent response, contains the textual agent response.")] + [JsonPropertyName("otherResponse")] + public string? OtherResponse { get; set; } + + /// + /// The location for which the weather forecast is given. + /// + [Description("If the answer is a weather forecast, contains the location for which the forecast is given.")] + [JsonPropertyName("location")] + public string? Location { get; set; } + + /// + /// The temperature in Celsius for the given location. + /// + [Description("If the answer is a weather forecast, contains the temperature in Celsius.")] + [JsonPropertyName("temperatureInCelsius")] + public string? TemperatureInCelsius { get; set; } + + /// + /// The meteorological condition for the given location. + /// + [Description("If the answer is a weather forecast, contains the meteorological condition (e.g., Sunny, Rainy).")] + [JsonPropertyName("meteorologicalCondition")] + public string? MeteorologicalCondition { get; set; } +} diff --git a/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponseContentType.cs b/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponseContentType.cs new file mode 100644 index 0000000000..cd888d0a0c --- /dev/null +++ b/dotnet/samples/M365Agent/Agents/WeatherForecastAgentResponseContentType.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text.Json.Serialization; + +namespace M365Agent.Agents; + +/// +/// The type of content contained in a . +/// +internal enum WeatherForecastAgentResponseContentType +{ + [JsonPropertyName("otherAgentResponse")] + OtherAgentResponse, + + [JsonPropertyName("weatherForecastAgentResponse")] + WeatherForecastAgentResponse +} diff --git a/dotnet/samples/M365Agent/Auth/AspNetExtensions.cs b/dotnet/samples/M365Agent/Auth/AspNetExtensions.cs new file mode 100644 index 0000000000..1452c5f05e --- /dev/null +++ b/dotnet/samples/M365Agent/Auth/AspNetExtensions.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Concurrent; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Microsoft.Agents.Authentication; +using Microsoft.Agents.Core; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Validators; + +namespace M365Agent; + +internal static class AspNetExtensions +{ + private static readonly CompositeFormat s_cachedValidTokenIssuerUrlTemplateV1Format = CompositeFormat.Parse(AuthenticationConstants.ValidTokenIssuerUrlTemplateV1); + private static readonly CompositeFormat s_cachedValidTokenIssuerUrlTemplateV2Format = CompositeFormat.Parse(AuthenticationConstants.ValidTokenIssuerUrlTemplateV2); + + private static readonly ConcurrentDictionary> s_openIdMetadataCache = new(); + + /// + /// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent using settings in configuration. + /// + /// The service collection to resolve dependencies. + /// Used to read configuration settings. + /// Name of the config section to read. + /// + /// This extension reads settings from configuration. If configuration is missing JWT token + /// is not enabled. + ///

The minimum, but typical, configuration is:

+ /// + /// "TokenValidation": { + /// "Enabled": boolean, + /// "Audiences": [ + /// "{{ClientId}}" // this is the Client ID used for the Azure Bot + /// ], + /// "TenantId": "{{TenantId}}" + /// } + /// + /// The full options are: + /// + /// "TokenValidation": { + /// "Enabled": boolean, + /// "Audiences": [ + /// "{required:agent-appid}" + /// ], + /// "TenantId": "{recommended:tenant-id}", + /// "ValidIssuers": [ + /// "{default:Public-AzureBotService}" + /// ], + /// "IsGov": {optional:false}, + /// "AzureBotServiceOpenIdMetadataUrl": optional, + /// "OpenIdMetadataUrl": optional, + /// "AzureBotServiceTokenHandling": "{optional:true}" + /// "OpenIdMetadataRefresh": "optional-12:00:00" + /// } + /// + ///
+ public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation") + { + IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName); + + if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true)) + { + // Noop if TokenValidation section missing or disabled. + System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled"); + return; + } + + services.AddAgentAspNetAuthentication(tokenValidationSection.Get()!); + } + + /// + /// Adds AspNet token validation typical for ABS/SMBA and agent-to-agent. + /// + public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions) + { + AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions)); + + // Must have at least one Audience. + if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId"); + } + + // Audience values must be GUID's + foreach (var audience in validationOptions.Audiences) + { + if (!Guid.TryParse(audience, out _)) + { + throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID"); + } + } + + // If ValidIssuers is empty, default for ABS Public Cloud + if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0) + { + validationOptions.ValidIssuers = + [ + "https://api.botframework.com", + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", + "https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/", + "https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0", + ]; + + if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _)) + { + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, s_cachedValidTokenIssuerUrlTemplateV1Format, validationOptions.TenantId)); + validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, s_cachedValidTokenIssuerUrlTemplateV2Format, validationOptions.TenantId)); + } + } + + // If the `AzureBotServiceOpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate ABS tokens. + if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl)) + { + validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl; + } + + // If the `OpenIdMetadataUrl` setting is not specified, use the default based on `IsGov`. This is what is used to authenticate Entra ID tokens. + if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl)) + { + validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl; + } + + var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval; + + _ = services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ClockSkew = TimeSpan.FromMinutes(5), + ValidIssuers = validationOptions.ValidIssuers, + ValidAudiences = validationOptions.Audiences, + ValidateIssuerSigningKey = true, + RequireSignedTokens = true, + }; + + // Using Microsoft.IdentityModel.Validators + options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation(); + + options.Events = new JwtBearerEvents + { + // Create a ConfigurationManager based on the requestor. This is to handle ABS non-Entra tokens. + OnMessageReceived = async context => + { + string authorizationHeader = context.Request.Headers.Authorization.ToString(); + + if (string.IsNullOrWhiteSpace(authorizationHeader)) + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + string[] parts = authorizationHeader.Split(' ')!; + if (parts.Length != 2 || parts[0] != "Bearer") + { + // Default to AadTokenValidation handling + context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager; + await Task.CompletedTask.ConfigureAwait(false); + return; + } + + JwtSecurityToken token = new(parts[1]); + string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!; + + string openIdMetadataUrl = (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer, StringComparison.Ordinal)) + ? validationOptions.AzureBotServiceOpenIdMetadataUrl + : validationOptions.OpenIdMetadataUrl; + + context.Options.TokenValidationParameters.ConfigurationManager = s_openIdMetadataCache.GetOrAdd(openIdMetadataUrl, key => + { + return new ConfigurationManager(openIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient()) + { + AutomaticRefreshInterval = openIdMetadataRefresh + }; + }); + + await Task.CompletedTask.ConfigureAwait(false); + }, + + OnTokenValidated = context => Task.CompletedTask, + OnForbidden = context => Task.CompletedTask, + OnAuthenticationFailed = context => Task.CompletedTask + }; + }); + } +} diff --git a/dotnet/samples/M365Agent/Auth/TokenValidationOptions.cs b/dotnet/samples/M365Agent/Auth/TokenValidationOptions.cs new file mode 100644 index 0000000000..f8f2fa2e08 --- /dev/null +++ b/dotnet/samples/M365Agent/Auth/TokenValidationOptions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.Authentication; + +namespace M365Agent; + +internal sealed class TokenValidationOptions +{ + /// + /// The list of audiences to validate against. + /// + public IList? Audiences { get; set; } + + /// + /// TenantId of the Azure Bot. Optional but recommended. + /// + public string? TenantId { get; set; } + + /// + /// Additional valid issuers. Optional, in which case the Public Azure Bot Service issuers are used. + /// + public IList? ValidIssuers { get; set; } + + /// + /// Can be omitted, in which case public Azure Bot Service and Azure Cloud metadata urls are used. + /// + public bool IsGov { get; set; } + + /// + /// Azure Bot Service OpenIdMetadataUrl. Optional, in which case default value depends on IsGov. + /// + /// + /// + public string? AzureBotServiceOpenIdMetadataUrl { get; set; } + + /// + /// Entra OpenIdMetadataUrl. Optional, in which case default value depends on IsGov. + /// + /// + /// + public string? OpenIdMetadataUrl { get; set; } + + /// + /// Determines if Azure Bot Service tokens are handled. Defaults to true and should always be true until Azure Bot Service sends Entra ID token. + /// + public bool AzureBotServiceTokenHandling { get; set; } = true; + + /// + /// OpenIdMetadata refresh interval. Defaults to 12 hours. + /// + public TimeSpan? OpenIdMetadataRefresh { get; set; } +} diff --git a/dotnet/samples/M365Agent/JsonUtilities.cs b/dotnet/samples/M365Agent/JsonUtilities.cs new file mode 100644 index 0000000000..c87367e65b --- /dev/null +++ b/dotnet/samples/M365Agent/JsonUtilities.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using M365Agent.Agents; +using Microsoft.Extensions.AI; + +namespace M365Agent; + +/// Provides a collection of utility methods for working with JSON data in the context of the application. +internal static partial class JsonUtilities +{ + /// + /// Gets the singleton used as the default in JSON serialization operations. + /// + /// + /// + /// For Native AOT or applications disabling , this instance + /// includes source generated contracts for all common exchange types contained in this library. + /// + /// + /// It additionally turns on the following settings: + /// + /// Enables defaults. + /// Enables as the default ignore condition for properties. + /// Enables as the default number handling for number types. + /// + /// + /// + public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions(); + + /// + /// Creates default options to use for agents-related serialization. + /// + /// The configured options. + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Converter is guarded by IsReflectionEnabledByDefault check.")] + private static JsonSerializerOptions CreateDefaultOptions() + { + // Copy the configuration from the source generated context. + JsonSerializerOptions options = new(JsonContext.Default.Options) + { + // Chain in the resolvers from both AgentAbstractionsJsonUtilities and our source generated context. + // We want AgentAbstractionsJsonUtilities first to ensure any M.E.AI types are handled via its resolver. + TypeInfoResolver = JsonTypeInfoResolver.Combine(AIJsonUtilities.DefaultOptions.TypeInfoResolver, JsonContext.Default), + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // same as in AgentAbstractionsJsonUtilities and AIJsonUtilities + }; + options.AddAIContentType(typeDiscriminatorId: "adaptiveCard"); + + if (JsonSerializer.IsReflectionEnabledByDefault) + { + options.Converters.Add(new JsonStringEnumConverter()); + } + + options.MakeReadOnly(); + return options; + } + + // Keep in sync with CreateDefaultOptions above. + [JsonSourceGenerationOptions(JsonSerializerDefaults.Web, + UseStringEnumConverter = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString)] + + // M365Agent specific types + [JsonSerializable(typeof(AdaptiveCardAIContent))] + + [ExcludeFromCodeCoverage] + internal sealed partial class JsonContext : JsonSerializerContext; +} diff --git a/dotnet/samples/M365Agent/M365Agent.csproj b/dotnet/samples/M365Agent/M365Agent.csproj new file mode 100644 index 0000000000..9beff68dc7 --- /dev/null +++ b/dotnet/samples/M365Agent/M365Agent.csproj @@ -0,0 +1,30 @@ + + + + Exe + net9.0 + enable + enable + b842df34-390f-490d-9dc0-73909363ad16 + $(NoWarn);CA1812 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dotnet/samples/M365Agent/Program.cs b/dotnet/samples/M365Agent/Program.cs new file mode 100644 index 0000000000..834ac2180a --- /dev/null +++ b/dotnet/samples/M365Agent/Program.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +// Sample that shows how to create an Agent Framework agent that is hosted using the M365 Agent SDK. +// The agent can then be consumed from various M365 channels. +// See the README.md for more information. + +using Azure.AI.OpenAI; +using Azure.Identity; +using M365Agent; +using M365Agent.Agents; +using Microsoft.Agents.AI; +using Microsoft.Agents.Builder; +using Microsoft.Agents.Hosting.AspNetCore; +using Microsoft.Agents.Storage; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenAI; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +if (builder.Environment.IsDevelopment()) +{ + builder.Configuration.AddUserSecrets(); +} + +builder.Services.AddHttpClient(); + +// Register the inference service of your choice. AzureOpenAI and OpenAI are demonstrated... +IChatClient chatClient; +if (builder.Configuration.GetSection("AIServices").GetValue("UseAzureOpenAI")) +{ + var deploymentName = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("DeploymentName")!; + var endpoint = builder.Configuration.GetSection("AIServices:AzureOpenAI").GetValue("Endpoint")!; + + chatClient = new AzureOpenAIClient( + new Uri(endpoint), + new AzureCliCredential()) + .GetChatClient(deploymentName) + .AsIChatClient(); +} +else +{ + var modelId = builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ModelId")!; + var apiKey = builder.Configuration.GetSection("AIServices:OpenAI").GetValue("ApiKey")!; + + chatClient = new OpenAIClient( + apiKey) + .GetChatClient(modelId) + .AsIChatClient(); +} +builder.Services.AddSingleton(chatClient); + +// Add AgentApplicationOptions from appsettings section "AgentApplication". +builder.AddAgentApplicationOptions(); + +// Add the WeatherForecastAgent plus a welcome message. +// These will be consumed by the AFAgentApplication and exposed as an Agent SDK AgentApplication. +builder.Services.AddSingleton(); +builder.Services.AddKeyedSingleton("AFAgentApplicationWelcomeMessage", "Hello and Welcome! I'm here to help with all your weather forecast needs!"); + +// Add the AgentApplication, which contains the logic for responding to +// user messages via the Agent SDK. +builder.AddAgent(); + +// Register IStorage. For development, MemoryStorage is suitable. +// For production Agents, persisted storage should be used so +// that state survives Agent restarts, and operates correctly +// in a cluster of Agent instances. +builder.Services.AddSingleton(); + +// Configure the HTTP request pipeline. + +// Add AspNet token validation for Azure Bot Service and Entra. Authentication is +// configured in the appsettings.json "TokenValidation" section. +builder.Services.AddControllers(); +builder.Services.AddAgentAspNetAuthentication(builder.Configuration); + +WebApplication app = builder.Build(); + +// Enable AspNet authentication and authorization +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapGet("/", () => "Microsoft Agents SDK Sample"); + +// This receives incoming messages and routes them to the registered AgentApplication. +var incomingRoute = app.MapPost("/api/messages", async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) => await adapter.ProcessAsync(request, response, agent, cancellationToken)); + +if (!app.Environment.IsDevelopment()) +{ + incomingRoute.RequireAuthorization(); +} +else +{ + // Hardcoded for brevity and ease of testing. + // In production, this should be set in configuration. + app.Urls.Add("http://localhost:3978"); +} + +app.Run(); diff --git a/dotnet/samples/M365Agent/Properties/launchSettings.json b/dotnet/samples/M365Agent/Properties/launchSettings.json new file mode 100644 index 0000000000..14d89c0bab --- /dev/null +++ b/dotnet/samples/M365Agent/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "M365Agent": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:49692;http://localhost:49693" + } + } +} \ No newline at end of file diff --git a/dotnet/samples/M365Agent/README.md b/dotnet/samples/M365Agent/README.md new file mode 100644 index 0000000000..e669474ef9 --- /dev/null +++ b/dotnet/samples/M365Agent/README.md @@ -0,0 +1,119 @@ +# Microsoft Agent Framework agents with the M365 Agents SDK Weather Agent sample + +This is a sample of a simple Weather Forecast Agent that is hosted on an Asp.Net core web service and is exposed via the M365 Agent SDK. This Agent is configured to accept a request asking for information about a weather forecast and respond to the caller with an Adaptive Card. This agent will handle multiple "turns" to get the required information from the user. + +This Agent Sample is intended to introduce you the basics of integrating Agent Framework with the Microsoft 365 Agents SDK in order to use Agent Framework agents in various M365 services and applications. It can also be used as the base for a custom Agent that you choose to develop. + +***Note:*** This sample requires JSON structured output from the model which works best from newer versions of the model such as gpt-4o-mini. + +## Prerequisites + +- [.NET 8.0 SDK or later](https://dotnet.microsoft.com/download) +- [devtunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) +- [Microsoft 365 Agents Toolkit](https://github.com/OfficeDev/microsoft-365-agents-toolkit) + +- You will need an Azure OpenAI or OpenAI resource using `gpt-4o-mini` + +- Configure OpenAI in appsettings + + ```json + "AIServices": { + "AzureOpenAI": { + "DeploymentName": "", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model + "Endpoint": "", // This is the Endpoint of the Azure OpenAI resource + "ApiKey": "" // This is the API Key of the Azure OpenAI resource. Optional, uses AzureCliCredential if not provided + }, + "OpenAI": { + "ModelId": "", // This is the Model ID of the OpenAI model + "ApiKey": "" // This is your API Key for the OpenAI service + }, + "UseAzureOpenAI": false // This is a flag to determine whether to use the Azure OpenAI or the OpenAI service + } + ``` + +## QuickStart using Agent Toolkit +1. If you haven't done so already, install the Agents Playground + + ``` + winget install agentsplayground + ``` +1. Start the sample application. +1. Start Agents Playground. At a command prompt: `agentsplayground` + - The tool will open a web browser showing the Microsoft 365 Agents Playground, ready to send messages to your agent. +1. Interact with the Agent via the browser + +## QuickStart using WebChat or Teams + +- Overview of running and testing an Agent + - Provision an Azure Bot in your Azure Subscription + - Configure your Agent settings to use to desired authentication type + - Running an instance of the Agent app (either locally or deployed to Azure) + - Test in a client + +1. Create an Azure Bot with one of these authentication types + - [SingleTenant, Client Secret](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-single-secret) + - [SingleTenant, Federated Credentials](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-federated-credentials) + - [User Assigned Managed Identity](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/azure-bot-create-managed-identity) + + > Be sure to follow the **Next Steps** at the end of these docs to configure your agent settings. + + > **IMPORTANT:** If you want to run your agent locally via devtunnels, the only support auth type is ClientSecret and Certificates + +1. Running the Agent + 1. Running the Agent locally + - Requires a tunneling tool to allow for local development and debugging should you wish to do local development whilst connected to a external client such as Microsoft Teams. + - **For ClientSecret or Certificate authentication types only.** Federated Credentials and Managed Identity will not work via a tunnel to a local agent and must be deployed to an App Service or container. + + 1. Run `devtunnel`. Please follow [Create and host a dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started?tabs=windows) and host the tunnel with anonymous user access command as shown below: + + ```bash + devtunnel host -p 3978 --allow-anonymous + ``` + + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `{tunnel-url}/api/messages` + + 1. Start the Agent in Visual Studio + + 1. Deploy Agent code to Azure + 1. VS Publish works well for this. But any tools used to deploy a web application will also work. + 1. On the Azure Bot, select **Settings**, then **Configuration**, and update the **Messaging endpoint** to `https://{{appServiceDomain}}/api/messages` + +## Testing this agent with WebChat + + 1. Select **Test in WebChat** under **Settings** on the Azure Bot in the Azure Portal + +## Testing this Agent in Teams or M365 + +1. Update the manifest.json + - Edit the `manifest.json` contained in the `/appManifest` folder + - Replace with your AppId (that was created above) *everywhere* you see the place holder string `<>` + - Replace `<>` with your Agent url. For example, the tunnel host name. + - Zip up the contents of the `/appManifest` folder to create a `manifest.zip` + - `manifest.json` + - `outline.png` + - `color.png` + +1. Your Azure Bot should have the **Microsoft Teams** channel added under **Channels**. + +1. Navigate to the Microsoft Admin Portal (MAC). Under **Settings** and **Integrated Apps,** select **Upload Custom App**. + +1. Select the `manifest.zip` created in the previous step. + +1. After a short period of time, the agent shows up in Microsoft Teams and Microsoft 365 Copilot. + +## Enabling JWT token validation +1. By default, the AspNet token validation is disabled in order to support local debugging. +1. Enable by updating appsettings + ```json + "TokenValidation": { + "Enabled": true, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + ``` + +## Further reading + +To learn more about using the M365 Agent SDK, see [Microsoft 365 Agents SDK](https://learn.microsoft.com/en-us/microsoft-365/agents-sdk/). diff --git a/dotnet/samples/M365Agent/appManifest/color.png b/dotnet/samples/M365Agent/appManifest/color.png new file mode 100644 index 0000000000..b8cf81afbe Binary files /dev/null and b/dotnet/samples/M365Agent/appManifest/color.png differ diff --git a/dotnet/samples/M365Agent/appManifest/manifest.json b/dotnet/samples/M365Agent/appManifest/manifest.json new file mode 100644 index 0000000000..ca5890d8ea --- /dev/null +++ b/dotnet/samples/M365Agent/appManifest/manifest.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.22/MicrosoftTeams.schema.json", + "manifestVersion": "1.22", + "version": "1.0.0", + "id": "<>", + "developer": { + "name": "Microsoft, Inc.", + "websiteUrl": "https://example.azurewebsites.net", + "privacyUrl": "https://example.azurewebsites.net/privacy", + "termsOfUseUrl": "https://example.azurewebsites.net/termsofuse" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "AF Sample Agent", + "full": "M365 AgentSDK and Microsoft Agent Framework Sample" + }, + "description": { + "short": "Sample demonstrating M365 AgentSDK, Teams, and Microsoft Agent Framework", + "full": "Sample demonstrating M365 AgentSDK, Teams, and Microsoft Agent Framework" + }, + "accentColor": "#FFFFFF", + "copilotAgents": { + "customEngineAgents": [ + { + "id": "<>", + "type": "bot" + } + ] + }, + "bots": [ + { + "botId": "<>", + "scopes": [ + "personal" + ], + "supportsFiles": false, + "isNotificationOnly": false + } + ], + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "<>" + ] +} \ No newline at end of file diff --git a/dotnet/samples/M365Agent/appManifest/outline.png b/dotnet/samples/M365Agent/appManifest/outline.png new file mode 100644 index 0000000000..2c3bf6fa65 Binary files /dev/null and b/dotnet/samples/M365Agent/appManifest/outline.png differ diff --git a/dotnet/samples/M365Agent/appsettings.json.template b/dotnet/samples/M365Agent/appsettings.json.template new file mode 100644 index 0000000000..7268acf39b --- /dev/null +++ b/dotnet/samples/M365Agent/appsettings.json.template @@ -0,0 +1,54 @@ +{ + "TokenValidation": { + "Enabled": false, + "Audiences": [ + "{{ClientId}}" // this is the Client ID used for the Azure Bot + ], + "TenantId": "{{TenantId}}" + }, + + "AgentApplication": { + "StartTypingTimer": true, + "RemoveRecipientMention": false, + "NormalizeMentions": false + }, + + "Connections": { + "ServiceConnection": { + "Settings": { + // this is the AuthType for the connection, valid values can be found in Microsoft.Agents.Authentication.Msal.Model.AuthTypes. The default is ClientSecret. + "AuthType": "" + + // Other properties dependent on the authorization type the Azure Bot uses. + } + } + }, + "ConnectionsMap": [ + { + "ServiceUrl": "*", + "Connection": "ServiceConnection" + } + ], + + // This is the configuration for the AI services, use environment variables or user secrets to store sensitive information. + // Do not store sensitive information in this file + "AIServices": { + "AzureOpenAI": { + "DeploymentName": "", // This is the Deployment (as opposed to model) Name of the Azure OpenAI model + "Endpoint": "", // This is the Endpoint of the Azure OpenAI resource + "ApiKey": "" // This is the API Key of the Azure OpenAI resource. Optional, uses AzureCliCredential if not provided + }, + "OpenAI": { + "ModelId": "", // This is the Model ID of the OpenAI model + "ApiKey": "" // This is your API Key for the OpenAI service + }, + "UseAzureOpenAI": false // This is a flag to determine whether to use the Azure OpenAI or the OpenAI service + }, + + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs index 29b7dc588a..3271b40853 100644 --- a/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/EntitiesApiExtensions.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Runtime.CompilerServices; using System.Text.Json; - using Microsoft.Agents.AI.DevUI.Entities; -using Microsoft.Agents.AI.Hosting; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; @@ -27,21 +24,26 @@ internal static class EntitiesApiExtensions /// GET /v1/entities/{entityId}/info - Get detailed information about a specific entity /// /// The endpoints are compatible with the Python DevUI frontend and automatically discover entities - /// from the registered and services. + /// from the registered agents and workflows in the dependency injection container. /// public static IEndpointConventionBuilder MapEntities(this IEndpointRouteBuilder endpoints) { + var registeredAIAgents = GetRegisteredEntities(endpoints.ServiceProvider); + var registeredWorkflows = GetRegisteredEntities(endpoints.ServiceProvider); + var group = endpoints.MapGroup("/v1/entities") .WithTags("Entities"); // List all entities - group.MapGet("", ListEntitiesAsync) + group.MapGet("", (CancellationToken cancellationToken) + => ListEntitiesAsync(registeredAIAgents, registeredWorkflows, cancellationToken)) .WithName("ListEntities") .WithSummary("List all registered entities (agents and workflows)") .Produces(StatusCodes.Status200OK, contentType: "application/json"); // Get detailed entity information - group.MapGet("{entityId}/info", GetEntityInfoAsync) + group.MapGet("{entityId}/info", (string entityId, string? type, CancellationToken cancellationToken) + => GetEntityInfoAsync(entityId, type, registeredAIAgents, registeredWorkflows, cancellationToken)) .WithName("GetEntityInfo") .WithSummary("Get detailed information about a specific entity") .Produces(StatusCodes.Status200OK, contentType: "application/json") @@ -51,8 +53,8 @@ internal static class EntitiesApiExtensions } private static async Task ListEntitiesAsync( - AgentCatalog? agentCatalog, - WorkflowCatalog? workflowCatalog, + IEnumerable agents, + IEnumerable workflows, CancellationToken cancellationToken) { try @@ -60,13 +62,13 @@ internal static class EntitiesApiExtensions var entities = new Dictionary(); // Discover agents - await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false)) + foreach (var agentInfo in DiscoverAgents(agents, entityIdFilter: null)) { entities[agentInfo.Id] = agentInfo; } // Discover workflows - await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false)) + foreach (var workflowInfo in DiscoverWorkflows(workflows, entityIdFilter: null)) { entities[workflowInfo.Id] = workflowInfo; } @@ -85,15 +87,15 @@ internal static class EntitiesApiExtensions private static async Task GetEntityInfoAsync( string entityId, string? type, - AgentCatalog? agentCatalog, - WorkflowCatalog? workflowCatalog, + IEnumerable agents, + IEnumerable workflows, CancellationToken cancellationToken) { try { if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase)) { - await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false)) + foreach (var workflowInfo in DiscoverWorkflows(workflows, entityId)) { return Results.Json(workflowInfo, EntitiesJsonContext.Default.EntityInfo); } @@ -101,7 +103,7 @@ internal static class EntitiesApiExtensions if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase)) { - await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false)) + foreach (var agentInfo in DiscoverAgents(agents, entityId)) { return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo); } @@ -118,17 +120,9 @@ internal static class EntitiesApiExtensions } } - private static async IAsyncEnumerable DiscoverAgentsAsync( - AgentCatalog? agentCatalog, - string? entityIdFilter, - [EnumeratorCancellation] CancellationToken cancellationToken) + private static IEnumerable DiscoverAgents(IEnumerable agents, string? entityIdFilter) { - if (agentCatalog is null) - { - yield break; - } - - await foreach (var agent in agentCatalog.GetAgentsAsync(cancellationToken).ConfigureAwait(false)) + foreach (var agent in agents) { // If filtering by entity ID, skip non-matching agents if (entityIdFilter is not null && @@ -148,17 +142,9 @@ internal static class EntitiesApiExtensions } } - private static async IAsyncEnumerable DiscoverWorkflowsAsync( - WorkflowCatalog? workflowCatalog, - string? entityIdFilter, - [EnumeratorCancellation] CancellationToken cancellationToken) + private static IEnumerable DiscoverWorkflows(IEnumerable workflows, string? entityIdFilter) { - if (workflowCatalog is null) - { - yield break; - } - - await foreach (var workflow in workflowCatalog.GetWorkflowsAsync(cancellationToken).ConfigureAwait(false)) + foreach (var workflow in workflows) { var workflowId = workflow.Name ?? workflow.StartExecutorId; @@ -304,4 +290,14 @@ internal static class EntitiesApiExtensions StartExecutorId = workflow.StartExecutorId }; } + + private static IEnumerable GetRegisteredEntities(IServiceProvider serviceProvider) + { + var keyedEntities = serviceProvider.GetKeyedServices(KeyedService.AnyKey); + var defaultEntities = serviceProvider.GetServices() ?? []; + + return keyedEntities + .Concat(defaultEntities) + .Where(entity => entity is not null); + } } diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/HostApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/HostApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..30fa9ad29e --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/HostApplicationBuilderExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for to configure DevUI. +/// +public static class MicrosoftAgentAIDevUIHostApplicationBuilderExtensions +{ + /// + /// Adds DevUI services to the host application builder. + /// + /// The to configure. + /// The for method chaining. + public static IHostApplicationBuilder AddDevUI(this IHostApplicationBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.AddDevUI(); + + return builder; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj index 37aa6c37f8..6c9c5bd9e3 100644 --- a/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/Microsoft.Agents.AI.DevUI.csproj @@ -1,7 +1,8 @@  - net9.0 + $(ProjectsCoreTargetFrameworks) + $(ProjectsDebugCoreTargetFrameworks) enable enable Microsoft.Agents.AI.DevUI @@ -12,6 +13,10 @@ $(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812 + + true + + @@ -33,4 +38,7 @@ Provides Microsoft Agent Framework support for developer UI. + + + diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/README.md b/dotnet/src/Microsoft.Agents.AI.DevUI/README.md index b55869748d..104c43729b 100644 --- a/dotnet/src/Microsoft.Agents.AI.DevUI/README.md +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/README.md @@ -24,9 +24,15 @@ var builder = WebApplication.CreateBuilder(args); // Register your agents builder.AddAIAgent("assistant", "You are a helpful assistant."); +// Register DevUI services +if (builder.Environment.IsDevelopment()) +{ + builder.AddDevUI(); +} + // Register services for OpenAI responses and conversations (also required for DevUI) -builder.Services.AddOpenAIResponses(); -builder.Services.AddOpenAIConversations(); +builder.AddOpenAIResponses(); +builder.AddOpenAIConversations(); var app = builder.Build(); diff --git a/dotnet/src/Microsoft.Agents.AI.DevUI/ServiceCollectionsExtensions.cs b/dotnet/src/Microsoft.Agents.AI.DevUI/ServiceCollectionsExtensions.cs new file mode 100644 index 0000000000..6971e3d2e0 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI.DevUI/ServiceCollectionsExtensions.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for to configure DevUI. +/// +public static class MicrosoftAgentAIDevUIServiceCollectionsExtensions +{ + /// + /// Adds services required for DevUI integration. + /// + /// The to configure. + /// The for method chaining. + public static IServiceCollection AddDevUI(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + // a factory that tries to construct an AIAgent from Workflow, + // even if workflow was not explicitly registered as an AIAgent. + +#pragma warning disable IDE0001 // Simplify Names + services.AddKeyedSingleton(KeyedService.AnyKey, (sp, key) => + { + var keyAsStr = key as string; + Throw.IfNullOrEmpty(keyAsStr); + + var workflow = sp.GetKeyedService(keyAsStr); + if (workflow is not null) + { + return workflow.AsAgent(name: workflow.Name); + } + + // another thing we can do is resolve a non-keyed workflow. + // however, we can't rely on anything than key to be equal to the workflow.Name. + // so we try: if we fail, we return null. + workflow = sp.GetService(); + if (workflow is not null && workflow.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true) + { + return workflow.AsAgent(name: workflow.Name); + } + + // and it's possible to lookup at the default-registered AIAgent + // with the condition of same name as the key. + var agent = sp.GetService(); + if (agent is not null && agent.Name?.Equals(keyAsStr, StringComparison.Ordinal) == true) + { + return agent; + } + + return null!; + }); +#pragma warning restore IDE0001 // Simplify Names + + return services; + } +} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs index f8c4618c9e..ebd378ac3b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs @@ -83,12 +83,13 @@ internal static class BuiltInFunctions string? threadIdValue = threadIdFromBody ?? threadIdFromQuery; - // If no session ID is provided, use a new one based on the function name and invocation ID. - // This may be better than a random one because it can be correlated with the function invocation. - // Specifying a session ID is how the caller correlates multiple calls to the same agent session. + // The thread_id is treated as a session key (not a full session ID). + // If no session key is provided, use the function invocation ID as the session key + // to help correlate the session with the function invocation. + string agentName = GetAgentName(context); AgentSessionId sessionId = string.IsNullOrEmpty(threadIdValue) - ? new AgentSessionId(GetAgentName(context), context.InvocationId) - : AgentSessionId.Parse(threadIdValue); + ? new AgentSessionId(agentName, context.InvocationId) + : new AgentSessionId(agentName, threadIdValue); if (string.IsNullOrWhiteSpace(message)) { @@ -110,7 +111,7 @@ internal static class BuiltInFunctions } } - AIAgent agentProxy = client.AsDurableAgentProxy(context, GetAgentName(context)); + AIAgent agentProxy = client.AsDurableAgentProxy(context, agentName); DurableAgentRunOptions options = new() { IsFireAndForget = !waitForResponse }; @@ -126,7 +127,7 @@ internal static class BuiltInFunctions req, context, HttpStatusCode.OK, - sessionId.ToString(), + sessionId.Key, agentResponse); } @@ -140,7 +141,7 @@ internal static class BuiltInFunctions return await CreateAcceptedResponseAsync( req, context, - sessionId.ToString()); + sessionId.Key); } public static async Task RunMcpToolAsync( diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs index 78fa89c9ab..f520acb662 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/HostedAgentResponseExecutor.cs @@ -63,7 +63,11 @@ internal sealed class HostedAgentResponseExecutor : IResponseExecutor return ValueTask.FromResult(new ResponseError { Code = "agent_not_found", - Message = $"Agent '{agentName}' not found. Ensure the agent is registered with AddAIAgent()." + Message = $""" + Agent '{agentName}' not found. + Ensure the agent is registered with '{agentName}' name in the dependency injection container. + We recommend using 'builder.AddAIAgent()' for simplicity. + """ }); } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentCatalog.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentCatalog.cs deleted file mode 100644 index 0d2ef69640..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentCatalog.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Threading; - -namespace Microsoft.Agents.AI.Hosting; - -/// -/// Provides a catalog of registered AI agents within the hosting environment. -/// -/// -/// The agent catalog allows enumeration of all registered agents in the dependency injection container. -/// This is useful for scenarios where you need to discover and interact with multiple agents programmatically. -/// -public abstract class AgentCatalog -{ - /// - /// Initializes a new instance of the class. - /// - protected AgentCatalog() - { - } - - /// - /// Asynchronously retrieves all registered AI agents from the catalog. - /// - /// The to monitor for cancellation requests. The default is . - /// - /// An asynchronous enumerable of instances representing all registered agents. - /// The enumeration will only include agents that are successfully resolved from the service provider. - /// - /// - /// This method enumerates through all registered agent names and attempts to resolve each agent - /// from the dependency injection container. Only successfully resolved agents are yielded. - /// The enumeration is lazy and agents are resolved on-demand during iteration. - /// - public abstract IAsyncEnumerable GetAgentsAsync(CancellationToken cancellationToken = default); -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs index d958fc3578..e12d017343 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/AgentHostingServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Agents.AI.Hosting.Local; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -126,31 +125,9 @@ public static class AgentHostingServiceCollectionExtensions return agent; }); - // Register the agent by name for discovery. - var agentHostBuilder = GetAgentRegistry(services); - agentHostBuilder.AgentNames.Add(name); - return new HostedAgentBuilder(name, services); } - private static LocalAgentRegistry GetAgentRegistry(IServiceCollection services) - { - var descriptor = services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalAgentRegistry))); - if (descriptor?.ImplementationInstance is not LocalAgentRegistry instance) - { - instance = new LocalAgentRegistry(); - ConfigureHostBuilder(services, instance); - } - - return instance; - } - - private static void ConfigureHostBuilder(IServiceCollection services, LocalAgentRegistry agentHostBuilderContext) - { - services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext)); - services.AddSingleton(); - } - private static IList GetRegisteredToolsForAgent(IServiceProvider serviceProvider, string agentName) { var registry = serviceProvider.GetService(); diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs index 2215a52a69..8075caec59 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting/HostApplicationBuilderWorkflowExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Linq; -using Microsoft.Agents.AI.Hosting.Local; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -47,28 +45,6 @@ public static class HostApplicationBuilderWorkflowExtensions return workflow; }); - // Register the workflow by name for discovery. - var workflowRegistry = GetWorkflowRegistry(builder); - workflowRegistry.WorkflowNames.Add(name); - return new HostedWorkflowBuilder(name, builder); } - - private static LocalWorkflowRegistry GetWorkflowRegistry(IHostApplicationBuilder builder) - { - var descriptor = builder.Services.FirstOrDefault(s => !s.IsKeyedService && s.ServiceType.Equals(typeof(LocalWorkflowRegistry))); - if (descriptor?.ImplementationInstance is not LocalWorkflowRegistry instance) - { - instance = new LocalWorkflowRegistry(); - ConfigureHostBuilder(builder, instance); - } - - return instance; - } - - private static void ConfigureHostBuilder(IHostApplicationBuilder builder, LocalWorkflowRegistry agentHostBuilderContext) - { - builder.Services.Add(ServiceDescriptor.Singleton(agentHostBuilderContext)); - builder.Services.AddSingleton(); - } } diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentCatalog.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentCatalog.cs deleted file mode 100644 index 0b44ad60cb..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentCatalog.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Agents.AI.Hosting.Local; - -// Implementation of an AgentCatalog which enumerates agents registered in the local service provider. -internal sealed class LocalAgentCatalog : AgentCatalog -{ - public readonly HashSet _registeredAgents; - private readonly IServiceProvider _serviceProvider; - - public LocalAgentCatalog(LocalAgentRegistry agentHostBuilder, IServiceProvider serviceProvider) - { - this._registeredAgents = [.. agentHostBuilder.AgentNames]; - this._serviceProvider = serviceProvider; - } - - public override async IAsyncEnumerable GetAgentsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await Task.CompletedTask.ConfigureAwait(false); - - foreach (var name in this._registeredAgents) - { - var agent = this._serviceProvider.GetKeyedService(name); - if (agent is not null) - { - yield return agent; - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentRegistry.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentRegistry.cs deleted file mode 100644 index df3db8f554..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalAgentRegistry.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; - -namespace Microsoft.Agents.AI.Hosting.Local; - -internal sealed class LocalAgentRegistry -{ - public HashSet AgentNames { get; } = []; -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowCatalog.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowCatalog.cs deleted file mode 100644 index 572b41830e..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowCatalog.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Agents.AI.Workflows; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.Agents.AI.Hosting.Local; - -internal sealed class LocalWorkflowCatalog : WorkflowCatalog -{ - public readonly HashSet _registeredWorkflows; - private readonly IServiceProvider _serviceProvider; - - public LocalWorkflowCatalog(LocalWorkflowRegistry workflowRegistry, IServiceProvider serviceProvider) - { - this._registeredWorkflows = [.. workflowRegistry.WorkflowNames]; - this._serviceProvider = serviceProvider; - } - - public override async IAsyncEnumerable GetWorkflowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await Task.CompletedTask.ConfigureAwait(false); - - foreach (var name in this._registeredWorkflows) - { - var workflow = this._serviceProvider.GetKeyedService(name); - if (workflow is not null) - { - yield return workflow; - } - } - } -} diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowRegistry.cs b/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowRegistry.cs deleted file mode 100644 index 803c24660f..0000000000 --- a/dotnet/src/Microsoft.Agents.AI.Hosting/Local/LocalWorkflowRegistry.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; - -namespace Microsoft.Agents.AI.Hosting.Local; - -internal sealed class LocalWorkflowRegistry -{ - public HashSet WorkflowNames { get; } = []; -} diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs index ad224e6777..f0f51895b2 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentThread.cs @@ -75,8 +75,8 @@ public class ChatClientAgentThread : AgentThread /// /// /// Note that either or may be set, but not both. - /// If is not null, and is set, - /// will be reverted to null, and vice versa. + /// If is not null, setting will throw an + /// exception. /// /// /// This property may be null in the following cases: @@ -91,6 +91,7 @@ public class ChatClientAgentThread : AgentThread /// to fork the thread with each iteration. /// /// + /// Attempted to set a conversation ID but a is already set. public string? ConversationId { get => this._conversationId; diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIExtensionsTests.cs new file mode 100644 index 0000000000..d002068626 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIExtensionsTests.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.DevUI.UnitTests; + +/// +/// Unit tests for DevUI service collection extensions. +/// Tests verify that workflows and agents can be resolved even when registered non-conventionally. +/// +public class DevUIExtensionsTests +{ + /// + /// Verifies that AddDevUI throws ArgumentNullException when services collection is null. + /// + [Fact] + public void AddDevUI_NullServices_ThrowsArgumentNullException() + { + IServiceCollection services = null!; + Assert.Throws(() => services.AddDevUI()); + } + + /// + /// Verifies that GetRequiredKeyedService throws for non-existent keys. + /// + [Fact] + public void AddDevUI_GetRequiredKeyedServiceNonExistent_ThrowsInvalidOperationException() + { + // Arrange + var services = new ServiceCollection(); + services.AddDevUI(); + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert + Assert.Throws(() => serviceProvider.GetRequiredKeyedService("non-existent")); + } + + /// + /// Verifies that an agent with null name can be resolved by its workflow. + /// + [Fact] + public void AddDevUI_WorkflowWithName_CanBeResolved_AsAIAgent() + { + // Arrange + var services = new ServiceCollection(); + var mockChatClient = new Mock(); + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null); + var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2); + + services.AddKeyedSingleton("workflow", workflow); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService("workflow"); + + // Assert + Assert.NotNull(resolvedWorkflowAsAgent); + Assert.Null(resolvedWorkflowAsAgent.Name); + } + + /// + /// Verifies that an agent with null name can be resolved by its workflow. + /// + [Fact] + public void AddDevUI_MultipleWorkflowsWithName_CanBeResolved_AsAIAgent() + { + var services = new ServiceCollection(); + var mockChatClient = new Mock(); + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null); + var workflow1 = AgentWorkflowBuilder.BuildSequential(agent1, agent2); + var workflow2 = AgentWorkflowBuilder.BuildSequential(agent1, agent2); + + services.AddKeyedSingleton("workflow1", workflow1); + services.AddKeyedSingleton("workflow2", workflow2); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + var resolvedWorkflow1AsAgent = serviceProvider.GetKeyedService("workflow1"); + Assert.NotNull(resolvedWorkflow1AsAgent); + Assert.Null(resolvedWorkflow1AsAgent.Name); + + var resolvedWorkflow2AsAgent = serviceProvider.GetKeyedService("workflow2"); + Assert.NotNull(resolvedWorkflow2AsAgent); + Assert.Null(resolvedWorkflow2AsAgent.Name); + + Assert.False(resolvedWorkflow1AsAgent == resolvedWorkflow2AsAgent); + } + + /// + /// Verifies that an agent with null name can be resolved by its workflow. + /// + [Fact] + public void AddDevUI_NonKeyedWorkflow_CanBeResolved_AsAIAgent() + { + var services = new ServiceCollection(); + var mockChatClient = new Mock(); + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null); + var workflow = AgentWorkflowBuilder.BuildSequential(agent1, agent2); + + services.AddKeyedSingleton("workflow", workflow); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + var resolvedWorkflowAsAgent = serviceProvider.GetKeyedService("workflow"); + Assert.NotNull(resolvedWorkflowAsAgent); + Assert.Null(resolvedWorkflowAsAgent.Name); + } + + /// + /// Verifies that an agent with null name can be resolved by its workflow. + /// + [Fact] + public void AddDevUI_NonKeyedWorkflow_PlusKeyedWorkflow_CanBeResolved_AsAIAgent() + { + var services = new ServiceCollection(); + var mockChatClient = new Mock(); + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test 1", name: null); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test 2", name: null); + var workflow = AgentWorkflowBuilder.BuildSequential("standardname", agent1, agent2); + var keyedWorkflow = AgentWorkflowBuilder.BuildSequential("keyedname", agent1, agent2); + + services.AddSingleton(workflow); + services.AddKeyedSingleton("keyed", keyedWorkflow); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + // resolve a workflow with the same name as workflow's name (which is registered without a key) + var standardAgent = serviceProvider.GetKeyedService("standardname"); + Assert.NotNull(standardAgent); + Assert.Equal("standardname", standardAgent.Name); + + var keyedAgent = serviceProvider.GetKeyedService("keyed"); + Assert.NotNull(keyedAgent); + Assert.Equal("keyedname", keyedAgent.Name); + + var nonExisting = serviceProvider.GetKeyedService("random-non-existing!!!"); + Assert.Null(nonExisting); + } + + /// + /// Verifies that an agent registered with a different key than its name can be resolved by key. + /// + [Fact] + public void AddDevUI_AgentRegisteredWithDifferentKey_CanBeResolvedByKey() + { + // Arrange + var services = new ServiceCollection(); + const string AgentName = "actual-agent-name"; + const string RegistrationKey = "different-key"; + var mockChatClient = new Mock(); + var agent = new ChatClientAgent(mockChatClient.Object, "Test", AgentName); + + services.AddKeyedSingleton(RegistrationKey, agent); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + // Act + var resolvedAgent = serviceProvider.GetKeyedService(RegistrationKey); + + // Assert + Assert.NotNull(resolvedAgent); + // The resolved agent should have the agent's name, not the registration key + Assert.Equal(AgentName, resolvedAgent.Name); + } + + /// + /// Verifies that an agent registered with a different key than its name can be resolved by key. + /// + [Fact] + public void AddDevUI_Keyed_AndStandard_BothCanBeResolved() + { + // Arrange + var services = new ServiceCollection(); + var mockChatClient = new Mock(); + var defaultAgent = new ChatClientAgent(mockChatClient.Object, "default", "default"); + var keyedAgent = new ChatClientAgent(mockChatClient.Object, "keyed", "keyed"); + + services.AddSingleton(defaultAgent); + services.AddKeyedSingleton("keyed-registration", keyedAgent); + services.AddDevUI(); + + var serviceProvider = services.BuildServiceProvider(); + + var resolvedKeyedAgent = serviceProvider.GetKeyedService("keyed-registration"); + Assert.NotNull(resolvedKeyedAgent); + Assert.Equal("keyed", resolvedKeyedAgent.Name); + + // resolving default agent based on its name, not on the registration-key + var resolvedDefaultAgent = serviceProvider.GetKeyedService("default"); + Assert.NotNull(resolvedDefaultAgent); + Assert.Equal("default", resolvedDefaultAgent.Name); + } + + /// + /// Verifies that the DevUI fallback handler error message includes helpful information. + /// + [Fact] + public void AddDevUI_InvalidResolution_ErrorMessageIsInformative() + { + // Arrange + var services = new ServiceCollection(); + services.AddDevUI(); + var serviceProvider = services.BuildServiceProvider(); + const string InvalidKey = "invalid-key-name"; + + // Act & Assert + var exception = Assert.Throws(() => serviceProvider.GetRequiredKeyedService(InvalidKey)); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs new file mode 100644 index 0000000000..b8512a856e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/DevUIIntegrationTests.cs @@ -0,0 +1,285 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.Agents.AI.DevUI.Entities; +using Microsoft.Agents.AI.Workflows; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Moq; + +namespace Microsoft.Agents.AI.DevUI.UnitTests; + +public class DevUIIntegrationTests +{ + private sealed class NoOpExecutor(string id) : Executor(id) + { + protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => + routeBuilder.AddHandler( + (msg, ctx) => ctx.SendMessageAsync(msg)); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesRequestToWorkflow_ByKeyAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockChatClient = new Mock(); + var agent = new ChatClientAgent(mockChatClient.Object, "Test", "agent-name"); + + builder.Services.AddKeyedSingleton("registration-key", agent); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var resolvedAgent = app.Services.GetKeyedService("registration-key"); + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(discoveryResponse); + Assert.Single(discoveryResponse.Entities); + Assert.Equal("agent-name", discoveryResponse.Entities[0].Name); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesMultipleAIAgents_ByKeyAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockChatClient = new Mock(); + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-one"); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-two"); + var agent3 = new ChatClientAgent(mockChatClient.Object, "Test", "agent-three"); + + builder.Services.AddKeyedSingleton("key-1", agent1); + builder.Services.AddKeyedSingleton("key-2", agent2); + builder.Services.AddKeyedSingleton("key-3", agent3); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.NotNull(discoveryResponse); + Assert.Equal(3, discoveryResponse.Entities.Count); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-one" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-two" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "agent-three" && e.Type == "agent"); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesAIAgents_WithKeyedAndDefaultRegistrationAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockChatClient = new Mock(); + var agentKeyed1 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-one"); + var agentKeyed2 = new ChatClientAgent(mockChatClient.Object, "Test", "keyed-agent-two"); + var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-agent"); + + builder.Services.AddKeyedSingleton("key-1", agentKeyed1); + builder.Services.AddKeyedSingleton("key-2", agentKeyed2); + builder.Services.AddSingleton(agentDefault); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.NotNull(discoveryResponse); + Assert.Equal(3, discoveryResponse.Entities.Count); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-one" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-agent-two" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-agent" && e.Type == "agent"); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesMultipleWorkflows_ByKeyAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var workflow1 = new WorkflowBuilder("executor-1") + .WithName("workflow-one") + .WithDescription("First workflow") + .BindExecutor(new NoOpExecutor("executor-1")) + .Build(); + + var workflow2 = new WorkflowBuilder("executor-2") + .WithName("workflow-two") + .WithDescription("Second workflow") + .BindExecutor(new NoOpExecutor("executor-2")) + .Build(); + + var workflow3 = new WorkflowBuilder("executor-3") + .WithName("workflow-three") + .WithDescription("Third workflow") + .BindExecutor(new NoOpExecutor("executor-3")) + .Build(); + + builder.Services.AddKeyedSingleton("key-1", workflow1); + builder.Services.AddKeyedSingleton("key-2", workflow2); + builder.Services.AddKeyedSingleton("key-3", workflow3); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.NotNull(discoveryResponse); + Assert.Equal(3, discoveryResponse.Entities.Count); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-one" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-two" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "workflow-three" && e.Type == "workflow"); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesWorkflows_WithKeyedAndDefaultRegistrationAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var workflowKeyed1 = new WorkflowBuilder("executor-1") + .WithName("keyed-workflow-one") + .BindExecutor(new NoOpExecutor("executor-1")) + .Build(); + + var workflowKeyed2 = new WorkflowBuilder("executor-2") + .WithName("keyed-workflow-two") + .BindExecutor(new NoOpExecutor("executor-2")) + .Build(); + + var workflowDefault = new WorkflowBuilder("executor-default") + .WithName("default-workflow") + .BindExecutor(new NoOpExecutor("executor-default")) + .Build(); + + builder.Services.AddKeyedSingleton("key-1", workflowKeyed1); + builder.Services.AddKeyedSingleton("key-2", workflowKeyed2); + builder.Services.AddSingleton(workflowDefault); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.NotNull(discoveryResponse); + Assert.Equal(3, discoveryResponse.Entities.Count); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-one" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "keyed-workflow-two" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-workflow" && e.Type == "workflow"); + } + + [Fact] + public async Task TestServerWithDevUI_ResolvesMixedAgentsAndWorkflows_AllRegistrationsAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + var mockChatClient = new Mock(); + + // Create AIAgents + var agent1 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-one"); + var agent2 = new ChatClientAgent(mockChatClient.Object, "Test", "mixed-agent-two"); + var agentDefault = new ChatClientAgent(mockChatClient.Object, "Test", "default-mixed-agent"); + + // Create Workflows + var workflow1 = new WorkflowBuilder("executor-1") + .WithName("mixed-workflow-one") + .BindExecutor(new NoOpExecutor("executor-1")) + .Build(); + + var workflow2 = new WorkflowBuilder("executor-2") + .WithName("mixed-workflow-two") + .BindExecutor(new NoOpExecutor("executor-2")) + .Build(); + + var workflowDefault = new WorkflowBuilder("executor-default") + .WithName("default-mixed-workflow") + .BindExecutor(new NoOpExecutor("executor-default")) + .Build(); + + // Register all + builder.Services.AddKeyedSingleton("agent-key-1", agent1); + builder.Services.AddKeyedSingleton("agent-key-2", agent2); + builder.Services.AddSingleton(agentDefault); + builder.Services.AddKeyedSingleton("workflow-key-1", workflow1); + builder.Services.AddKeyedSingleton("workflow-key-2", workflow2); + builder.Services.AddSingleton(workflowDefault); + builder.Services.AddDevUI(); + + using WebApplication app = builder.Build(); + app.MapDevUI(); + + await app.StartAsync(); + + // Act + var client = app.GetTestClient(); + var response = await client.GetAsync(new Uri("/v1/entities", uriKind: UriKind.Relative)); + + var discoveryResponse = await response.Content.ReadFromJsonAsync(); + + // Assert + Assert.NotNull(discoveryResponse); + Assert.Equal(6, discoveryResponse.Entities.Count); + + // Verify agents + Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-one" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-agent-two" && e.Type == "agent"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-agent" && e.Type == "agent"); + + // Verify workflows + Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-one" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "mixed-workflow-two" && e.Type == "workflow"); + Assert.Contains(discoveryResponse.Entities, e => e.Name == "default-mixed-workflow" && e.Type == "workflow"); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj new file mode 100644 index 0000000000..9135a90e2e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Microsoft.Agents.AI.DevUI.UnitTests.csproj @@ -0,0 +1,20 @@ + + + + $(ProjectsCoreTargetFrameworks) + $(ProjectsDebugCoreTargetFrameworks) + false + $(NoWarn);CA1812 + + + + + + + + + + + + + diff --git a/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json new file mode 100644 index 0000000000..783215ce29 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.DevUI.UnitTests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Agents.AI.DevUI.UnitTests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:63009;http://localhost:63010" + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json new file mode 100644 index 0000000000..6b8f8d04a4 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Microsoft.Agents.AI.Hosting.A2A.UnitTests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:52186;http://localhost:52187" + } + } +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs index f2bfcf234c..ef9807fddf 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.AzureFunctions.IntegrationTests/SamplesValidation.cs @@ -75,9 +75,9 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi // The response headers should include the agent thread ID, which can be used to continue the conversation. string? threadId = response.Headers.GetValues("x-ms-thread-id")?.FirstOrDefault(); Assert.NotNull(threadId); + Assert.NotEmpty(threadId); this._outputHelper.WriteLine($"Agent thread ID: {threadId}"); - Assert.StartsWith("@dafx-joker@", threadId); // Wait for up to 30 seconds to see if the agent response is available in the logs await this.WaitForConditionAsync( @@ -289,7 +289,7 @@ public sealed class SamplesValidation(ITestOutputHelper outputHelper) : IAsyncLi startResponse.Headers.TryGetValues("x-ms-thread-id", out IEnumerable? agentIdValues); string? threadId = agentIdValues?.FirstOrDefault(); Assert.NotNull(threadId); - Assert.StartsWith("@dafx-publisher@", threadId); + Assert.NotEmpty(threadId); // Wait for the orchestration to report that it's waiting for human approval await this.WaitForConditionAsync( diff --git a/python/CHANGELOG.md b/python/CHANGELOG.md index 0edfd01c42..5d41d5ddf6 100644 --- a/python/CHANGELOG.md +++ b/python/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0b251117] - 2025-11-17 + +### Fixed + +- **agent-framework-ag-ui**: Fix ag-ui state handling issues ([#2289](https://github.com/microsoft/agent-framework/pull/2289)) + ## [1.0.0b251114] - 2025-11-14 ### Added diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_events.py b/python/packages/ag-ui/agent_framework_ag_ui/_events.py index e10f3e92c8..8aec59d52c 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_events.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_events.py @@ -86,6 +86,7 @@ class AgentFrameworkEventBridge: self.pending_tool_calls: list[dict[str, Any]] = [] # Track tool calls for assistant message self.tool_results: list[dict[str, Any]] = [] # Track tool results self.tool_calls_ended: set[str] = set() # Track which tool calls have had ToolCallEndEvent emitted + self.accumulated_text_content: str = "" # Track accumulated text for final MessagesSnapshotEvent async def from_agent_run_update(self, update: AgentRunResponseUpdate) -> list[BaseEvent]: """ @@ -99,18 +100,29 @@ class AgentFrameworkEventBridge: """ events: list[BaseEvent] = [] - for content in update.contents: + logger.info(f"Processing AgentRunUpdate with {len(update.contents)} content items") + for idx, content in enumerate(update.contents): + logger.info(f" Content {idx}: type={type(content).__name__}") if isinstance(content, TextContent): + logger.info( + f" TextContent found: text_length={len(content.text)}, text_preview='{content.text[:100]}'" + ) + logger.info( + f" Flags: skip_text_content={self.skip_text_content}, should_stop_after_confirm={self.should_stop_after_confirm}" + ) + # Skip text content if using structured outputs (it's just the JSON) if self.skip_text_content: + logger.info(" SKIPPING TextContent: skip_text_content is True") continue # Skip text content if we're about to emit confirm_changes # The summary should only appear after user confirms if self.should_stop_after_confirm: - logger.debug("Skipping text content - waiting for confirm_changes response") + logger.info(" SKIPPING TextContent: waiting for confirm_changes response") # Save the summary text to show after confirmation self.suppressed_summary += content.text + logger.info(f" Suppressed summary now has {len(self.suppressed_summary)} chars") continue if not self.current_message_id: @@ -119,14 +131,16 @@ class AgentFrameworkEventBridge: message_id=self.current_message_id, role="assistant", ) - logger.debug(f"Emitting TextMessageStartEvent with message_id={self.current_message_id}") + logger.info(f" EMITTING TextMessageStartEvent with message_id={self.current_message_id}") events.append(start_event) event = TextMessageContentEvent( message_id=self.current_message_id, delta=content.text, ) - logger.debug(f"Emitting TextMessageContentEvent with delta: {content.text}") + # Accumulate text content for final MessagesSnapshotEvent + self.accumulated_text_content += content.text + logger.info(f" EMITTING TextMessageContentEvent with delta: '{content.text}'") events.append(event) elif isinstance(content, FunctionCallContent): @@ -427,7 +441,24 @@ class AgentFrameworkEventBridge: # Emit MessagesSnapshotEvent with the complete conversation including tool calls and results # This is required for CopilotKit's useCopilotAction to detect tool result - if self.pending_tool_calls and self.tool_results: + # HOWEVER: Skip this for predictive tools when require_confirmation=False, because + # the agent will generate a follow-up text message and we'll emit a complete snapshot at the end. + # Emitting here would create an incomplete snapshot that gets replaced, causing UI flicker. + should_emit_snapshot = self.pending_tool_calls and self.tool_results + + # Check if this is a predictive tool that will have a follow-up message + is_predictive_without_confirmation = False + if should_emit_snapshot and self.current_tool_call_name and self.predict_state_config: + for state_key, config in self.predict_state_config.items(): + if config["tool"] == self.current_tool_call_name and not self.require_confirmation: + is_predictive_without_confirmation = True + logger.info( + f"Skipping intermediate MessagesSnapshotEvent for predictive tool '{self.current_tool_call_name}' " + "- will emit complete snapshot after follow-up message" + ) + break + + if should_emit_snapshot and not is_predictive_without_confirmation: # Import message adapter from ._message_adapters import agent_framework_messages_to_agui diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py index a6c1001386..11d2977f90 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_message_adapters.py @@ -283,8 +283,62 @@ def extract_text_from_contents(contents: list[Any]) -> str: return "".join(text_parts) +def agui_messages_to_snapshot_format(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Normalize AG-UI messages for MessagesSnapshotEvent. + + Converts AG-UI input format (with 'input_text' type) to snapshot format (with 'text' type). + + Args: + messages: List of AG-UI messages in input format + + Returns: + List of normalized messages suitable for MessagesSnapshotEvent + """ + from ._utils import generate_event_id + + result: list[dict[str, Any]] = [] + for msg in messages: + normalized_msg = msg.copy() + + # Ensure ID exists + if "id" not in normalized_msg: + normalized_msg["id"] = generate_event_id() + + # Normalize content field + content = normalized_msg.get("content") + if isinstance(content, list): + # Convert content array format to simple string + text_parts = [] + for item in content: + if isinstance(item, dict): + # Convert 'input_text' to 'text' type + if item.get("type") == "input_text": + text_parts.append(item.get("text", "")) + elif item.get("type") == "text": + text_parts.append(item.get("text", "")) + else: + # Other types - just extract text field if present + text_parts.append(item.get("text", "")) + normalized_msg["content"] = "".join(text_parts) + elif content is None: + normalized_msg["content"] = "" + + # Normalize tool_call_id to toolCallId for tool messages + if normalized_msg.get("role") == "tool": + if "tool_call_id" in normalized_msg: + normalized_msg["toolCallId"] = normalized_msg["tool_call_id"] + del normalized_msg["tool_call_id"] + elif "toolCallId" not in normalized_msg: + normalized_msg["toolCallId"] = "" + + result.append(normalized_msg) + + return result + + __all__ = [ "agui_messages_to_agent_framework", "agent_framework_messages_to_agui", + "agui_messages_to_snapshot_format", "extract_text_from_contents", ] diff --git a/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py b/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py index 1f6a43d8c1..6da46d819f 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py +++ b/python/packages/ag-ui/agent_framework_ag_ui/_orchestrators.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any from ag_ui.core import ( BaseEvent, + MessagesSnapshotEvent, RunErrorEvent, TextMessageContentEvent, TextMessageEndEvent, @@ -588,32 +589,37 @@ class DefaultOrchestrator(Orchestrator): # We should NOT add to thread.on_new_messages() as that would cause duplication. # Instead, we pass messages directly to the agent via messages_to_run. - # Inject current state as system message context if we have state + # Inject current state as system message context if we have state and this is a new user turn messages_to_run: list[Any] = [] + # Check if the last message is from the user (new turn) vs assistant/tool (mid-execution) + is_new_user_turn = False + if provider_messages: + last_msg = provider_messages[-1] + is_new_user_turn = last_msg.role.value == "user" + + # Check if conversation has tool calls (indicates mid-execution) conversation_has_tool_calls = False - logger.debug(f"Checking {len(provider_messages)} provider messages for tool calls") - for i, msg in enumerate(provider_messages): - logger.debug( - f" Message {i}: role={msg.role.value}, contents={len(msg.contents) if hasattr(msg, 'contents') and msg.contents else 0}" - ) for msg in provider_messages: if msg.role.value == "assistant" and hasattr(msg, "contents") and msg.contents: if any(isinstance(content, FunctionCallContent) for content in msg.contents): conversation_has_tool_calls = True break - if current_state and context.config.state_schema and not conversation_has_tool_calls: + + # Only inject state context on new user turns AND when conversation doesn't have tool calls + # (tool calls indicate we're mid-execution, so state context was already injected) + if current_state and context.config.state_schema and is_new_user_turn and not conversation_has_tool_calls: state_json = json.dumps(current_state, indent=2) state_context_msg = ChatMessage( role="system", contents=[ TextContent( text=f"""Current state of the application: -{state_json} + {state_json} -When modifying state, you MUST include ALL existing data plus your changes. -For example, if adding a new ingredient, include all existing ingredients PLUS the new one. -Never replace existing data - always append or merge.""" + When modifying state, you MUST include ALL existing data plus your changes. + For example, if adding one new item to a list, include ALL existing items PLUS the one new item. + Never replace existing data - always preserve and append or merge.""" ) ], ) @@ -714,12 +720,19 @@ Never replace existing data - always append or merge.""" # Collect all updates to get the final structured output all_updates: list[Any] = [] + update_count = 0 async for update in context.agent.run_stream(messages_to_run, thread=thread, tools=tools_param): + update_count += 1 + logger.info(f"[STREAM] Received update #{update_count} from agent") all_updates.append(update) events = await event_bridge.from_agent_run_update(update) + logger.info(f"[STREAM] Update #{update_count} produced {len(events)} events") for event in events: + logger.info(f"[STREAM] Yielding event: {type(event).__name__}") yield event + logger.info(f"[STREAM] Agent stream completed. Total updates: {update_count}") + # After agent completes, check if we should stop (waiting for user to confirm changes) if event_bridge.should_stop_after_confirm: logger.info("Stopping run after confirm_changes - waiting for user response") @@ -793,9 +806,56 @@ Never replace existing data - always append or merge.""" yield TextMessageEndEvent(message_id=message_id) logger.info(f"Emitted conversational message: {response_dict['message'][:100]}...") + logger.info(f"[FINALIZE] Checking for unclosed message. current_message_id={event_bridge.current_message_id}") if event_bridge.current_message_id: + logger.info(f"[FINALIZE] Emitting TextMessageEndEvent for message_id={event_bridge.current_message_id}") yield event_bridge.create_message_end_event(event_bridge.current_message_id) + # Emit MessagesSnapshotEvent to persist the final assistant text message + from ._message_adapters import agui_messages_to_snapshot_format + + # Build the final assistant message with accumulated text content + assistant_text_message = { + "id": event_bridge.current_message_id, + "role": "assistant", + "content": event_bridge.accumulated_text_content, + } + + # Convert input messages to snapshot format (normalize content structure) + # event_bridge.input_messages are already in AG-UI format, just need normalization + converted_input_messages = agui_messages_to_snapshot_format(event_bridge.input_messages) + + # Build complete messages array + # Include: input messages + any pending tool calls/results + final text message + all_messages = converted_input_messages.copy() + + # Add assistant message with tool calls if any + if event_bridge.pending_tool_calls: + tool_call_message = { + "id": generate_event_id(), + "role": "assistant", + "tool_calls": event_bridge.pending_tool_calls.copy(), + } + all_messages.append(tool_call_message) + + # Add tool results if any + all_messages.extend(event_bridge.tool_results.copy()) + + # Add final text message + all_messages.append(assistant_text_message) + + messages_snapshot = MessagesSnapshotEvent( + messages=all_messages, # type: ignore[arg-type] + ) + logger.info( + f"[FINALIZE] Emitting MessagesSnapshotEvent with {len(all_messages)} messages " + f"(text content length: {len(event_bridge.accumulated_text_content)})" + ) + yield messages_snapshot + else: + logger.info("[FINALIZE] No current_message_id - skipping TextMessageEndEvent") + + logger.info("[FINALIZE] Emitting RUN_FINISHED event") yield event_bridge.create_run_finished_event() logger.info(f"Completed agent run for thread_id={context.thread_id}, run_id={context.run_id}") diff --git a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py index dfdd058bc7..e39fb8c75e 100644 --- a/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py +++ b/python/packages/ag-ui/agent_framework_ag_ui_examples/agents/recipe_agent.py @@ -130,4 +130,5 @@ def recipe_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent: "recipe": {"tool": "update_recipe", "tool_argument": "recipe"}, }, confirmation_strategy=RecipeConfirmationStrategy(), + require_confirmation=False, ) diff --git a/python/packages/ag-ui/pyproject.toml b/python/packages/ag-ui/pyproject.toml index 9b31801054..26208aef04 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.0b251114" +version = "1.0.0b251117" description = "AG-UI protocol integration for Agent Framework" readme = "README.md" license-files = ["LICENSE"] diff --git a/python/packages/ag-ui/tests/test_client.py b/python/packages/ag-ui/tests/test_ag_ui_client.py similarity index 100% rename from python/packages/ag-ui/tests/test_client.py rename to python/packages/ag-ui/tests/test_ag_ui_client.py diff --git a/python/packages/core/agent_framework/_workflows/__init__.py b/python/packages/core/agent_framework/_workflows/__init__.py index 6c4948f0ea..18dd674a92 100644 --- a/python/packages/core/agent_framework/_workflows/__init__.py +++ b/python/packages/core/agent_framework/_workflows/__init__.py @@ -37,6 +37,8 @@ from ._events import ( ExecutorFailedEvent, ExecutorInvokedEvent, RequestInfoEvent, + SuperStepCompletedEvent, + SuperStepStartedEvent, WorkflowErrorDetails, WorkflowEvent, WorkflowEventSource, @@ -152,6 +154,8 @@ __all__ = [ "StandardMagenticManager", "SubWorkflowRequestMessage", "SubWorkflowResponseMessage", + "SuperStepCompletedEvent", + "SuperStepStartedEvent", "SwitchCaseEdgeGroup", "SwitchCaseEdgeGroupCase", "SwitchCaseEdgeGroupDefault", diff --git a/python/packages/core/agent_framework/_workflows/__init__.pyi b/python/packages/core/agent_framework/_workflows/__init__.pyi index 44247d685c..c9f8c6cb62 100644 --- a/python/packages/core/agent_framework/_workflows/__init__.pyi +++ b/python/packages/core/agent_framework/_workflows/__init__.pyi @@ -35,6 +35,8 @@ from ._events import ( ExecutorFailedEvent, ExecutorInvokedEvent, RequestInfoEvent, + SuperStepCompletedEvent, + SuperStepStartedEvent, WorkflowErrorDetails, WorkflowEvent, WorkflowEventSource, @@ -148,6 +150,8 @@ __all__ = [ "StandardMagenticManager", "SubWorkflowRequestMessage", "SubWorkflowResponseMessage", + "SuperStepCompletedEvent", + "SuperStepStartedEvent", "SwitchCaseEdgeGroup", "SwitchCaseEdgeGroupCase", "SwitchCaseEdgeGroupDefault", diff --git a/python/packages/core/agent_framework/_workflows/_agent_executor.py b/python/packages/core/agent_framework/_workflows/_agent_executor.py index 8fa85b7f84..358cee94dd 100644 --- a/python/packages/core/agent_framework/_workflows/_agent_executor.py +++ b/python/packages/core/agent_framework/_workflows/_agent_executor.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import logging +import sys from dataclasses import dataclass from typing import Any, cast @@ -20,6 +21,11 @@ from ._message_utils import normalize_messages_input from ._request_info_mixin import response_handler from ._workflow_context import WorkflowContext +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + logger = logging.getLogger(__name__) @@ -179,7 +185,8 @@ class AgentExecutor(Executor): self._pending_responses_to_agent.clear() await self._run_agent_and_emit(ctx) - async def snapshot_state(self) -> dict[str, Any]: + @override + async def on_checkpoint_save(self) -> dict[str, Any]: """Capture current executor state for checkpointing. NOTE: if the thread storage is on the server side, the full thread state @@ -196,9 +203,6 @@ class AgentExecutor(Executor): client_module = self._agent.chat_client.__class__.__module__ if client_class_name == "AzureAIAgentClient" and "azure_ai" in client_module: - # TODO(TaoChenOSU): update this warning when we surface the hooks for - # custom executor checkpointing. - # https://github.com/microsoft/agent-framework/issues/1816 logger.warning( "Checkpointing an AgentExecutor with AzureAIAgentClient that uses server-side threads. " "Currently, checkpointing does not capture messages from server-side threads " @@ -217,7 +221,8 @@ class AgentExecutor(Executor): "pending_responses_to_agent": encode_checkpoint_value(self._pending_responses_to_agent), } - async def restore_state(self, state: dict[str, Any]) -> None: + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: """Restore executor state from checkpoint. Args: diff --git a/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py b/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py index 5752febab5..8b49de740c 100644 --- a/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py +++ b/python/packages/core/agent_framework/_workflows/_base_group_chat_orchestrator.py @@ -4,6 +4,7 @@ import inspect import logging +import sys from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Sequence from typing import Any @@ -13,6 +14,12 @@ from ._executor import Executor from ._orchestrator_helpers import ParticipantRegistry from ._workflow_context import WorkflowContext +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + logger = logging.getLogger(__name__) @@ -210,11 +217,12 @@ class BaseGroupChatOrchestrator(Executor, ABC): # State persistence (shared across all patterns) - def snapshot_state(self) -> dict[str, Any]: + @override + async def on_checkpoint_save(self) -> dict[str, Any]: """Capture current orchestrator state for checkpointing. Default implementation uses OrchestrationState to serialize common state. - Subclasses should override _snapshot_pattern_metadata() to add pattern-specific data. + Subclasses can override this method or _snapshot_pattern_metadata() to add pattern-specific data. Returns: Serialized state dict @@ -238,11 +246,12 @@ class BaseGroupChatOrchestrator(Executor, ABC): """ return {} - def restore_state(self, state: dict[str, Any]) -> None: + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: """Restore orchestrator state from checkpoint. Default implementation uses OrchestrationState to deserialize common state. - Subclasses should override _restore_pattern_metadata() to restore pattern-specific data. + Subclasses can override this method or _restore_pattern_metadata() to restore pattern-specific data. Args: state: Serialized state dict diff --git a/python/packages/core/agent_framework/_workflows/_conversation_history.py b/python/packages/core/agent_framework/_workflows/_conversation_history.py index 7e19671b27..52d7d99c74 100644 --- a/python/packages/core/agent_framework/_workflows/_conversation_history.py +++ b/python/packages/core/agent_framework/_workflows/_conversation_history.py @@ -6,9 +6,7 @@ These utilities operate on standard `list[ChatMessage]` collections and simple dictionary snapshots so orchestrators can share logic without new mixins. """ -import json -from collections.abc import Mapping, Sequence -from typing import Any +from collections.abc import Sequence from .._types import ChatMessage @@ -26,25 +24,3 @@ def ensure_author(message: ChatMessage, fallback: str) -> ChatMessage: """Attach `fallback` author if message is missing `author_name`.""" message.author_name = message.author_name or fallback return message - - -def snapshot_state(conversation: Sequence[ChatMessage]) -> dict[str, Any]: - """Build an immutable snapshot for checkpoint storage.""" - if hasattr(conversation, "to_dict"): - result = conversation.to_dict() # type: ignore[attr-defined] - if isinstance(result, dict): - return result # type: ignore[return-value] - if isinstance(result, Mapping): - return dict(result) # type: ignore[arg-type] - serialisable: list[dict[str, Any]] = [] - for message in conversation: - if hasattr(message, "to_dict") and callable(message.to_dict): # type: ignore[attr-defined] - msg_dict = message.to_dict() # type: ignore[attr-defined] - serialisable.append(dict(msg_dict) if isinstance(msg_dict, Mapping) else msg_dict) # type: ignore[arg-type] - elif hasattr(message, "to_json") and callable(message.to_json): # type: ignore[attr-defined] - json_payload = message.to_json() # type: ignore[attr-defined] - parsed = json.loads(json_payload) if isinstance(json_payload, str) else json_payload - serialisable.append(dict(parsed) if isinstance(parsed, Mapping) else parsed) # type: ignore[arg-type] - else: - serialisable.append(dict(getattr(message, "__dict__", {}))) # type: ignore[arg-type] - return {"messages": serialisable} diff --git a/python/packages/core/agent_framework/_workflows/_events.py b/python/packages/core/agent_framework/_workflows/_events.py index 76ae7f8a4f..b681544876 100644 --- a/python/packages/core/agent_framework/_workflows/_events.py +++ b/python/packages/core/agent_framework/_workflows/_events.py @@ -294,6 +294,36 @@ class WorkflowOutputEvent(WorkflowEvent): return f"{self.__class__.__name__}(data={self.data}, source_executor_id={self.source_executor_id})" +class SuperStepEvent(WorkflowEvent): + """Event triggered when a superstep starts or ends.""" + + def __init__(self, iteration: int, data: Any | None = None): + """Initialize the superstep event. + + Args: + iteration: The number of the superstep (1-based index). + data: Optional data associated with the superstep event. + """ + super().__init__(data) + self.iteration = iteration + + def __repr__(self) -> str: + """Return a string representation of the superstep event.""" + return f"{self.__class__.__name__}(iteration={self.iteration}, data={self.data})" + + +class SuperStepStartedEvent(SuperStepEvent): + """Event triggered when a superstep starts.""" + + ... + + +class SuperStepCompletedEvent(SuperStepEvent): + """Event triggered when a superstep ends.""" + + ... + + class ExecutorEvent(WorkflowEvent): """Base class for executor events.""" @@ -310,17 +340,13 @@ class ExecutorEvent(WorkflowEvent): class ExecutorInvokedEvent(ExecutorEvent): """Event triggered when an executor handler is invoked.""" - def __repr__(self) -> str: - """Return a string representation of the executor handler invoke event.""" - return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})" + ... class ExecutorCompletedEvent(ExecutorEvent): """Event triggered when an executor handler is completed.""" - def __repr__(self) -> str: - """Return a string representation of the executor handler complete event.""" - return f"{self.__class__.__name__}(executor_id={self.executor_id}, data={self.data})" + ... class ExecutorFailedEvent(ExecutorEvent): diff --git a/python/packages/core/agent_framework/_workflows/_executor.py b/python/packages/core/agent_framework/_workflows/_executor.py index 1563dd7c53..80df16592b 100644 --- a/python/packages/core/agent_framework/_workflows/_executor.py +++ b/python/packages/core/agent_framework/_workflows/_executor.py @@ -155,6 +155,11 @@ class Executor(RequestInfoMixin, DictConvertible): that parent workflows can intercept. See WorkflowExecutor documentation for details on workflow composition patterns and request/response handling. + ## State Management + Executors can contain states that persist across workflow runs and checkpoints. Override the + `on_checkpoint_save` and `on_checkpoint_restore` methods to implement custom state + serialization and restoration logic. + ## Implementation Notes - Do not call `execute()` directly - it's invoked by the workflow engine - Do not override `execute()` - define handlers using decorators instead @@ -460,6 +465,32 @@ class Executor(RequestInfoMixin, DictConvertible): return self._handlers[message_type] raise RuntimeError(f"Executor {self.__class__.__name__} cannot handle message of type {type(message)}.") + async def on_checkpoint_save(self) -> dict[str, Any]: + """Hook called when the workflow is being saved to a checkpoint. + + Override this method in subclasses to implement custom logic that should + return state to be saved in the checkpoint. + + The returned state dictionary will be passed to `on_checkpoint_restore` + when the workflow is restored from the checkpoint. The dictionary should + only contain JSON-serializable data. + + Returns: + A state dictionary to be saved during checkpointing. + """ + return {} + + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + """Hook called when the workflow is restored from a checkpoint. + + Override this method in subclasses to implement custom logic that should + run when the workflow is restored from a checkpoint. + + Args: + state: The state dictionary that was saved during checkpointing. + """ + ... + # endregion: Executor diff --git a/python/packages/core/agent_framework/_workflows/_function_executor.py b/python/packages/core/agent_framework/_workflows/_function_executor.py index f79d85b1a7..417a4ee51b 100644 --- a/python/packages/core/agent_framework/_workflows/_function_executor.py +++ b/python/packages/core/agent_framework/_workflows/_function_executor.py @@ -17,6 +17,7 @@ Design Pattern: import asyncio import inspect +import typing from collections.abc import Awaitable, Callable from typing import Any, overload @@ -218,15 +219,16 @@ def _validate_function_signature(func: Callable[..., Any]) -> tuple[type, Any, l if message_param.annotation == inspect.Parameter.empty: raise ValueError(f"Function instance {func.__name__} must have a type annotation for the message parameter") - message_type = message_param.annotation + type_hints = typing.get_type_hints(func) + message_type = type_hints.get(message_param.name, message_param.annotation) # Check if there's a context parameter if len(params) == 2: ctx_param = params[1] + ctx_annotation = type_hints.get(ctx_param.name, ctx_param.annotation) output_types, workflow_output_types = validate_workflow_context_annotation( - ctx_param.annotation, f"parameter '{ctx_param.name}'", "Function instance" + ctx_annotation, f"parameter '{ctx_param.name}'", "Function instance" ) - ctx_annotation = ctx_param.annotation else: # No context parameter (only valid for function executors) output_types, workflow_output_types = [], [] diff --git a/python/packages/core/agent_framework/_workflows/_handoff.py b/python/packages/core/agent_framework/_workflows/_handoff.py index c29e3f55ad..d18bc59562 100644 --- a/python/packages/core/agent_framework/_workflows/_handoff.py +++ b/python/packages/core/agent_framework/_workflows/_handoff.py @@ -16,6 +16,7 @@ Key properties: import logging import re +import sys from collections.abc import Awaitable, Callable, Mapping, Sequence from dataclasses import dataclass, field from typing import Any @@ -50,6 +51,12 @@ from ._workflow import Workflow from ._workflow_builder import WorkflowBuilder from ._workflow_context import WorkflowContext +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + logger = logging.getLogger(__name__) @@ -307,15 +314,6 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): ctx: WorkflowContext[AgentExecutorRequest | list[ChatMessage], list[ChatMessage] | _ConversationForUserInput], ) -> None: """Process an agent's response and determine whether to route, request input, or terminate.""" - # Hydrate coordinator state (and detect new run) using checkpointable executor state - state = await ctx.get_executor_state() - if not state: - self._clear_conversation() - elif not self._get_conversation(): - restored = self._restore_conversation_from_state(state) - if restored: - self._conversation = list(restored) - source = ctx.get_source_executor_id() is_starting_agent = source == self._starting_agent_id @@ -343,7 +341,7 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): # Update current agent when handoff occurs self._current_agent_id = target logger.info(f"Handoff detected: {source} -> {target}. Routing control to specialist '{target}'.") - await self._persist_state(ctx) + # Clean tool-related content before sending to next agent cleaned = clean_conversation_for_handoff(conversation) request = AgentExecutorRequest(messages=cleaned, should_respond=True) @@ -360,7 +358,6 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): f"Agent '{source}' responded without handoff. " f"Requesting user input. Return-to-previous: {self._return_to_previous}" ) - await self._persist_state(ctx) if await self._check_termination(): # Clean the output conversation for display @@ -388,7 +385,6 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): """Receive full conversation with new user input from gateway, update history, trim for agent.""" # Update authoritative conversation self._conversation = list(message.full_conversation) - await self._persist_state(ctx) # Check termination before sending to agent if await self._check_termination(): @@ -473,11 +469,7 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): ) return list(conversation) - async def _persist_state(self, ctx: WorkflowContext[Any, Any]) -> None: - """Store authoritative conversation snapshot without losing rich metadata.""" - state_payload = self.snapshot_state() - await ctx.set_executor_state(state_payload) - + @override def _snapshot_pattern_metadata(self) -> dict[str, Any]: """Serialize pattern-specific state. @@ -492,6 +484,7 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): } return {} + @override def _restore_pattern_metadata(self, metadata: dict[str, Any]) -> None: """Restore pattern-specific state. @@ -503,17 +496,6 @@ class _HandoffCoordinator(BaseGroupChatOrchestrator): if self._return_to_previous and "current_agent_id" in metadata: self._current_agent_id = metadata["current_agent_id"] - def _restore_conversation_from_state(self, state: Mapping[str, Any]) -> list[ChatMessage]: - """Rehydrate the coordinator's conversation history from checkpointed state. - - DEPRECATED: Use restore_state() instead. Kept for backward compatibility. - """ - from ._orchestration_state import OrchestrationState - - orch_state_dict = {"conversation": state.get("full_conversation", state.get("conversation", []))} - temp_state = OrchestrationState.from_dict(orch_state_dict) - return list(temp_state.conversation) - def _apply_response_metadata(self, conversation: list[ChatMessage], agent_response: AgentRunResponse) -> None: """Merge top-level response metadata into the latest assistant message.""" if not agent_response.additional_properties: diff --git a/python/packages/core/agent_framework/_workflows/_magentic.py b/python/packages/core/agent_framework/_workflows/_magentic.py index 9d21391ad8..ea6fb259a6 100644 --- a/python/packages/core/agent_framework/_workflows/_magentic.py +++ b/python/packages/core/agent_framework/_workflows/_magentic.py @@ -45,9 +45,15 @@ from ._workflow import Workflow, WorkflowRunResult from ._workflow_context import WorkflowContext if sys.version_info >= (3, 11): - from typing import Self # pragma: no cover + from typing import Self else: - from typing_extensions import Self # pragma: no cover + from typing_extensions import Self + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + logger = logging.getLogger(__name__) @@ -673,11 +679,11 @@ class MagenticManagerBase(ABC): """Prepare the final answer.""" ... - def snapshot_state(self) -> dict[str, Any]: + def on_checkpoint_save(self) -> dict[str, Any]: """Serialize runtime state for checkpointing.""" return {} - def restore_state(self, state: dict[str, Any]) -> None: + def on_checkpoint_restore(self, state: dict[str, Any]) -> None: """Restore runtime state from checkpoint data.""" return @@ -695,22 +701,6 @@ class StandardMagenticManager(MagenticManagerBase): task_ledger: _MagenticTaskLedger | None - def snapshot_state(self) -> dict[str, Any]: - state = super().snapshot_state() - if self.task_ledger is not None: - state = dict(state) - state["task_ledger"] = self.task_ledger.to_dict() - return state - - def restore_state(self, state: dict[str, Any]) -> None: - super().restore_state(state) - ledger = state.get("task_ledger") - if ledger is not None: - try: - self.task_ledger = _MagenticTaskLedger.from_dict(ledger) - except Exception: # pragma: no cover - defensive - logger.warning("Failed to restore manager task ledger from checkpoint state") - def __init__( self, chat_client: ChatClientProtocol, @@ -940,6 +930,22 @@ class StandardMagenticManager(MagenticManagerBase): author_name=response.author_name or MAGENTIC_MANAGER_NAME, ) + @override + def on_checkpoint_save(self) -> dict[str, Any]: + state: dict[str, Any] = {} + if self.task_ledger is not None: + state["task_ledger"] = self.task_ledger.to_dict() + return state + + @override + def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + ledger = state.get("task_ledger") + if ledger is not None: + try: + self.task_ledger = _MagenticTaskLedger.from_dict(ledger) + except Exception: # pragma: no cover - defensive + logger.warning("Failed to restore manager task ledger from checkpoint state") + # endregion Magentic Manager @@ -997,7 +1003,6 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): # Terminal state marker to stop further processing after completion/limits self._terminated = False # Tracks whether checkpoint state has been applied for this run - self._state_restored = False def _get_author_name(self) -> str: """Get the magentic manager name for orchestrator-generated messages.""" @@ -1036,7 +1041,8 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): ) await ctx.add_event(event) - def snapshot_state(self) -> dict[str, Any]: + @override + async def on_checkpoint_save(self) -> dict[str, Any]: """Capture current orchestrator state for checkpointing. Uses OrchestrationState for structure but maintains Magentic's complex metadata @@ -1055,14 +1061,16 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): state["magentic_context"] = self._context.to_dict() if self._task_ledger is not None: state["task_ledger"] = _message_to_payload(self._task_ledger) - manager_state: dict[str, Any] | None = None - with contextlib.suppress(Exception): - manager_state = self._manager.snapshot_state() - if manager_state: - state["manager_state"] = manager_state + + try: + state["manager_state"] = self._manager.on_checkpoint_save() + except Exception as exc: + logger.warning("Failed to save manager state for checkpoint: %s\nSkipping...", exc) + return state - def restore_state(self, state: dict[str, Any]) -> None: + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: """Restore orchestrator state from checkpoint. Maintains backward compatibility with existing Magentic checkpoints @@ -1112,7 +1120,7 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): manager_state = state.get("manager_state") if manager_state is not None: try: - self._manager.restore_state(manager_state) + self._manager.on_checkpoint_restore(manager_state) except Exception as exc: # pragma: no cover logger.warning("Failed to restore manager state: %s", exc) @@ -1142,49 +1150,6 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): for name, description in expected.items(): restored[name] = description - def _snapshot_pattern_metadata(self) -> dict[str, Any]: - """Serialize pattern-specific state. - - Magentic uses custom snapshot_state() instead of base class hooks. - This method exists to satisfy the base class contract. - - Returns: - Empty dict (Magentic manages its own state) - """ - return {} - - def _restore_pattern_metadata(self, metadata: dict[str, Any]) -> None: - """Restore pattern-specific state. - - Magentic uses custom restore_state() instead of base class hooks. - This method exists to satisfy the base class contract. - - Args: - metadata: Pattern-specific state dict (ignored) - """ - pass - - async def _ensure_state_restored( - self, - context: WorkflowContext[Any, Any], - ) -> None: - if self._state_restored and self._context is not None: - return - state = await context.get_executor_state() - if not state: - self._state_restored = True - return - if not isinstance(state, dict): - self._state_restored = True - return - try: - self.restore_state(state) - except Exception as exc: # pragma: no cover - logger.warning("Magentic Orchestrator: Failed to apply checkpoint state: %s", exc, exc_info=True) - raise - else: - self._state_restored = True - @handler async def handle_start_message( self, @@ -1204,7 +1169,7 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): ) if message.messages: self._context.chat_history.extend(message.messages) - self._state_restored = True + # Non-streaming callback for the orchestrator receipt of the task await self._emit_orchestrator_message(context, message.task, ORCH_MSG_KIND_USER_TASK) @@ -1269,7 +1234,7 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): """Handle responses from agents.""" if getattr(self, "_terminated", False): return - await self._ensure_state_restored(context) + if self._context is None: raise RuntimeError("Magentic Orchestrator: Received response but not initialized") @@ -1301,7 +1266,7 @@ class MagenticOrchestratorExecutor(BaseGroupChatOrchestrator): ) -> None: if getattr(self, "_terminated", False): return - await self._ensure_state_restored(context) + if self._context is None: return @@ -1636,9 +1601,9 @@ class MagenticAgentExecutor(Executor): self._agent = agent self._agent_id = agent_id self._chat_history: list[ChatMessage] = [] - self._state_restored = False - def snapshot_state(self) -> dict[str, Any]: + @override + async def on_checkpoint_save(self) -> dict[str, Any]: """Capture current executor state for checkpointing. Returns: @@ -1650,7 +1615,8 @@ class MagenticAgentExecutor(Executor): "chat_history": encode_chat_messages(self._chat_history), } - def restore_state(self, state: dict[str, Any]) -> None: + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: """Restore executor state from checkpoint. Args: @@ -1668,24 +1634,6 @@ class MagenticAgentExecutor(Executor): else: self._chat_history = [] - async def _ensure_state_restored(self, context: WorkflowContext[Any, Any]) -> None: - if self._state_restored and self._chat_history: - return - state = await context.get_executor_state() - if not state: - self._state_restored = True - return - if not isinstance(state, dict): - self._state_restored = True - return - try: - self.restore_state(state) - except Exception as exc: # pragma: no cover - logger.warning("Agent %s: Failed to apply checkpoint state: %s", self._agent_id, exc, exc_info=True) - raise - else: - self._state_restored = True - @handler async def handle_response_message( self, message: _MagenticResponseMessage, context: WorkflowContext[_MagenticResponseMessage] @@ -1693,8 +1641,6 @@ class MagenticAgentExecutor(Executor): """Handle response message (task ledger broadcast).""" logger.debug("Agent %s: Received response message", self._agent_id) - await self._ensure_state_restored(context) - # Check if this message is intended for this agent if message.target_agent is not None and message.target_agent != self._agent_id and not message.broadcast: # Message is targeted to a different agent, ignore it @@ -1735,8 +1681,6 @@ class MagenticAgentExecutor(Executor): logger.info("Agent %s: Received request to respond", self._agent_id) - await self._ensure_state_restored(context) - # Add persona adoption message with appropriate role persona_role = self._get_persona_adoption_role() persona_msg = ChatMessage( @@ -1783,7 +1727,6 @@ class MagenticAgentExecutor(Executor): """Reset the internal chat history of the agent (internal operation).""" logger.debug("Agent %s: Resetting chat history", self._agent_id) self._chat_history.clear() - self._state_restored = True async def _emit_agent_delta_event( self, diff --git a/python/packages/core/agent_framework/_workflows/_runner.py b/python/packages/core/agent_framework/_workflows/_runner.py index 51ff79a864..8cc01c23cf 100644 --- a/python/packages/core/agent_framework/_workflows/_runner.py +++ b/python/packages/core/agent_framework/_workflows/_runner.py @@ -11,7 +11,7 @@ from ._checkpoint_encoding import DATACLASS_MARKER, MODEL_MARKER, decode_checkpo from ._const import EXECUTOR_STATE_KEY from ._edge import EdgeGroup from ._edge_runner import EdgeRunner, create_edge_runner -from ._events import WorkflowEvent +from ._events import SuperStepCompletedEvent, SuperStepStartedEvent, WorkflowEvent from ._executor import Executor from ._runner_context import ( Message, @@ -92,6 +92,7 @@ class Runner: while self._iteration < self._max_iterations: logger.info(f"Starting superstep {self._iteration + 1}") + yield SuperStepStartedEvent(iteration=self._iteration + 1) # Run iteration concurrently with live event streaming: we poll # for new events while the iteration coroutine progresses. @@ -126,6 +127,9 @@ class Runner: # Create checkpoint after each superstep iteration await self._create_checkpoint_if_enabled(f"superstep_{self._iteration}") + yield SuperStepCompletedEvent(iteration=self._iteration) + + # Check for convergence: no more messages to process if not await self._ctx.has_messages(): break @@ -183,8 +187,8 @@ class Runner: return None try: - # Auto-snapshot executor states - await self._auto_snapshot_executor_states() + # Snapshot executor states + await self._save_executor_states() checkpoint_category = "initial" if checkpoint_type == "after_initial_execution" else "superstep" metadata = { "superstep": self._iteration, @@ -203,41 +207,6 @@ class Runner: logger.warning(f"Failed to create {checkpoint_type} checkpoint: {e}") return None - async def _auto_snapshot_executor_states(self) -> None: - """Populate executor state by calling snapshot hooks on executors if available. - - TODO(@taochen#1614): this method is potentially problematic if executors also call - set_executor_state on the context directly. We should clarify the intended usage - pattern for executor state management. - - Convention: - - If an executor defines an async or sync method `snapshot_state(self) -> dict`, use it. - - Else if it has a plain attribute `state` that is a dict, use that. - Only JSON-serializable dicts should be provided by executors. - """ - for exec_id, executor in self._executors.items(): - state_dict: dict[str, Any] | None = None - snapshot = getattr(executor, "snapshot_state", None) - try: - if callable(snapshot): - maybe = snapshot() - if asyncio.iscoroutine(maybe): # type: ignore[arg-type] - maybe = await maybe # type: ignore[assignment] - if isinstance(maybe, dict): - state_dict = maybe # type: ignore[assignment] - else: - state_attr = getattr(executor, "state", None) - if isinstance(state_attr, dict): - state_dict = state_attr # type: ignore[assignment] - except Exception as ex: # pragma: no cover - logger.debug(f"Executor {exec_id} snapshot_state failed: {ex}") - - if state_dict is not None: - try: - await self._set_executor_state(exec_id, state_dict) - except Exception as ex: # pragma: no cover - logger.debug(f"Failed to persist state for executor {exec_id}: {ex}") - async def restore_from_checkpoint( self, checkpoint_id: str, @@ -300,7 +269,65 @@ class Runner: logger.error(f"Failed to restore from checkpoint {checkpoint_id}: {e}") return False + async def _save_executor_states(self) -> None: + """Populate executor state by calling checkpoint hooks on executors. + + Backward compatibility behavior: + - If an executor defines an async or sync method `snapshot_state(self) -> dict`, use it. + - Else if it has a plain attribute `state` that is a dict, use that. + + Updated behavior: + - Executors should implement `on_checkpoint_save(self) -> dict` to provide state. + + This method will try the backward compatibility behavior first; if that does not yield state, + it falls back to the updated behavior. + + Only JSON-serializable dicts should be provided by executors. + """ + for exec_id, executor in self._executors.items(): + state_dict: dict[str, Any] | None = None + # Try backward compatibility behavior first + # TODO(@taochen): Remove backward compatibility + snapshot = getattr(executor, "snapshot_state", None) + try: + if callable(snapshot): + maybe = snapshot() + if asyncio.iscoroutine(maybe): # type: ignore[arg-type] + maybe = await maybe # type: ignore[assignment] + if isinstance(maybe, dict): + state_dict = maybe # type: ignore[assignment] + else: + state_attr = getattr(executor, "state", None) + if isinstance(state_attr, dict): + state_dict = state_attr # type: ignore[assignment] + except Exception as ex: # pragma: no cover + logger.debug(f"Executor {exec_id} snapshot_state failed: {ex}") + + if state_dict is None: + # Try the updated behavior only if backward compatibility did not yield state + try: + state_dict = await executor.on_checkpoint_save() + except Exception as ex: # pragma: no cover + raise ValueError(f"Executor {exec_id} on_checkpoint_save failed: {ex}") from ex + + try: + await self._set_executor_state(exec_id, state_dict) + except Exception as ex: # pragma: no cover + logger.debug(f"Failed to persist state for executor {exec_id}: {ex}") + async def _restore_executor_states(self) -> None: + """Restore executor state by calling restore hooks on executors. + + Backward compatibility behavior: + - If an executor defines an async or sync method `restore_state(self, state: dict)`, use it. + - Else, skip restoration for that executor. + + Updated behavior: + - Executors should implement `on_checkpoint_restore(self, state: dict)` to restore state. + + This method will try the backward compatibility behavior first; if that does not restore state, + it falls back to the updated behavior. + """ has_executor_states = await self._shared_state.has(EXECUTOR_STATE_KEY) if not has_executor_states: return @@ -309,16 +336,18 @@ class Runner: if not isinstance(executor_states, dict): raise ValueError("Executor states in shared state is not a dictionary. Unable to restore.") - for executor_id, state in executor_states.items(): + for executor_id, state in executor_states.items(): # pyright: ignore[reportUnknownVariableType] if not isinstance(executor_id, str): raise ValueError("Executor ID in executor states is not a string. Unable to restore.") - if not isinstance(state, dict): - raise ValueError(f"Executor state for {executor_id} is not a dictionary. Unable to restore.") + if not isinstance(state, dict) or not all(isinstance(k, str) for k in state): # pyright: ignore[reportUnknownVariableType] + raise ValueError(f"Executor state for {executor_id} is not a dict[str, Any]. Unable to restore.") executor = self._executors.get(executor_id) if not executor: raise ValueError(f"Executor {executor_id} not found during state restoration.") + # Try backward compatibility behavior first + # TODO(@taochen): Remove backward compatibility restored = False restore_method = getattr(executor, "restore_state", None) try: @@ -330,6 +359,14 @@ class Runner: except Exception as ex: # pragma: no cover - defensive raise ValueError(f"Executor {executor_id} restore_state failed: {ex}") from ex + if not restored: + # Try the updated behavior only if backward compatibility did not restore + try: + await executor.on_checkpoint_restore(state) # pyright: ignore[reportUnknownArgumentType] + restored = True + except Exception as ex: # pragma: no cover - defensive + raise ValueError(f"Executor {executor_id} on_checkpoint_restore failed: {ex}") from ex + if not restored: logger.debug(f"Executor {executor_id} does not support state restoration; skipping.") diff --git a/python/packages/core/agent_framework/_workflows/_workflow.py b/python/packages/core/agent_framework/_workflows/_workflow.py index e5fd02a611..a14542b2a6 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow.py +++ b/python/packages/core/agent_framework/_workflows/_workflow.py @@ -109,9 +109,9 @@ class Workflow(DictConvertible): """A graph-based execution engine that orchestrates connected executors. ## Overview - A workflow executes a directed graph of executors connected via edge groups using a Pregel-like model, - running in supersteps until the graph becomes idle. Workflows are created using the - WorkflowBuilder class - do not instantiate this class directly. + A workflow executes a directed graph of executors connected via edge groups using a + Pregel-like model, running in supersteps until the graph becomes idle. Workflows + are created using the WorkflowBuilder class - do not instantiate this class directly. ## Execution Model Executors run in synchronized supersteps where each executor: @@ -142,6 +142,10 @@ class Workflow(DictConvertible): - HIL continuation: Provide `responses` to continue after RequestInfoExecutor requests - Runtime checkpointing: Provide `checkpoint_storage` to enable/override checkpointing for this run + ## State Management + Workflow instances contain states and states are preserved across calls to `run` and `run_stream`. + To execute multiple independent runs, create separate Workflow instances via WorkflowBuilder. + ## External Input Requests Executors within a workflow can request external input using `ctx.request_info()`: 1. Executor calls `ctx.request_info()` to request input diff --git a/python/packages/core/agent_framework/_workflows/_workflow_builder.py b/python/packages/core/agent_framework/_workflows/_workflow_builder.py index a1b90408be..70f8747ec9 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_builder.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_builder.py @@ -39,7 +39,40 @@ logger = logging.getLogger(__name__) class WorkflowBuilder: """A builder class for constructing workflows. - This class provides methods to add edges and set the starting executor for the workflow. + This class provides a fluent API for defining workflow graphs by connecting executors + with edges and configuring execution parameters. Call :meth:`build` to create an + immutable :class:`Workflow` instance. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class UpperCaseExecutor(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(text.upper()) + + + class ReverseExecutor(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(text[::-1]) + + + # Build a workflow + workflow = ( + WorkflowBuilder() + .add_edge(UpperCaseExecutor(id="upper"), ReverseExecutor(id="reverse")) + .set_start_executor("upper") + .build() + ) + + # Run the workflow + events = await workflow.run("hello") + print(events.get_outputs()) # ['OLLEH'] """ def __init__( @@ -51,7 +84,7 @@ class WorkflowBuilder: """Initialize the WorkflowBuilder with an empty list of edges and no starting executor. Args: - max_iterations: Maximum number of iterations for workflow convergence. + max_iterations: Maximum number of iterations for workflow convergence. Default is 100. name: Optional human-readable name for the workflow. description: Optional description of what the workflow does. """ @@ -164,10 +197,22 @@ class WorkflowBuilder: id: A unique identifier for the executor. If None, the agent's name will be used if available. Returns: - The WorkflowBuilder instance (for method chaining). + Self: The WorkflowBuilder instance for method chaining. Raises: ValueError: If the provided id or agent name conflicts with an existing executor. + + Example: + .. code-block:: python + + from agent_framework import WorkflowBuilder + from agent_framework_anthropic import AnthropicAgent + + # Create an agent + agent = AnthropicAgent(name="writer", model="claude-3-5-sonnet-20241022") + + # Add the agent to a workflow + workflow = WorkflowBuilder().add_agent(agent, output_response=True).set_start_executor(agent).build() """ executor = self._maybe_wrap_agent( agent, agent_thread=agent_thread, output_response=output_response, executor_id=id @@ -184,12 +229,53 @@ class WorkflowBuilder: """Add a directed edge between two executors. The output types of the source and the input types of the target must be compatible. + Messages sent by the source executor will be routed to the target executor. Args: source: The source executor of the edge. target: The target executor of the edge. condition: An optional condition function that determines whether the edge should be traversed based on the message type. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class ProcessorA(Executor): + @handler + async def process(self, data: str, ctx: WorkflowContext[int]) -> None: + await ctx.send_message(len(data)) + + + class ProcessorB(Executor): + @handler + async def process(self, count: int, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(f"Processed {count} characters") + + + # Connect executors with an edge + workflow = ( + WorkflowBuilder().add_edge(ProcessorA(id="a"), ProcessorB(id="b")).set_start_executor("a").build() + ) + + + # With a condition + def only_large_numbers(msg: int) -> bool: + return msg > 100 + + + workflow = ( + WorkflowBuilder() + .add_edge(ProcessorA(id="a"), ProcessorB(id="b"), condition=only_large_numbers) + .set_start_executor("a") + .build() + ) """ # TODO(@taochen): Support executor factories for lazy initialization source_exec = self._maybe_wrap_agent(source) @@ -204,13 +290,50 @@ class WorkflowBuilder: source: Executor | AgentProtocol, targets: Sequence[Executor | AgentProtocol], ) -> Self: - """Add multiple edges to the workflow where messages from the source will be sent to all target. + """Add multiple edges to the workflow where messages from the source will be sent to all targets. The output types of the source and the input types of the targets must be compatible. + Messages from the source will be broadcast to all target executors concurrently. Args: source: The source executor of the edges. targets: A list of target executors for the edges. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class DataSource(Executor): + @handler + async def generate(self, count: int, ctx: WorkflowContext[str]) -> None: + for i in range(count): + await ctx.send_message(f"data_{i}") + + + class ValidatorA(Executor): + @handler + async def validate(self, data: str, ctx: WorkflowContext) -> None: + print(f"ValidatorA: {data}") + + + class ValidatorB(Executor): + @handler + async def validate(self, data: str, ctx: WorkflowContext) -> None: + print(f"ValidatorB: {data}") + + + # Broadcast to multiple validators + workflow = ( + WorkflowBuilder() + .add_fan_out_edges(DataSource(id="source"), [ValidatorA(id="val_a"), ValidatorB(id="val_b")]) + .set_start_executor("source") + .build() + ) """ source_exec = self._maybe_wrap_agent(source) target_execs = [self._maybe_wrap_agent(t) for t in targets] @@ -241,6 +364,53 @@ class WorkflowBuilder: Args: source: The source executor of the edges. cases: A list of case objects that determine the target executor for each message. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler, Case, Default + from dataclasses import dataclass + + + @dataclass + class Result: + score: int + + + class Evaluator(Executor): + @handler + async def evaluate(self, text: str, ctx: WorkflowContext[Result]) -> None: + await ctx.send_message(Result(score=len(text))) + + + class HighScoreHandler(Executor): + @handler + async def handle(self, result: Result, ctx: WorkflowContext) -> None: + print(f"High score: {result.score}") + + + class LowScoreHandler(Executor): + @handler + async def handle(self, result: Result, ctx: WorkflowContext) -> None: + print(f"Low score: {result.score}") + + + # Route based on score value + workflow = ( + WorkflowBuilder() + .add_switch_case_edge_group( + Evaluator(id="eval"), + [ + Case(condition=lambda r: r.score > 10, target=HighScoreHandler(id="high")), + Default(target=LowScoreHandler(id="low")), + ], + ) + .set_start_executor("eval") + .build() + ) """ source_exec = self._maybe_wrap_agent(source) source_id = self._add_executor(source_exec) @@ -270,13 +440,67 @@ class WorkflowBuilder: Messages from the source executor will be sent to multiple target executors based on the provided selection function. - The selection function should take a message and the name of the target executors, - and return a list of indices indicating which target executors should receive the message. + The selection function should take a message and a list of target executor IDs, + and return a list of executor IDs indicating which target executors should receive the message. Args: source: The source executor of the edges. targets: A list of target executors for the edges. selection_func: A function that selects target executors for messages. + Takes (message, list[executor_id]) and returns list[executor_id]. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + from dataclasses import dataclass + + + @dataclass + class Task: + priority: str + data: str + + + class TaskDispatcher(Executor): + @handler + async def dispatch(self, text: str, ctx: WorkflowContext[Task]) -> None: + priority = "high" if len(text) > 10 else "low" + await ctx.send_message(Task(priority=priority, data=text)) + + + class WorkerA(Executor): + @handler + async def process(self, task: Task, ctx: WorkflowContext) -> None: + print(f"WorkerA processing: {task.data}") + + + class WorkerB(Executor): + @handler + async def process(self, task: Task, ctx: WorkflowContext) -> None: + print(f"WorkerB processing: {task.data}") + + + # Select workers based on task priority + def select_workers(task: Task, executor_ids: list[str]) -> list[str]: + if task.priority == "high": + return executor_ids # Send to all workers + return [executor_ids[0]] # Send to first worker only + + + workflow = ( + WorkflowBuilder() + .add_multi_selection_edge_group( + TaskDispatcher(id="dispatcher"), + [WorkerA(id="worker_a"), WorkerB(id="worker_b")], + selection_func=select_workers, + ) + .set_start_executor("dispatcher") + .build() + ) """ source_exec = self._maybe_wrap_agent(source) target_execs = [self._maybe_wrap_agent(t) for t in targets] @@ -298,31 +522,42 @@ class WorkflowBuilder: The target executor will receive a list of messages aggregated from all source executors. Thus the input types of the target executor must be compatible with a list of the output - types of the source executors. For example: - - class Target(Executor): - @handler - def handle_messages(self, messages: list[Message]) -> None: - # Process the aggregated messages from all sources - - class Source(Executor): - @handler(output_type=[Message]) - def handle_message(self, message: Message) -> None: - # Send a message to the target executor - self.send_message(message) - - workflow = ( - WorkflowBuilder() - .add_fan_in_edges( - [Source(id="source1"), Source(id="source2")], - Target(id="target") - ) - .build() - ) + types of the source executors. Args: sources: A list of source executors for the edges. target: The target executor for the edges. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class Producer(Executor): + @handler + async def produce(self, seed: int, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(f"result_{seed}") + + + class Aggregator(Executor): + @handler + async def aggregate(self, results: list[str], ctx: WorkflowContext[Never, str]) -> None: + combined = ", ".join(results) + await ctx.yield_output(f"Combined: {combined}") + + + # Collect results from multiple producers + workflow = ( + WorkflowBuilder() + .add_fan_in_edges([Producer(id="prod_1"), Producer(id="prod_2")], Aggregator(id="agg")) + .set_start_executor("prod_1") + .build() + ) """ source_execs = [self._maybe_wrap_agent(s) for s in sources] target_exec = self._maybe_wrap_agent(target) @@ -342,6 +577,42 @@ class WorkflowBuilder: Args: executors: A list of executors to be added to the chain. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class Step1(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(text.upper()) + + + class Step2(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(text[::-1]) + + + class Step3(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(f"Final: {text}") + + + # Chain executors in sequence + workflow = ( + WorkflowBuilder() + .add_chain([Step1(id="step1"), Step2(id="step2"), Step3(id="step3")]) + .set_start_executor("step1") + .build() + ) """ # Wrap each candidate first to ensure stable IDs before adding edges wrapped: list[Executor] = [self._maybe_wrap_agent(e) for e in executors] @@ -352,8 +623,46 @@ class WorkflowBuilder: def set_start_executor(self, executor: Executor | AgentProtocol | str) -> Self: """Set the starting executor for the workflow. + The start executor is the entry point for the workflow. When the workflow is executed, + the initial message will be sent to this executor. + Args: - executor: The starting executor, which can be an Executor instance or its ID. + executor: The starting executor, which can be an Executor instance, AgentProtocol instance, + or the string ID of an executor previously added to the workflow. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class EntryPoint(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(text.upper()) + + + class Processor(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(text) + + + # Set by executor instance + entry = EntryPoint(id="entry") + workflow = WorkflowBuilder().add_edge(entry, Processor(id="proc")).set_start_executor(entry).build() + + # Set by executor ID string + workflow = ( + WorkflowBuilder() + .add_edge(EntryPoint(id="entry"), Processor(id="proc")) + .set_start_executor("entry") + .build() + ) """ if isinstance(executor, str): self._start_executor = executor @@ -370,8 +679,43 @@ class WorkflowBuilder: def set_max_iterations(self, max_iterations: int) -> Self: """Set the maximum number of iterations for the workflow. + When a workflow contains cycles, this limit prevents infinite loops by capping + the total number of executor invocations. The default is 100 iterations. + Args: max_iterations: The maximum number of iterations the workflow will run for convergence. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class StepA(Executor): + @handler + async def process(self, count: int, ctx: WorkflowContext[int]) -> None: + if count < 10: + await ctx.send_message(count + 1) + + + class StepB(Executor): + @handler + async def process(self, count: int, ctx: WorkflowContext[int]) -> None: + await ctx.send_message(count) + + + # Set a custom iteration limit for workflow with cycles + workflow = ( + WorkflowBuilder() + .set_max_iterations(500) + .add_edge(StepA(id="step_a"), StepB(id="step_b")) + .add_edge(StepB(id="step_b"), StepA(id="step_a")) # Cycle + .set_start_executor("step_a") + .build() + ) """ self._max_iterations = max_iterations return self @@ -381,8 +725,48 @@ class WorkflowBuilder: def with_checkpointing(self, checkpoint_storage: CheckpointStorage) -> Self: """Enable checkpointing with the specified storage. + Checkpointing allows workflows to save their state periodically, enabling + pause/resume functionality and recovery from failures. The checkpoint storage + implementation determines where checkpoints are persisted. + Args: - checkpoint_storage: The checkpoint storage to use. + checkpoint_storage: The checkpoint storage implementation to use. + + Returns: + Self: The WorkflowBuilder instance for method chaining. + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + from agent_framework import FileCheckpointStorage + + + class ProcessorA(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[str]) -> None: + await ctx.send_message(text.upper()) + + + class ProcessorB(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(text) + + + # Enable checkpointing with file-based storage + storage = FileCheckpointStorage("./checkpoints") + workflow = ( + WorkflowBuilder() + .add_edge(ProcessorA(id="proc_a"), ProcessorB(id="proc_b")) + .set_start_executor("proc_a") + .with_checkpointing(storage) + .build() + ) + + # Run with checkpoint saving + events = await workflow.run("input") """ self._checkpoint_storage = checkpoint_storage return self @@ -390,15 +774,43 @@ class WorkflowBuilder: def build(self) -> Workflow: """Build and return the constructed workflow. - This method performs validation before building the workflow. + This method performs validation before building the workflow to ensure: + - A starting executor has been set + - All edges connect valid executors + - The graph is properly connected + - Type compatibility between connected executors Returns: - A Workflow instance with the defined edges and starting executor. + Workflow: An immutable Workflow instance ready for execution. Raises: ValueError: If starting executor is not set. WorkflowValidationError: If workflow validation fails (includes EdgeDuplicationError, TypeCompatibilityError, and GraphConnectivityError subclasses). + + Example: + .. code-block:: python + + from typing_extensions import Never + from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler + + + class MyExecutor(Executor): + @handler + async def process(self, text: str, ctx: WorkflowContext[Never, str]) -> None: + await ctx.yield_output(text.upper()) + + + # Build and execute a workflow + workflow = WorkflowBuilder().set_start_executor(MyExecutor(id="executor")).build() + + # The workflow is now immutable and ready to run + events = await workflow.run("hello") + print(events.get_outputs()) # ['HELLO'] + + # Workflows can be reused multiple times + events2 = await workflow.run("world") + print(events2.get_outputs()) # ['WORLD'] """ # Create workflow build span that includes validation and workflow creation with create_workflow_span(OtelAttr.WORKFLOW_BUILD_SPAN) as span: diff --git a/python/packages/core/agent_framework/_workflows/_workflow_context.py b/python/packages/core/agent_framework/_workflows/_workflow_context.py index d2a3648298..dcf6715d62 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_context.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_context.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Generic, Union, cast, get_args, get_origi from opentelemetry.propagate import inject from opentelemetry.trace import SpanKind -from typing_extensions import Never, TypeVar +from typing_extensions import Never, TypeVar, deprecated from ..observability import OtelAttr, create_workflow_span from ._const import EXECUTOR_STATE_KEY @@ -410,6 +410,11 @@ class WorkflowContext(Generic[T_Out, T_W_Out]): """Get the shared state.""" return self._shared_state + @deprecated( + "Override `on_checkpoint_save()` methods instead. " + "For cross-executor state sharing, use set_shared_state() instead. " + "This API will be removed after 12/01/2025." + ) async def set_executor_state(self, state: dict[str, Any]) -> None: """Store executor state in shared state under a reserved key. @@ -428,6 +433,11 @@ class WorkflowContext(Generic[T_Out, T_W_Out]): existing_states[self._executor_id] = state await self._shared_state.set(EXECUTOR_STATE_KEY, existing_states) + @deprecated( + "Override `on_checkpoint_restore()` methods instead. " + "For cross-executor state sharing, use get_shared_state() instead. " + "This API will be removed after 12/01/2025." + ) async def get_executor_state(self) -> dict[str, Any] | None: """Retrieve previously persisted state for this executor, if any.""" has_existing_states = await self._shared_state.has(EXECUTOR_STATE_KEY) diff --git a/python/packages/core/agent_framework/_workflows/_workflow_executor.py b/python/packages/core/agent_framework/_workflows/_workflow_executor.py index 77acbc5a58..cc028f337c 100644 --- a/python/packages/core/agent_framework/_workflows/_workflow_executor.py +++ b/python/packages/core/agent_framework/_workflows/_workflow_executor.py @@ -1,8 +1,8 @@ # Copyright (c) Microsoft. All rights reserved. import asyncio -import contextlib import logging +import sys import uuid from dataclasses import dataclass from typing import TYPE_CHECKING, Any @@ -26,6 +26,12 @@ from ._typing_utils import is_instance_of from ._workflow import WorkflowRunResult from ._workflow_context import WorkflowContext +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + logger = logging.getLogger(__name__) @@ -181,8 +187,7 @@ class WorkflowExecutor(Executor): # Includes all sub-workflow output types # Plus SubWorkflowRequestMessage if sub-workflow can make requests - output_types = workflow.output_types + [SubWorkflowRequestMessage] # if applicable - ``` + output_types = workflow.output_types + [SubWorkflowRequestMessage] # if applicable ## Error Handling WorkflowExecutor propagates sub-workflow failures: @@ -221,23 +226,10 @@ class WorkflowExecutor(Executor): ### Important Considerations **Shared Workflow Instance**: All concurrent executions use the same underlying workflow instance. - For proper isolation, ensure that: - - The wrapped workflow and its executors are stateless - - Executors use WorkflowContext state management instead of instance variables - - Any shared state is managed through WorkflowContext.get_shared_state/set_shared_state + For proper isolation, ensure that the wrapped workflow and its executors are stateless. .. code-block:: python - # Good: Stateless executor using context state - class StatelessExecutor(Executor): - @handler - async def process(self, data: str, ctx: WorkflowContext[str]) -> None: - # Use context state instead of instance variables - state = await ctx.get_executor_state() or {} - state["processed"] = data - await ctx.set_executor_state(state) - - # Avoid: Stateful executor with instance variables class StatefulExecutor(Executor): def __init__(self): @@ -246,23 +238,23 @@ class WorkflowExecutor(Executor): ## Integration with Parent Workflows Parent workflows can intercept sub-workflow requests: - ```python - class ParentExecutor(Executor): - @handler - async def handle_subworkflow_request( - self, - request: SubWorkflowRequestMessage, - ctx: WorkflowContext[SubWorkflowResponseMessage], - ) -> None: - # Handle request locally or forward to external source - if self.can_handle_locally(request): - # Send response back to sub-workflow - response = request.create_response(data="local response data") - await ctx.send_message(response, target_id=request.source_executor_id) - else: - # Forward to external handler - await ctx.request_info(request.source_event, response_type=request.source_event.response_type) - ``` + + .. code-block:: python + class ParentExecutor(Executor): + @handler + async def handle_subworkflow_request( + self, + request: SubWorkflowRequestMessage, + ctx: WorkflowContext[SubWorkflowResponseMessage], + ) -> None: + # Handle request locally or forward to external source + if self.can_handle_locally(request): + # Send response back to sub-workflow + response = request.create_response(data="local response data") + await ctx.send_message(response, target_id=request.source_executor_id) + else: + # Forward to external handler + await ctx.request_info(request.source_event, response_type=request.source_event.response_type) ## Implementation Notes - Sub-workflows run to completion before processing their results @@ -296,7 +288,6 @@ class WorkflowExecutor(Executor): self._execution_contexts: dict[str, ExecutionContext] = {} # execution_id -> ExecutionContext # Map request_id to execution_id for response routing self._request_to_execution: dict[str, str] = {} # request_id -> execution_id - self._state_loaded: bool = False @property def input_types(self) -> list[type[Any]]: @@ -362,8 +353,6 @@ class WorkflowExecutor(Executor): input_data: The input data to send to the sub-workflow. ctx: The workflow context from the parent. """ - await self._ensure_state_loaded(ctx) - # Create execution context for this sub-workflow run execution_id = str(uuid.uuid4()) execution_context = ExecutionContext( @@ -405,8 +394,6 @@ class WorkflowExecutor(Executor): response: The response to a previous request. ctx: The workflow context. """ - await self._ensure_state_loaded(ctx) - # Find the execution context for this request original_request = response.source_event execution_id = self._request_to_execution.get(original_request.request_id) @@ -434,8 +421,6 @@ class WorkflowExecutor(Executor): # Accumulate the response in this execution's context execution_context.collected_responses[original_request.request_id] = response.data - await self._persist_execution_state(ctx) - # Check if we have all expected responses for this execution if len(execution_context.collected_responses) < execution_context.expected_response_count: logger.debug( @@ -459,25 +444,20 @@ class WorkflowExecutor(Executor): if not execution_context.pending_requests: del self._execution_contexts[execution_id] - async def _ensure_state_loaded(self, ctx: WorkflowContext[Any]) -> None: - if self._state_loaded: - return + @override + async def on_checkpoint_save(self) -> dict[str, Any]: + """Get the current state of the WorkflowExecutor for checkpointing purposes.""" + return { + "execution_contexts": { + execution_id: encode_checkpoint_value(execution_context) + for execution_id, execution_context in self._execution_contexts.items() + }, + "request_to_execution": dict(self._request_to_execution), + } - state: dict[str, Any] | None = None - try: - state = await ctx.get_executor_state() - except Exception: - state = None - - if isinstance(state, dict) and state: - with contextlib.suppress(Exception): - await self.restore_state(state) - self._state_loaded = True - else: - self._state_loaded = True - - async def restore_state(self, state: dict[str, Any]) -> None: - """Restore pending request bookkeeping from a checkpoint snapshot.""" + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + """Restore the WorkflowExecutor state from a checkpoint snapshot.""" # Validate the state contains the right keys if "execution_contexts" not in state: raise KeyError("Missing 'execution_contexts' in WorkflowExecutor state.") @@ -529,23 +509,6 @@ class WorkflowExecutor(Executor): for event in request_info_events ]) - self._state_loaded = True - - async def _persist_execution_state(self, ctx: WorkflowContext) -> None: - """Persist the state of the WorkflowExecutor for checkpointing purposes.""" - state = { - "execution_contexts": { - execution_id: encode_checkpoint_value(execution_context) - for execution_id, execution_context in self._execution_contexts.items() - }, - "request_to_execution": dict(self._request_to_execution), - } - - try: - await ctx.set_executor_state(state) - except Exception as exc: # pragma: no cover - transport specific - logger.warning(f"WorkflowExecutor {self.id} failed to persist state: {exc}") - async def _process_workflow_result( self, result: WorkflowRunResult, @@ -635,5 +598,3 @@ class WorkflowExecutor(Executor): ) else: raise RuntimeError(f"Unexpected workflow run state: {workflow_run_state}") - - await self._persist_execution_state(ctx) diff --git a/python/packages/core/agent_framework/observability.py b/python/packages/core/agent_framework/observability.py index 3e44fae23c..d26cc1c4b7 100644 --- a/python/packages/core/agent_framework/observability.py +++ b/python/packages/core/agent_framework/observability.py @@ -1120,7 +1120,7 @@ def _trace_agent_run( span=span, provider_name=provider_name, messages=messages, - system_instructions=getattr(self, "instructions", None), + system_instructions=getattr(getattr(self, "chat_options", None), "instructions", None), ) try: response = await run_func(self, messages=messages, thread=thread, **kwargs) @@ -1189,7 +1189,7 @@ def _trace_agent_run_stream( span=span, provider_name=provider_name, messages=messages, - system_instructions=getattr(self, "instructions", None), + system_instructions=getattr(getattr(self, "chat_options", None), "instructions", None), ) try: async for update in run_streaming_func(self, messages=messages, thread=thread, **kwargs): diff --git a/python/packages/core/tests/workflow/test_agent_executor.py b/python/packages/core/tests/workflow/test_agent_executor.py index 77fd969f12..2815c3152c 100644 --- a/python/packages/core/tests/workflow/test_agent_executor.py +++ b/python/packages/core/tests/workflow/test_agent_executor.py @@ -158,8 +158,8 @@ async def test_agent_executor_checkpoint_stores_and_restores_state() -> None: assert thread_messages[1].text == "Initial response 1" -async def test_agent_executor_snapshot_and_restore_state_directly() -> None: - """Test AgentExecutor's snapshot_state and restore_state methods directly.""" +async def test_agent_executor_save_and_restore_state_directly() -> None: + """Test AgentExecutor's on_checkpoint_save and on_checkpoint_restore methods directly.""" # Create agent with thread containing messages agent = _CountingAgent(id="direct_test_agent", name="DirectTestAgent") thread = AgentThread(message_store=ChatMessageStore()) @@ -182,7 +182,7 @@ async def test_agent_executor_snapshot_and_restore_state_directly() -> None: executor._cache = list(cache_messages) # type: ignore[reportPrivateUsage] # Snapshot the state - state = await executor.snapshot_state() # type: ignore[reportUnknownMemberType] + state = await executor.on_checkpoint_save() # Verify snapshot contains both cache and thread assert "cache" in state @@ -206,7 +206,7 @@ async def test_agent_executor_snapshot_and_restore_state_directly() -> None: assert len(initial_thread_msgs) == 0 # Restore state - await new_executor.restore_state(state) # type: ignore[reportUnknownMemberType] + await new_executor.on_checkpoint_restore(state) # Verify cache is restored restored_cache = new_executor._cache # type: ignore[reportPrivateUsage] diff --git a/python/packages/core/tests/workflow/test_function_executor_future.py b/python/packages/core/tests/workflow/test_function_executor_future.py new file mode 100644 index 0000000000..a4a15aeba0 --- /dev/null +++ b/python/packages/core/tests/workflow/test_function_executor_future.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +from typing import Any + +from agent_framework import FunctionExecutor, WorkflowContext, executor + + +class TestFunctionExecutorFutureAnnotations: + """Test suite for FunctionExecutor with from __future__ import annotations.""" + + def test_executor_decorator_future_annotations(self): + """Test @executor decorator works with stringified annotations.""" + + @executor(id="future_test") + async def process_future(value: int, ctx: WorkflowContext[int]) -> None: + await ctx.send_message(value * 2) + + assert isinstance(process_future, FunctionExecutor) + assert process_future.id == "future_test" + assert int in process_future._handlers + + # Check spec + spec = process_future._handler_specs[0] + assert spec["message_type"] is int + assert spec["output_types"] == [int] + + def test_executor_decorator_future_annotations_complex(self): + """Test @executor decorator works with complex stringified annotations.""" + + @executor + async def process_complex(data: dict[str, Any], ctx: WorkflowContext[list[str]]) -> None: + await ctx.send_message(["done"]) + + assert isinstance(process_complex, FunctionExecutor) + spec = process_complex._handler_specs[0] + assert spec["message_type"] == dict[str, Any] + assert spec["output_types"] == [list[str]] diff --git a/python/packages/core/tests/workflow/test_handoff.py b/python/packages/core/tests/workflow/test_handoff.py index a799fb6f73..5dfd7522df 100644 --- a/python/packages/core/tests/workflow/test_handoff.py +++ b/python/packages/core/tests/workflow/test_handoff.py @@ -288,57 +288,6 @@ def test_build_fails_without_participants(): HandoffBuilder().build() -async def test_multiple_runs_dont_leak_conversation(): - """Verify that running the same workflow multiple times doesn't leak conversation history.""" - triage = _RecordingAgent(name="triage", handoff_to="specialist") - specialist = _RecordingAgent(name="specialist") - - workflow = ( - HandoffBuilder(participants=[triage, specialist]) - .set_coordinator("triage") - .with_termination_condition(lambda conv: sum(1 for m in conv if m.role == Role.USER) >= 2) - .build() - ) - - # First run - events = await _drain(workflow.run_stream("First run message")) - requests = [ev for ev in events if isinstance(ev, RequestInfoEvent)] - assert requests - events = await _drain(workflow.send_responses_streaming({requests[-1].request_id: "Second message"})) - outputs = [ev for ev in events if isinstance(ev, WorkflowOutputEvent)] - assert outputs, "First run should emit output" - - first_run_conversation = outputs[-1].data - assert isinstance(first_run_conversation, list) - first_run_conv_list = cast(list[ChatMessage], first_run_conversation) - first_run_user_messages = [msg for msg in first_run_conv_list if msg.role == Role.USER] - assert len(first_run_user_messages) == 2 - assert any("First run message" in msg.text for msg in first_run_user_messages if msg.text) - - # Second run - should start fresh, not include first run's messages - triage.calls.clear() - specialist.calls.clear() - - events = await _drain(workflow.run_stream("Second run different message")) - requests = [ev for ev in events if isinstance(ev, RequestInfoEvent)] - assert requests - events = await _drain(workflow.send_responses_streaming({requests[-1].request_id: "Another message"})) - outputs = [ev for ev in events if isinstance(ev, WorkflowOutputEvent)] - assert outputs, "Second run should emit output" - - second_run_conversation = outputs[-1].data - assert isinstance(second_run_conversation, list) - second_run_conv_list = cast(list[ChatMessage], second_run_conversation) - second_run_user_messages = [msg for msg in second_run_conv_list if msg.role == Role.USER] - assert len(second_run_user_messages) == 2, ( - "Second run should have exactly 2 user messages, not accumulate first run" - ) - assert any("Second run different message" in msg.text for msg in second_run_user_messages if msg.text) - assert not any("First run message" in msg.text for msg in second_run_user_messages if msg.text), ( - "Second run should NOT contain first run's messages" - ) - - async def test_handoff_async_termination_condition() -> None: """Test that async termination conditions work correctly.""" termination_call_count = 0 @@ -585,7 +534,7 @@ async def test_return_to_previous_state_serialization(): coordinator._current_agent_id = "specialist_a" # type: ignore[reportPrivateUsage] # Snapshot the state - state = coordinator.snapshot_state() + state = await coordinator.on_checkpoint_save() # Verify pattern metadata includes current_agent_id assert "metadata" in state @@ -603,7 +552,7 @@ async def test_return_to_previous_state_serialization(): ) # Restore state - coordinator2.restore_state(state) + await coordinator2.on_checkpoint_restore(state) # Verify current_agent_id was restored assert coordinator2._current_agent_id == "specialist_a", "Current agent should be restored from checkpoint" # type: ignore[reportPrivateUsage] diff --git a/python/packages/core/tests/workflow/test_magentic.py b/python/packages/core/tests/workflow/test_magentic.py index eda0675361..cc1e8ad132 100644 --- a/python/packages/core/tests/workflow/test_magentic.py +++ b/python/packages/core/tests/workflow/test_magentic.py @@ -1,5 +1,6 @@ # Copyright (c) Microsoft. All rights reserved. +import sys from collections.abc import AsyncIterable from dataclasses import dataclass from typing import Any, cast @@ -42,6 +43,11 @@ from agent_framework._workflows._magentic import ( # type: ignore[reportPrivate _MagenticStartMessage, # type: ignore ) +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + def test_magentic_start_message_from_string(): msg = _MagenticStartMessage.from_string("Do the thing") @@ -101,8 +107,9 @@ class FakeManager(MagenticManagerBase): next_speaker_name: str = "agentA" instruction_text: str = "Proceed with step 1" - def snapshot_state(self) -> dict[str, Any]: - state = super().snapshot_state() + @override + def on_checkpoint_save(self) -> dict[str, Any]: + state = super().on_checkpoint_save() if self.task_ledger is not None: state = dict(state) state["task_ledger"] = { @@ -111,8 +118,9 @@ class FakeManager(MagenticManagerBase): } return state - def restore_state(self, state: dict[str, Any]) -> None: - super().restore_state(state) + @override + def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + super().on_checkpoint_restore(state) ledger_state = state.get("task_ledger") if isinstance(ledger_state, dict): ledger_dict = cast(dict[str, Any], ledger_state) @@ -185,7 +193,6 @@ async def test_standard_manager_progress_ledger_and_fallback(): assert ledger2.is_request_satisfied.answer is False -@pytest.mark.skip(reason="Response handling refactored - responses no longer passed to run_stream()") async def test_magentic_workflow_plan_review_approval_to_completion(): manager = FakeManager(max_round_count=10) wf = ( @@ -204,7 +211,7 @@ async def test_magentic_workflow_plan_review_approval_to_completion(): completed = False output: ChatMessage | None = None - async for ev in wf.run_stream( + async for ev in wf.send_responses_streaming( responses={req_event.request_id: MagenticPlanReviewReply(decision=MagenticPlanReviewDecision.APPROVE)} ): if isinstance(ev, WorkflowStatusEvent) and ev.state == WorkflowRunState.IDLE: @@ -218,7 +225,6 @@ async def test_magentic_workflow_plan_review_approval_to_completion(): assert isinstance(output, ChatMessage) -@pytest.mark.skip(reason="Response handling refactored - responses no longer passed to run_stream()") async def test_magentic_plan_review_approve_with_comments_replans_and_proceeds(): class CountingManager(FakeManager): # Declare as a model field so assignment is allowed under Pydantic @@ -250,7 +256,7 @@ async def test_magentic_plan_review_approve_with_comments_replans_and_proceeds() # Reply APPROVE with comments (no edited text). Expect one replan and no second review round. saw_second_review = False completed = False - async for ev in wf.run_stream( + async for ev in wf.send_responses_streaming( responses={ req_event.request_id: MagenticPlanReviewReply( decision=MagenticPlanReviewDecision.APPROVE, @@ -298,7 +304,6 @@ async def test_magentic_orchestrator_round_limit_produces_partial_result(): assert data.role == Role.ASSISTANT -@pytest.mark.skip(reason="Response handling refactored - send_responses_streaming no longer exists") async def test_magentic_checkpoint_resume_round_trip(): storage = InMemoryCheckpointStorage() @@ -369,7 +374,7 @@ class _DummyExec(Executor): pass -def test_magentic_agent_executor_snapshot_roundtrip(): +async def test_magentic_agent_executor_on_checkpoint_save_and_restore_roundtrip(): backing_executor = _DummyExec("backing") agent_exec = MagenticAgentExecutor(backing_executor, "agentA") agent_exec._chat_history.extend([ # type: ignore[reportPrivateUsage] @@ -377,10 +382,10 @@ def test_magentic_agent_executor_snapshot_roundtrip(): ChatMessage(role=Role.ASSISTANT, text="world", author_name="agentA"), ]) - state = agent_exec.snapshot_state() + state = await agent_exec.on_checkpoint_save() restored_executor = MagenticAgentExecutor(_DummyExec("backing2"), "agentA") - restored_executor.restore_state(state) + await restored_executor.on_checkpoint_restore(state) assert len(restored_executor._chat_history) == 2 # type: ignore[reportPrivateUsage] assert restored_executor._chat_history[0].text == "hello" # type: ignore[reportPrivateUsage] diff --git a/python/packages/core/tests/workflow/test_workflow.py b/python/packages/core/tests/workflow/test_workflow.py index 7f1a7fdce6..059c94803e 100644 --- a/python/packages/core/tests/workflow/test_workflow.py +++ b/python/packages/core/tests/workflow/test_workflow.py @@ -199,7 +199,10 @@ async def test_fan_out(): # Each executor will emit two events: ExecutorInvokedEvent and ExecutorCompletedEvent # executor_b will also emit a WorkflowOutputEvent (no WorkflowCompletedEvent anymore) - assert len(events) == 7 + # Each superstep will emit also emit a WorkflowStartedEvent and WorkflowCompletedEvent + # This workflow will converge in 2 supersteps because executor_c will send one more message + # after executor_b completes + assert len(events) == 11 assert events.get_final_state() == WorkflowRunState.IDLE outputs = events.get_outputs() @@ -220,7 +223,9 @@ async def test_fan_out_multiple_completed_events(): # Each executor will emit two events: ExecutorInvokedEvent and ExecutorCompletedEvent # executor_b and executor_c will also emit a WorkflowOutputEvent (no WorkflowCompletedEvent anymore) - assert len(events) == 8 + # Each superstep will emit also emit a WorkflowStartedEvent and WorkflowCompletedEvent + # This workflow will converge in 1 superstep because executor_a and executor_b will not send further messages + assert len(events) == 10 # Multiple outputs are expected from both executors outputs = events.get_outputs() @@ -246,7 +251,8 @@ async def test_fan_in(): # Each executor will emit two events: ExecutorInvokedEvent and ExecutorCompletedEvent # aggregator will also emit a WorkflowOutputEvent (no WorkflowCompletedEvent anymore) - assert len(events) == 9 + # Each superstep will emit also emit a WorkflowStartedEvent and WorkflowCompletedEvent + assert len(events) == 13 assert events.get_final_state() == WorkflowRunState.IDLE outputs = events.get_outputs() diff --git a/python/packages/devui/agent_framework_devui/_deployment.py b/python/packages/devui/agent_framework_devui/_deployment.py index e1cf1d5c3d..45f99a315a 100644 --- a/python/packages/devui/agent_framework_devui/_deployment.py +++ b/python/packages/devui/agent_framework_devui/_deployment.py @@ -10,6 +10,7 @@ import uuid from collections.abc import AsyncGenerator from datetime import datetime, timezone from pathlib import Path +from urllib.parse import urlparse from .models._discovery_models import Deployment, DeploymentConfig, DeploymentEvent @@ -467,11 +468,18 @@ CMD ["devui", "/app/entity", "--mode", "{config.ui_mode}", "--host", "0.0.0.0", await event_queue.put( DeploymentEvent(type="deploy.progress", message=f"Docker build: {line_text}") ) - elif "https://" in line_text and ".azurecontainerapps.io" in line_text: - # Deployment URL detected - await event_queue.put( - DeploymentEvent(type="deploy.progress", message="Deployment URL generated!") - ) + elif "https://" in line_text: + # Try to extract all URLs and check if any is on azurecontainerapps.io + urls = re.findall(r'https://[^\s<>"]+', line_text) + for url in urls: + # Strip common trailing punctuation to ensure clean URL parsing + url_clean = url.rstrip(".,;:!?'\")}]") + host = urlparse(url_clean).hostname + if host and (host == "azurecontainerapps.io" or host.endswith(".azurecontainerapps.io")): + await event_queue.put( + DeploymentEvent(type="deploy.progress", message="Deployment URL generated!") + ) + break # Wait for process to complete return_code = await process.wait() diff --git a/python/packages/devui/frontend/yarn.lock b/python/packages/devui/frontend/yarn.lock index 8303d2d478..7e932da19e 100644 --- a/python/packages/devui/frontend/yarn.lock +++ b/python/packages/devui/frontend/yarn.lock @@ -24,7 +24,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.28.3": +"@babel/core@^7.28.3": version "7.28.3" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz" integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== @@ -168,6 +168,153 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@emnapi/core@^1.4.3", "@emnapi/core@^1.4.5": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4" + integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg== + dependencies: + "@emnapi/wasi-threads" "1.1.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.4.5": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.0.4": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== + dependencies: + tslib "^2.4.0" + +"@esbuild/aix-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9" + integrity sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA== + +"@esbuild/android-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz#d2e70be7d51a529425422091e0dcb90374c1546c" + integrity sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg== + +"@esbuild/android-arm@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.9.tgz#d2a753fe2a4c73b79437d0ba1480e2d760097419" + integrity sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ== + +"@esbuild/android-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.9.tgz#5278836e3c7ae75761626962f902a0d55352e683" + integrity sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw== + +"@esbuild/darwin-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz#f1513eaf9ec8fa15dcaf4c341b0f005d3e8b47ae" + integrity sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg== + +"@esbuild/darwin-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz#e27dbc3b507b3a1cea3b9280a04b8b6b725f82be" + integrity sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ== + +"@esbuild/freebsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz#364e3e5b7a1fd45d92be08c6cc5d890ca75908ca" + integrity sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q== + +"@esbuild/freebsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz#7c869b45faeb3df668e19ace07335a0711ec56ab" + integrity sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg== + +"@esbuild/linux-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz#48d42861758c940b61abea43ba9a29b186d6cb8b" + integrity sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw== + +"@esbuild/linux-arm@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz#6ce4b9cabf148274101701d112b89dc67cc52f37" + integrity sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw== + +"@esbuild/linux-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz#207e54899b79cac9c26c323fc1caa32e3143f1c4" + integrity sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A== + +"@esbuild/linux-loong64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz#0ba48a127159a8f6abb5827f21198b999ffd1fc0" + integrity sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ== + +"@esbuild/linux-mips64el@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz#a4d4cc693d185f66a6afde94f772b38ce5d64eb5" + integrity sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA== + +"@esbuild/linux-ppc64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz#0f5805c1c6d6435a1dafdc043cb07a19050357db" + integrity sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w== + +"@esbuild/linux-riscv64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz#6776edece0f8fca79f3386398b5183ff2a827547" + integrity sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg== + +"@esbuild/linux-s390x@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz#3f6f29ef036938447c2218d309dc875225861830" + integrity sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA== + +"@esbuild/linux-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz#831fe0b0e1a80a8b8391224ea2377d5520e1527f" + integrity sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg== + +"@esbuild/netbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz#06f99d7eebe035fbbe43de01c9d7e98d2a0aa548" + integrity sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q== + +"@esbuild/netbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz#db99858e6bed6e73911f92a88e4edd3a8c429a52" + integrity sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g== + +"@esbuild/openbsd-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz#afb886c867e36f9d86bb21e878e1185f5d5a0935" + integrity sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ== + +"@esbuild/openbsd-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz#30855c9f8381fac6a0ef5b5f31ac6e7108a66ecf" + integrity sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA== + +"@esbuild/openharmony-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz#2f2144af31e67adc2a8e3705c20c2bd97bd88314" + integrity sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg== + +"@esbuild/sunos-x64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz#69b99a9b5bd226c9eb9c6a73f990fddd497d732e" + integrity sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw== + +"@esbuild/win32-arm64@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz#d789330a712af916c88325f4ffe465f885719c6b" + integrity sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ== + +"@esbuild/win32-ia32@0.25.9": + version "0.25.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz#52fc735406bd49688253e74e4e837ac2ba0789e3" + integrity sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww== + "@esbuild/win32-x64@0.25.9": version "0.25.9" resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz" @@ -221,7 +368,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^9.33.0", "@eslint/js@9.33.0": +"@eslint/js@9.33.0", "@eslint/js@^9.33.0": version "9.33.0" resolved "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz" integrity sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A== @@ -335,6 +482,15 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@napi-rs/wasm-runtime@^0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" + integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== + dependencies: + "@emnapi/core" "^1.4.3" + "@emnapi/runtime" "^1.4.3" + "@tybys/wasm-util" "^0.10.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -343,7 +499,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -693,6 +849,101 @@ resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz" integrity sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g== +"@rollup/rollup-android-arm-eabi@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz#6e236cd2fd29bb01a300ad4ff6ed0f1a17550e69" + integrity sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g== + +"@rollup/rollup-android-arm64@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz#808f2c9c7e68161add613ebcb0eac5a058a0df3c" + integrity sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA== + +"@rollup/rollup-darwin-arm64@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz#fa41e413c8e73d61039d6375b234595f24b1e5e3" + integrity sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ== + +"@rollup/rollup-darwin-x64@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz#9aac64e886435493f2e3a0aa5e4aad098a90814c" + integrity sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ== + +"@rollup/rollup-freebsd-arm64@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz#9fc804264f7b7a7cdad3747950299f990163be1f" + integrity sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw== + +"@rollup/rollup-freebsd-x64@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz#933feaff864feb03bbbcd0c18ea351ade957cf79" + integrity sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA== + +"@rollup/rollup-linux-arm-gnueabihf@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz#02915e6b2c55fe5961c27404aba2d9c8ef48ac6c" + integrity sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A== + +"@rollup/rollup-linux-arm-musleabihf@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz#1afef33191b26e76ae7f0d0dc767efc6be1285ce" + integrity sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q== + +"@rollup/rollup-linux-arm64-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz#6e7f38fb99d14143de3ce33204e6cd61e1c2c780" + integrity sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA== + +"@rollup/rollup-linux-arm64-musl@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz#25ab09f14bbcba85a604bcee2962d2486db90794" + integrity sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA== + +"@rollup/rollup-linux-loongarch64-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz#d3e3a3fd61e21b2753094391dee9b515a2bc9ecd" + integrity sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg== + +"@rollup/rollup-linux-ppc64-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz#6b44445e2bd5866692010de241bf18d2ae8b0cb8" + integrity sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ== + +"@rollup/rollup-linux-riscv64-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz#3ff412d20d3b157e6aadabf84788e8c5cb221ba7" + integrity sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow== + +"@rollup/rollup-linux-riscv64-musl@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz#104f451497d53d82a49c6d08c13c59f5f30eed57" + integrity sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw== + +"@rollup/rollup-linux-s390x-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz#d04de7b21d181f30750760cb3553946306506172" + integrity sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ== + +"@rollup/rollup-linux-x64-gnu@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz#a6ba88ff7480940a435b1e67ddbb3f207a7ae02f" + integrity sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g== + +"@rollup/rollup-linux-x64-musl@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz#c912c8ffa0c242ed3175cd91cdeaef98109afa54" + integrity sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ== + +"@rollup/rollup-win32-arm64-msvc@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz#ca5eaae89443554b461bb359112a056528cfdac0" + integrity sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA== + +"@rollup/rollup-win32-ia32-msvc@4.47.1": + version "4.47.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz#34e76172515fb4b374eb990d59f54faff938246e" + integrity sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ== + "@rollup/rollup-win32-x64-msvc@4.47.1": version "4.47.1" resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz" @@ -711,6 +962,68 @@ source-map-js "^1.2.1" tailwindcss "4.1.12" +"@tailwindcss/oxide-android-arm64@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz#27920fe61fa2743afe8a8ca296fa640b609d17d5" + integrity sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ== + +"@tailwindcss/oxide-darwin-arm64@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz#e8bd4798f26ec1d012bf0683aeb77449f71505cd" + integrity sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw== + +"@tailwindcss/oxide-darwin-x64@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz#8ddb7e5ddfd9b049ec84a2bda99f2b04a86859f5" + integrity sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg== + +"@tailwindcss/oxide-freebsd-x64@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz#da1c0b16b7a5f95a1e400f299a3ec94fb6fd40ac" + integrity sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz#34e558aa6e869c6fe9867cb78ed7ba651b9fcaa4" + integrity sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ== + +"@tailwindcss/oxide-linux-arm64-gnu@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz#0a00a8146ab6215f81b2d385056c991441bf390e" + integrity sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g== + +"@tailwindcss/oxide-linux-arm64-musl@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz#b138f494068884ae0d8c343dc1904b22f5e98dc6" + integrity sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA== + +"@tailwindcss/oxide-linux-x64-gnu@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz#5b9d5f23b15cdb714639f5b9741c0df5d610f794" + integrity sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q== + +"@tailwindcss/oxide-linux-x64-musl@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz#f68ec530d3ca6875ea9015bcd5dd0762ee5e2f5d" + integrity sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A== + +"@tailwindcss/oxide-wasm32-wasi@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz#9fd15a1ebde6076c42c445c5e305c31673ead965" + integrity sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg== + dependencies: + "@emnapi/core" "^1.4.5" + "@emnapi/runtime" "^1.4.5" + "@emnapi/wasi-threads" "^1.0.4" + "@napi-rs/wasm-runtime" "^0.2.12" + "@tybys/wasm-util" "^0.10.0" + tslib "^2.8.0" + +"@tailwindcss/oxide-win32-arm64-msvc@4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz#938bcc6a82e1120ea4fe2ce94be0a8cdf3ae92c7" + integrity sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg== + "@tailwindcss/oxide-win32-x64-msvc@4.1.12": version "4.1.12" resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz" @@ -746,6 +1059,13 @@ "@tailwindcss/oxide" "4.1.12" tailwindcss "4.1.12" +"@tybys/wasm-util@^0.10.0": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== + dependencies: + tslib "^2.4.0" + "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" @@ -818,7 +1138,7 @@ "@types/d3-interpolate" "*" "@types/d3-selection" "*" -"@types/estree@^1.0.6", "@types/estree@1.0.8": +"@types/estree@1.0.8", "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -828,19 +1148,19 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/node@^20.19.0 || >=22.12.0", "@types/node@^24.3.0": +"@types/node@^24.3.0": version "24.3.0" resolved "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz" integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== dependencies: undici-types "~7.10.0" -"@types/react-dom@*", "@types/react-dom@^19.1.7": +"@types/react-dom@^19.1.7": version "19.1.7" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz" integrity sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw== -"@types/react@*", "@types/react@^19.0.0", "@types/react@^19.1.10", "@types/react@>=16.8", "@types/react@>=18.0.0": +"@types/react@^19.1.10": version "19.1.10" resolved "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz" integrity sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg== @@ -862,7 +1182,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^8.40.0", "@typescript-eslint/parser@8.40.0": +"@typescript-eslint/parser@8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz" integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw== @@ -890,7 +1210,7 @@ "@typescript-eslint/types" "8.40.0" "@typescript-eslint/visitor-keys" "8.40.0" -"@typescript-eslint/tsconfig-utils@^8.40.0", "@typescript-eslint/tsconfig-utils@8.40.0": +"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz" integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw== @@ -906,7 +1226,7 @@ debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@8.40.0": +"@typescript-eslint/types@8.40.0", "@typescript-eslint/types@^8.40.0": version "8.40.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz" integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg== @@ -986,7 +1306,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.15.0: +acorn@^8.15.0: version "8.15.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -1047,7 +1367,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0, "browserslist@>= 4.21.0": +browserslist@^4.24.0: version "4.25.3" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz" integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ== @@ -1143,7 +1463,7 @@ csstype@^3.0.2: resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== -d3-drag@^3.0.0, "d3-drag@2 - 3": +"d3-drag@2 - 3", d3-drag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== @@ -1156,14 +1476,14 @@ d3-drag@^3.0.0, "d3-drag@2 - 3": resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== -d3-interpolate@^3.0.1, "d3-interpolate@1 - 3": +"d3-interpolate@1 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== dependencies: d3-color "1 - 3" -d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3: +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== @@ -1300,7 +1620,7 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -"eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^8.57.0 || ^9.0.0", eslint@^9.33.0, eslint@>=8.40: +eslint@^9.33.0: version "9.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz" integrity sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA== @@ -1546,7 +1866,7 @@ isexe@^2.0.0: resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jiti@*, jiti@^2.5.1, jiti@>=1.21.0: +jiti@^2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz" integrity sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w== @@ -1557,9 +1877,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" @@ -1603,12 +1923,57 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz#3d47ce5e221b9567c703950edf2529ca4a3700ae" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + lightningcss-win32-x64-msvc@1.30.1: version "1.30.1" resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz" integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== -lightningcss@^1.21.0, lightningcss@1.30.1: +lightningcss@1.30.1: version "1.30.1" resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== @@ -1779,7 +2144,7 @@ picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -"picomatch@^3 || ^4", picomatch@^4.0.3: +picomatch@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== @@ -1808,7 +2173,7 @@ queue-microtask@^1.2.2: resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -"react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", react-dom@^19.1.1, react-dom@>=16.8.0, react-dom@>=17: +react-dom@^19.1.1: version "19.1.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz" integrity sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw== @@ -1847,7 +2212,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^19.1.1, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18.0.0: +react@^19.1.1: version "19.1.1" resolved "https://registry.npmjs.org/react/-/react-19.1.1.tgz" integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== @@ -1947,7 +2312,7 @@ tailwind-merge@^3.3.1: resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz" integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== -tailwindcss@^4.1.12, tailwindcss@4.1.12: +tailwindcss@4.1.12, tailwindcss@^4.1.12: version "4.1.12" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz" integrity sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA== @@ -1989,7 +2354,7 @@ ts-api-utils@^2.1.0: resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== -tslib@^2.0.0, tslib@^2.1.0: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -2016,7 +2381,7 @@ typescript-eslint@^8.39.1: "@typescript-eslint/typescript-estree" "8.40.0" "@typescript-eslint/utils" "8.40.0" -typescript@>=4.8.4, "typescript@>=4.8.4 <6.0.0", typescript@~5.8.3: +typescript@~5.8.3: version "5.8.3" resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== @@ -2056,12 +2421,12 @@ use-sidecar@^1.1.3: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.2, use-sync-external-store@>=1.2.0: +use-sync-external-store@^1.2.2: version "1.5.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== -"vite@^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.2.0 || ^6 || ^7", vite@^7.1.11: +vite@^7.1.11: version "7.1.12" resolved "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz" integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug== diff --git a/python/packages/devui/tests/test_checkpoints.py b/python/packages/devui/tests/test_checkpoints.py index 473b4faac0..838195fdd3 100644 --- a/python/packages/devui/tests/test_checkpoints.py +++ b/python/packages/devui/tests/test_checkpoints.py @@ -37,10 +37,14 @@ class WorkflowHILRequest: class WorkflowTestExecutor(Executor): """Test executor with HIL.""" + def __init__(self, id: str) -> None: + super().__init__(id=id) + self._data_value: str | None = None + @handler async def process(self, data: WorkflowTestData, ctx: WorkflowContext) -> None: """Process data and request approval.""" - await ctx.set_executor_state({"data_value": data.value}) + self._data_value = data.value # Request HIL (checkpoint created here) await ctx.request_info(request_data=WorkflowHILRequest(question=f"Approve {data.value}?"), response_type=str) @@ -50,8 +54,7 @@ class WorkflowTestExecutor(Executor): self, original_request: WorkflowHILRequest, response: str, ctx: WorkflowContext[str] ) -> None: """Handle HIL response.""" - state = await ctx.get_executor_state() or {} - value = state.get("data_value", "") + value = self._data_value or "" await ctx.send_message(f"{value}_approved" if response.lower() == "yes" else f"{value}_rejected") diff --git a/python/samples/README.md b/python/samples/README.md index c790e60519..937c303c77 100644 --- a/python/samples/README.md +++ b/python/samples/README.md @@ -17,11 +17,11 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen | [`getting_started/agents/anthropic/anthropic_basic.py`](./getting_started/agents/anthropic/anthropic_basic.py) | Agent with Anthropic Client | | [`getting_started/agents/anthropic/anthropic_advanced.py`](./getting_started/agents/anthropic/anthropic_advanced.py) | Advanced sample with `thinking` and hosted tools. | -### Azure AI +### Azure AI (based on `azure-ai-agents` V1 package) | File | Description | |------|-------------| -| [`getting_started/agents/azure_ai_agent/azure_ai_basic.py`](./getting_started/agents/azure_ai/azure_ai_basic.py) | Azure AI Agent Basic Example | +| [`getting_started/agents/azure_ai_agent/azure_ai_basic.py`](./getting_started/agents/azure_ai_agent/azure_ai_basic.py) | Azure AI Agent Basic Example | | [`getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py) | Azure AI Agent with Azure AI Search Example | | [`getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py) | Azure AI agent with Bing Grounding search for real-time web information | | [`getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example | @@ -36,6 +36,28 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen | [`getting_started/agents/azure_ai_agent/azure_ai_with_openapi_tools.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_openapi_tools.py) | Azure AI agent with OpenAPI tools | | [`getting_started/agents/azure_ai_agent/azure_ai_with_thread.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_thread.py) | Azure AI Agent with Thread Management Example | +### Azure AI (based on `azure-ai-projects` V2 package) + +| File | Description | +|------|-------------| +| [`getting_started/agents/azure_ai/azure_ai_basic.py`](./getting_started/agents/azure_ai/azure_ai_basic.py) | Azure AI Agent Basic Example | +| [`getting_started/agents/azure_ai/azure_ai_use_latest_version.py`](./getting_started/agents/azure_ai/azure_ai_use_latest_version.py) | Azure AI Agent latest version reuse example | +| [`getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py`](./getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py) | Azure AI Agent with Azure AI Search Example | +| [`getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py`](./getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py) | Azure AI Agent with Bing Grounding Example | +| [`getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py`](./getting_started/agents/azure_ai/azure_ai_with_bing_custom_search.py) | Azure AI Agent with Bing Custom Search Example | +| [`getting_started/agents/azure_ai/azure_ai_with_browser_automation.py`](./getting_started/agents/azure_ai/azure_ai_with_browser_automation.py) | Azure AI Agent with Browser Automation Example | +| [`getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example | +| [`getting_started/agents/azure_ai/azure_ai_with_existing_agent.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_agent.py) | Azure AI Agent with Existing Agent Example | +| [`getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py) | Azure AI Agent with Existing Conversation Example | +| [`getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py`](./getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py) | Azure AI Agent with Explicit Settings Example | +| [`getting_started/agents/azure_ai/azure_ai_with_file_search.py`](./getting_started/agents/azure_ai/azure_ai_with_file_search.py) | Azure AI Agent with File Search Example | +| [`getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py`](./getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py) | Azure AI Agent with Hosted MCP Example | +| [`getting_started/agents/azure_ai/azure_ai_with_response_format.py`](./getting_started/agents/azure_ai/azure_ai_with_response_format.py) | Azure AI Agent with Structured Output Example | +| [`getting_started/agents/azure_ai/azure_ai_with_thread.py`](./getting_started/agents/azure_ai/azure_ai_with_thread.py) | Azure AI Agent with Thread Management Example | +| [`getting_started/agents/azure_ai/azure_ai_with_image_generation.py`](./getting_started/agents/azure_ai/azure_ai_with_image_generation.py) | Azure AI Agent with Image Generation Example | +| [`getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py`](./getting_started/agents/azure_ai/azure_ai_with_microsoft_fabric.py) | Azure AI Agent with Microsoft Fabric Example | +| [`getting_started/agents/azure_ai/azure_ai_with_web_search.py`](./getting_started/agents/azure_ai/azure_ai_with_web_search.py) | Azure AI Agent with Web Search Example | + ### Azure OpenAI | File | Description | diff --git a/python/samples/getting_started/agents/README.md b/python/samples/getting_started/agents/README.md index e8c5f42e9c..6f1601cdc4 100644 --- a/python/samples/getting_started/agents/README.md +++ b/python/samples/getting_started/agents/README.md @@ -8,7 +8,8 @@ This folder contains examples demonstrating how to create and use agents with di | Folder | Description | |--------|-------------| -| **[`azure_ai/`](azure_ai/)** | Create agents using Azure AI Foundry Agent Service with various tools including function tools, code interpreter, MCP integration, and thread management | +| **[`azure_ai_agent/`](azure_ai_agent/)** | Create agents using Azure AI Agent Service (based on `azure-ai-agents` V1 package) including function tools, code interpreter, MCP integration, thread management, and more. | +| **[`azure_ai/`](azure_ai/)** | Create agents using Azure AI Agent Service (based on `azure-ai-projects` [V2](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11) package) including function tools, code interpreter, MCP integration, thread management, and more. | ### Microsoft Copilot Studio Examples diff --git a/python/samples/getting_started/agents/anthropic/README.md b/python/samples/getting_started/agents/anthropic/README.md index a4f76d7225..2fee2f5c07 100644 --- a/python/samples/getting_started/agents/anthropic/README.md +++ b/python/samples/getting_started/agents/anthropic/README.md @@ -9,10 +9,16 @@ This folder contains examples demonstrating how to use Anthropic's Claude models | [`anthropic_basic.py`](anthropic_basic.py) | Demonstrates how to setup a simple agent using the AnthropicClient, with both streaming and non-streaming responses. | | [`anthropic_advanced.py`](anthropic_advanced.py) | Shows advanced usage of the AnthropicClient, including hosted tools and `thinking`. | | [`anthropic_skills.py`](anthropic_skills.py) | Illustrates how to use Anthropic-managed Skills with an agent, including the Code Interpreter tool and file generation and saving. | +| [`anthropic_foundry.py`](anthropic_foundry.py) | Example of using Foundry's Anthropic integration with the Agent Framework. | ## Environment Variables Set the following environment variables before running the examples: - `ANTHROPIC_API_KEY`: Your Anthropic API key (get one from [Anthropic Console](https://console.anthropic.com/)) -- `ANTHROPIC_MODEL`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`) +- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use (e.g., `claude-haiku-4-5`, `claude-sonnet-4-5-20250929`) + +Or, for Foundry: +- `ANTHROPIC_FOUNDRY_API_KEY`: Your Foundry Anthropic API key +- `ANTHROPIC_FOUNDRY_ENDPOINT`: The endpoint URL for your Foundry Anthropic resource +- `ANTHROPIC_CHAT_MODEL_ID`: The Claude model to use in Foundry (e.g., `claude-haiku-4-5`) diff --git a/python/samples/getting_started/agents/anthropic/anthropic_foundry.py b/python/samples/getting_started/agents/anthropic/anthropic_foundry.py new file mode 100644 index 0000000000..cb1b690d54 --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_foundry.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio + +from agent_framework import HostedMCPTool, HostedWebSearchTool, TextReasoningContent, UsageContent +from agent_framework.anthropic import AnthropicClient +from anthropic import AsyncAnthropicFoundry + +""" +Anthropic Foundry Chat Agent Example + +This sample demonstrates using Anthropic with: +- Setting up an Anthropic-based agent with hosted tools. +- Using the `thinking` feature. +- Displaying both thinking and usage information during streaming responses. + +This example requires `anthropic>=0.74.0` and an endpoint in Foundry for Anthropic. + +To use the Foundry integration ensure you have the following environment variables set: +- ANTHROPIC_FOUNDRY_API_KEY + Alternatively you can pass in a azure_ad_token_provider function to the AsyncAnthropicFoundry constructor. +- ANTHROPIC_FOUNDRY_ENDPOINT + Should be something like https://.services.ai.azure.com/anthropic/ +- ANTHROPIC_CHAT_MODEL_ID + Should be something like claude-haiku-4-5 +""" + + +async def main() -> None: + """Example of streaming response (get results as they are generated).""" + agent = AnthropicClient(anthropic_client=AsyncAnthropicFoundry()).create_agent( + name="DocsAgent", + instructions="You are a helpful agent for both Microsoft docs questions and general questions.", + tools=[ + HostedMCPTool( + name="Microsoft Learn MCP", + url="https://learn.microsoft.com/api/mcp", + ), + HostedWebSearchTool(), + ], + # anthropic needs a value for the max_tokens parameter + # we set it to 1024, but you can override like this: + max_tokens=20000, + additional_chat_options={"thinking": {"type": "enabled", "budget_tokens": 10000}}, + ) + + query = "Can you compare Python decorators with C# attributes?" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + async for chunk in agent.run_stream(query): + for content in chunk.contents: + if isinstance(content, TextReasoningContent): + print(f"\033[32m{content.text}\033[0m", end="", flush=True) + if isinstance(content, UsageContent): + print(f"\n\033[34m[Usage so far: {content.details}]\033[0m\n", end="", flush=True) + if chunk.text: + print(chunk.text, end="", flush=True) + + print("\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/azure_ai/README.md b/python/samples/getting_started/agents/azure_ai/README.md index 475dad5a1a..8fd883b0cb 100644 --- a/python/samples/getting_started/agents/azure_ai/README.md +++ b/python/samples/getting_started/agents/azure_ai/README.md @@ -1,6 +1,6 @@ # Azure AI Agent Examples -This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. +This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). ## Examples @@ -8,6 +8,7 @@ This folder contains examples demonstrating different ways to create and use age |------|-------------| | [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIClient`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. | | [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. | +| [`azure_ai_with_agent_to_agent.py`](azure_ai_with_agent_to_agent.py) | Shows how to use Agent-to-Agent (A2A) capabilities with Azure AI agents to enable communication with other agents using the A2A protocol. Requires an A2A connection configured in your Azure AI project. | | [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. | | [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. | | [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. | @@ -19,6 +20,7 @@ This folder contains examples demonstrating different ways to create and use age | [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. | | [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. | | [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. | +| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. | | [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. | | [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. | | [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. | diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py new file mode 100644 index 0000000000..8efb3d9473 --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_agent_to_agent.py @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft. All rights reserved. +import asyncio +import os + +from agent_framework.azure import AzureAIClient +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent with Agent-to-Agent (A2A) Example + +This sample demonstrates usage of AzureAIClient with Agent-to-Agent (A2A) capabilities +to enable communication with other agents using the A2A protocol. + +Prerequisites: +1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. +2. Ensure you have an A2A connection configured in your Azure AI project + and set A2A_PROJECT_CONNECTION_ID environment variable. +""" + + +async def main() -> None: + async with ( + AzureCliCredential() as credential, + AzureAIClient(async_credential=credential).create_agent( + name="MyA2AAgent", + instructions="""You are a helpful assistant that can communicate with other agents. + Use the A2A tool when you need to interact with other agents to complete tasks + or gather information from specialized agents.""", + tools={ + "type": "a2a_preview", + "project_connection_id": os.environ["A2A_PROJECT_CONNECTION_ID"], + }, + ) as agent, + ): + query = "What can the secondary agent do?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Result: {result}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py b/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py new file mode 100644 index 0000000000..cdd81a33af --- /dev/null +++ b/python/samples/getting_started/agents/azure_ai/azure_ai_with_sharepoint.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft. All rights reserved. +import asyncio +import os + +from agent_framework.azure import AzureAIClient +from azure.identity.aio import AzureCliCredential + +""" +Azure AI Agent with SharePoint Example + +This sample demonstrates usage of AzureAIClient with SharePoint +to search through SharePoint content and answer user questions about it. + +Prerequisites: +1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables. +2. Ensure you have a SharePoint connection configured in your Azure AI project + and set SHAREPOINT_PROJECT_CONNECTION_ID environment variable. +""" + + +async def main() -> None: + async with ( + AzureCliCredential() as credential, + AzureAIClient(async_credential=credential).create_agent( + name="MySharePointAgent", + instructions="""You are a helpful agent that can use SharePoint tools to assist users. + Use the available SharePoint tools to answer questions and perform tasks.""", + tools={ + "type": "sharepoint_grounding_preview", + "sharepoint_grounding_preview": { + "project_connection_id": os.environ["SHAREPOINT_PROJECT_CONNECTION_ID"] + }, + }, + ) as agent, + ): + query = "What is Contoso whistleblower policy?" + print(f"User: {query}") + result = await agent.run(query) + print(f"Result: {result}\n") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/getting_started/agents/azure_ai_agent/README.md b/python/samples/getting_started/agents/azure_ai_agent/README.md index 19165b89ed..c7337bbe82 100644 --- a/python/samples/getting_started/agents/azure_ai_agent/README.md +++ b/python/samples/getting_started/agents/azure_ai_agent/README.md @@ -1,6 +1,6 @@ # Azure AI Agent Examples -This folder contains examples demonstrating different ways to create and use agents with the Azure AI chat client from the `agent_framework.azure` package. +This folder contains examples demonstrating different ways to create and use agents with the Azure AI chat client from the `agent_framework.azure` package. These examples use the `AzureAIAgentClient` with the `azure-ai-agents` 1.x (V1) API surface. For updated V2 (`azure-ai-projects` 2.x) samples, see the [Azure AI V2 examples folder](../azure_ai/). ## Examples diff --git a/python/samples/getting_started/azure_functions/01_single_agent/README.md b/python/samples/getting_started/azure_functions/01_single_agent/README.md index 5f7c1887be..38c6ce58f5 100644 --- a/python/samples/getting_started/azure_functions/01_single_agent/README.md +++ b/python/samples/getting_started/azure_functions/01_single_agent/README.md @@ -18,20 +18,40 @@ Follow the common setup steps in `../README.md` to install tooling, configure Az Send a prompt to the Joker agent: +Bash (Linux/macOS/WSL): + ```bash -curl -X POST http://localhost:7071/api/agents/Joker/run \ - -H "Content-Type: text/plain" \ +curl -i -X POST http://localhost:7071/api/agents/Joker/run \ -d "Tell me a short joke about cloud computing." ``` +PowerShell: + +```powershell +Invoke-RestMethod -Method Post -Uri http://localhost:7071/api/agents/Joker/run ` + -Body "Tell me a short joke about cloud computing." +``` + The agent responds with a JSON payload that includes the generated joke. -> **Note:** To return immediately with an HTTP 202 response instead of waiting for the agent output, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body. The default behavior waits for the response. +> [!TIP] +> To return immediately with an HTTP 202 response instead of waiting for the agent output, set the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body. The default behavior waits for the response. ## Expected Output -When you send a POST request with plain-text input, the Functions host responds with an HTTP 202 and queues the request for the durable agent entity. A typical response body looks like the following: -Expected HTTP 202 payload: +The default plain-text response looks like the following: + +```http +HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +x-ms-thread-id: 4f205157170244bfbd80209df383757e + +Why did the cloud break up with the server? + +Because it found someone more "uplifting"! +``` + +When you specify the `x-ms-wait-for-response` header or include `"wait_for_response": false` in the request body, the Functions host responds with an HTTP 202 and queues the request to run in the background. A typical response body looks like the following: ```json { diff --git a/python/samples/getting_started/devui/spam_workflow/workflow.py b/python/samples/getting_started/devui/spam_workflow/workflow.py index 020d22a4c7..73be349cc6 100644 --- a/python/samples/getting_started/devui/spam_workflow/workflow.py +++ b/python/samples/getting_started/devui/spam_workflow/workflow.py @@ -17,7 +17,7 @@ Workflow Steps: import asyncio import logging from dataclasses import dataclass -from typing import Literal, Annotated +from typing import Literal from agent_framework import ( Case, @@ -31,9 +31,11 @@ from agent_framework import ( from pydantic import BaseModel, Field from typing_extensions import Never + # Define response model with clear user guidance class SpamDecision(BaseModel): """User's decision on whether the email is spam.""" + decision: Literal["spam", "not spam"] = Field( description="Enter 'spam' to mark as spam, or 'not spam' to mark as legitimate" ) @@ -71,10 +73,11 @@ class SpamDetectorResponse: class SpamApprovalRequest: """Human-in-the-loop approval request for spam classification.""" - email_message: str = "" - detected_as_spam: bool = False - confidence: float = 0.0 - reasons: str = "" + email_message: str + detected_as_spam: bool + confidence: float + reasons: list[str] + full_email_content: EmailContent @dataclass @@ -128,8 +131,6 @@ class EmailPreprocessor(Executor): await ctx.send_message(result) - - class SpamDetector(Executor): """Step 2: An executor that analyzes content and determines if a message is spam.""" @@ -139,7 +140,9 @@ class SpamDetector(Executor): self._spam_keywords = spam_keywords @handler - async def handle_email_content(self, email_content: EmailContent, ctx: WorkflowContext[SpamApprovalRequest]) -> None: + async def handle_email_content( + self, email_content: EmailContent, ctx: WorkflowContext[SpamApprovalRequest] + ) -> None: """Analyze email content and determine if the message is spam, then request human approval.""" await asyncio.sleep(2.0) # Simulate analysis and detection time @@ -186,25 +189,13 @@ class SpamDetector(Executor): is_spam = spam_score >= 0.5 - # Store detection result in executor state for later use - # Store minimal data needed (not complex objects that don't serialize well) - await ctx.set_executor_state({ - "original_message": email_content.original_message, - "cleaned_message": email_content.cleaned_message, - "word_count": email_content.word_count, - "has_suspicious_patterns": email_content.has_suspicious_patterns, - "is_spam": is_spam, - "ai_original_classification": is_spam, # Store original AI decision - "confidence_score": spam_score, - "spam_reasons": spam_reasons - }) - # Request human approval before proceeding using new API approval_request = SpamApprovalRequest( email_message=email_text[:200], # First 200 chars detected_as_spam=is_spam, confidence=spam_score, - reasons=", ".join(spam_reasons) if spam_reasons else "no specific reasons" + reasons=spam_reasons, + full_email_content=email_content, ) await ctx.request_info( @@ -214,20 +205,15 @@ class SpamDetector(Executor): @response_handler async def handle_human_response( - self, - original_request: SpamApprovalRequest, - response: SpamDecision, - ctx: WorkflowContext[SpamDetectorResponse] + self, original_request: SpamApprovalRequest, response: SpamDecision, ctx: WorkflowContext[SpamDetectorResponse] ) -> None: """Process human approval response and continue workflow.""" print(f"[SpamDetector] handle_human_response called with response: {response}") # Get stored detection result - state = await ctx.get_executor_state() or {} - print(f"[SpamDetector] Retrieved state: {state}") - ai_original = state.get("ai_original_classification", False) - confidence_score = state.get("confidence_score", 0.0) - spam_reasons = state.get("spam_reasons", []) + ai_original = original_request.detected_as_spam + confidence_score = original_request.confidence + spam_reasons = original_request.reasons # Parse human decision from the response model human_decision = response.decision.strip().lower() @@ -241,27 +227,21 @@ class SpamDetector(Executor): # Default to AI decision if unclear is_spam = ai_original - # Reconstruct EmailContent from stored primitives - email_content = EmailContent( - original_message=state.get("original_message", ""), - cleaned_message=state.get("cleaned_message", ""), - word_count=state.get("word_count", 0), - has_suspicious_patterns=state.get("has_suspicious_patterns", False) - ) - result = SpamDetectorResponse( - email_content=email_content, + email_content=original_request.full_email_content, is_spam=is_spam, confidence_score=confidence_score, spam_reasons=spam_reasons, human_reviewed=True, human_decision=response.decision, - ai_original_classification=ai_original + ai_original_classification=ai_original, ) - print(f"[SpamDetector] Sending SpamDetectorResponse: is_spam={is_spam}, confidence={confidence_score}, human_reviewed=True") + print( + f"[SpamDetector] Sending SpamDetectorResponse: is_spam={is_spam}, confidence={confidence_score}, human_reviewed=True" + ) await ctx.send_message(result) - print(f"[SpamDetector] Message sent successfully") + print("[SpamDetector] Message sent successfully") class SpamHandler(Executor): @@ -427,7 +407,9 @@ workflow = ( spam_detector, [ Case(condition=lambda x: isinstance(x, SpamDetectorResponse) and x.is_spam, target=spam_handler), - Default(target=legitimate_message_handler), # Default handles non-spam and non-SpamDetectorResponse messages + Default( + target=legitimate_message_handler + ), # Default handles non-spam and non-SpamDetectorResponse messages ], ) .add_edge(spam_handler, final_processor) diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py index 9fb870bf01..7effcdd28e 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_human_in_the_loop.py @@ -3,6 +3,7 @@ import asyncio from dataclasses import dataclass from pathlib import Path +from typing import Any, override # NOTE: the Azure client imports above are real dependencies. When running this # sample outside of Azure-enabled environments you may wish to swap in the @@ -116,19 +117,19 @@ class ReviewGateway(Executor): def __init__(self, id: str, writer_id: str) -> None: super().__init__(id=id) self._writer_id = writer_id + self._iteration = 0 @handler async def on_agent_response(self, response: AgentExecutorResponse, ctx: WorkflowContext) -> None: # Capture the agent output so we can surface it to the reviewer and persist iterations. - draft = response.agent_run_response.text or "" - iteration = int((await ctx.get_executor_state() or {}).get("iteration", 0)) + 1 - await ctx.set_executor_state({"iteration": iteration, "last_draft": draft}) + self._iteration += 1 + # Emit a human approval request. await ctx.request_info( request_data=HumanApprovalRequest( prompt="Review the draft. Reply 'approve' or provide edit instructions.", - draft=draft, - iteration=iteration, + draft=response.agent_run_response.text, + iteration=self._iteration, ), response_type=str, ) @@ -142,28 +143,33 @@ class ReviewGateway(Executor): ) -> None: # The `original_request` is the request we sent earlier that is now being answered. reply = feedback.strip() - state = await ctx.get_executor_state() or {} - draft = state.get("last_draft") or (original_request.draft or "") - if reply.lower() == "approve": + if len(reply) == 0 or reply.lower() == "approve": # Workflow is completed when the human approves. - await ctx.yield_output(draft) + await ctx.yield_output(original_request.draft) return # Any other response loops us back to the writer with fresh guidance. - guidance = reply or "Tighten the copy and emphasise customer benefit." - iteration = int(state.get("iteration", 1)) + 1 - await ctx.set_executor_state({"iteration": iteration, "last_draft": draft}) prompt = ( "Revise the launch note. Respond with the new copy only.\n\n" - f"Previous draft:\n{draft}\n\n" - f"Human guidance: {guidance}" + f"Previous draft:\n{original_request.draft}\n\n" + f"Human guidance: {reply}" ) await ctx.send_message( AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True), target_id=self._writer_id, ) + @override + async def on_checkpoint_save(self) -> dict[str, Any]: + # Save the current iteration count in executor state for checkpointing. + return {"iteration": self._iteration} + + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + # Restore the iteration count from executor state during checkpoint recovery. + self._iteration = state.get("iteration", 0) + def create_workflow(checkpoint_storage: FileCheckpointStorage) -> Workflow: """Assemble the workflow graph used by both the initial run and resume.""" @@ -247,10 +253,10 @@ async def run_interactive_session( else: if initial_message: print(f"\nStarting workflow with brief: {initial_message}\n") - event_stream = workflow.run_stream(initial_message) + event_stream = workflow.run_stream(message=initial_message) elif checkpoint_id: print("\nStarting workflow from checkpoint...\n") - event_stream = workflow.run_stream(checkpoint_id) + event_stream = workflow.run_stream(checkpoint_id=checkpoint_id) else: raise ValueError("Either initial_message or checkpoint_id must be provided") diff --git a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py index cb0c7705c5..0b7e874a1a 100644 --- a/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py +++ b/python/samples/getting_started/workflows/checkpoint/checkpoint_with_resume.py @@ -1,322 +1,157 @@ # Copyright (c) Microsoft. All rights reserved. -import asyncio -import os -from pathlib import Path -from typing import TYPE_CHECKING, Any - -from agent_framework import ( - AgentExecutor, - AgentExecutorRequest, - AgentExecutorResponse, - ChatMessage, - Executor, - FileCheckpointStorage, - Role, - WorkflowBuilder, - WorkflowContext, - get_checkpoint_summary, - handler, -) -from agent_framework.azure import AzureOpenAIChatClient -from azure.identity import AzureCliCredential - -if TYPE_CHECKING: - from agent_framework import Workflow - from agent_framework._workflows._checkpoint import WorkflowCheckpoint - """ -Sample: Checkpointing and Resuming a Workflow (with an Agent stage) +Sample: Checkpointing and Resuming a Workflow Purpose: -This sample shows how to enable checkpointing at superstep boundaries, persist both -executor-local state and shared workflow state, and then resume execution from a specific -checkpoint. The workflow demonstrates a simple text-processing pipeline that includes -an LLM-backed AgentExecutor stage. - -Pipeline: -1) UpperCaseExecutor converts input to uppercase and records state. -2) ReverseTextExecutor reverses the string. -3) SubmitToLowerAgent prepares an AgentExecutorRequest for the lowercasing agent. -4) lower_agent (AgentExecutor) converts text to lowercase via Azure OpenAI. -5) FinalizeFromAgent yields the final result. +This sample shows how to enable checkpointing for a long-running workflow +that can be paused and resumed. What you learn: -- How to persist executor state using ctx.get_executor_state and ctx.set_executor_state. -- How to persist shared workflow state using ctx.set_shared_state for cross-executor visibility. -- How to configure FileCheckpointStorage and call with_checkpointing on WorkflowBuilder. -- How to list and inspect checkpoints programmatically. -- How to interactively choose a checkpoint to resume from (instead of always resuming - from the most recent or a hard-coded one) using run_stream. -- How workflows complete by yielding outputs when idle, not via explicit completion events. +- How to configure checkpointing storage (InMemoryCheckpointStorage for testing) +- How to resume a workflow from a checkpoint after interruption +- How to implement executor state management with checkpoint hooks +- How to handle workflow interruptions and automatic recovery + +Pipeline: +This sample shows a workflow that computes factor pairs for numbers up to a given limit: +1) A start executor that receives the upper limit and creates the initial task +2) A worker executor that processes each number to find its factor pairs +3) The worker uses checkpoint hooks to save/restore its internal state Prerequisites: -- Azure AI or Azure OpenAI available for AzureOpenAIChatClient. -- Authentication with azure-identity via AzureCliCredential. Run az login locally. -- Filesystem access for writing JSON checkpoint files in a temp directory. +- Basic understanding of workflow concepts, including executors, edges, events, etc. """ -# Define the temporary directory for storing checkpoints. -# These files allow the workflow to be resumed later. -DIR = os.path.dirname(__file__) -TEMP_DIR = os.path.join(DIR, "tmp", "checkpoints") -os.makedirs(TEMP_DIR, exist_ok=True) +import asyncio +from dataclasses import dataclass +from random import random +from typing import Any, override + +from agent_framework import ( + Executor, + InMemoryCheckpointStorage, + SuperStepCompletedEvent, + WorkflowBuilder, + WorkflowCheckpoint, + WorkflowContext, + WorkflowOutputEvent, + handler, +) -class UpperCaseExecutor(Executor): - """Uppercases the input text and persists both local and shared state.""" +@dataclass +class ComputeTask: + """Task containing the list of numbers remaining to be processed.""" + + remaining_numbers: list[int] + + +class StartExecutor(Executor): + """Initiates the workflow by providing the upper limit for factor pair computation.""" @handler - async def to_upper_case(self, text: str, ctx: WorkflowContext[str]) -> None: - result = text.upper() - print(f"UpperCaseExecutor: '{text}' -> '{result}'") - - # Persist executor-local state so it is captured in checkpoints - # and available after resume for observability or logic. - prev = await ctx.get_executor_state() or {} - count = int(prev.get("count", 0)) + 1 - await ctx.set_executor_state({ - "count": count, - "last_input": text, - "last_output": result, - }) - - # Write to shared_state so downstream executors and any resumed runs can read it. - await ctx.set_shared_state("original_input", text) - await ctx.set_shared_state("upper_output", result) - - # Send transformed text to the next executor. - await ctx.send_message(result) + async def start(self, upper_limit: int, ctx: WorkflowContext[ComputeTask]) -> None: + """Start the workflow with a list of numbers to process.""" + print(f"StartExecutor: Starting factor pair computation up to {upper_limit}") + await ctx.send_message(ComputeTask(remaining_numbers=list(range(1, upper_limit + 1)))) -class SubmitToLowerAgent(Executor): - """Builds an AgentExecutorRequest to send to the lowercasing agent while keeping shared-state visibility.""" +class WorkerExecutor(Executor): + """Processes numbers to compute their factor pairs and manages executor state for checkpointing.""" - def __init__(self, id: str, agent_id: str): + def __init__(self, id: str) -> None: super().__init__(id=id) - self._agent_id = agent_id + self._composite_number_pairs: dict[int, list[tuple[int, int]]] = {} @handler - async def submit(self, text: str, ctx: WorkflowContext[AgentExecutorRequest]) -> None: - # Demonstrate reading shared_state written by UpperCaseExecutor. - # Shared state survives across checkpoints and is visible to all executors. - orig = await ctx.get_shared_state("original_input") - upper = await ctx.get_shared_state("upper_output") - print(f"LowerAgent (shared_state): original_input='{orig}', upper_output='{upper}'") + async def compute( + self, + task: ComputeTask, + ctx: WorkflowContext[ComputeTask, dict[int, list[tuple[int, int]]]], + ) -> None: + """Process the next number in the task, computing its factor pairs.""" + next_number = task.remaining_numbers.pop(0) - # Build a minimal, deterministic prompt for the AgentExecutor. - prompt = f"Convert the following text to lowercase. Return ONLY the transformed text.\n\nText: {text}" + print(f"WorkerExecutor: Computing factor pairs for {next_number}") + pairs: list[tuple[int, int]] = [] + for i in range(1, next_number): + if next_number % i == 0: + pairs.append((i, next_number // i)) + self._composite_number_pairs[next_number] = pairs - # Send to the AgentExecutor. should_respond=True instructs the agent to produce a reply. - await ctx.send_message( - AgentExecutorRequest(messages=[ChatMessage(Role.USER, text=prompt)], should_respond=True), - target_id=self._agent_id, - ) + if not task.remaining_numbers: + # All numbers processed - output the results + await ctx.yield_output(self._composite_number_pairs) + else: + # More numbers to process - continue with remaining task + await ctx.send_message(task) + @override + async def on_checkpoint_save(self) -> dict[str, Any]: + """Save the executor's internal state for checkpointing.""" + return {"composite_number_pairs": self._composite_number_pairs} -class FinalizeFromAgent(Executor): - """Consumes the AgentExecutorResponse and yields the final result.""" - - @handler - async def finalize(self, response: AgentExecutorResponse, ctx: WorkflowContext[Any, str]) -> None: - result = response.agent_run_response.text or "" - - # Persist executor-local state for auditability when inspecting checkpoints. - prev = await ctx.get_executor_state() or {} - count = int(prev.get("count", 0)) + 1 - await ctx.set_executor_state({ - "count": count, - "last_output": result, - "final": True, - }) - - # Yield the final result so external consumers see the final value. - await ctx.yield_output(result) - - -class ReverseTextExecutor(Executor): - """Reverses the input text and persists local state.""" - - @handler - async def reverse_text(self, text: str, ctx: WorkflowContext[str]) -> None: - result = text[::-1] - print(f"ReverseTextExecutor: '{text}' -> '{result}'") - - # Persist executor-local state so checkpoint inspection can reveal progress. - prev = await ctx.get_executor_state() or {} - count = int(prev.get("count", 0)) + 1 - await ctx.set_executor_state({ - "count": count, - "last_input": text, - "last_output": result, - }) - - # Forward the reversed string to the next stage. - await ctx.send_message(result) - - -def create_workflow(checkpoint_storage: FileCheckpointStorage) -> "Workflow": - # Instantiate the pipeline executors. - upper_case_executor = UpperCaseExecutor(id="upper-case") - reverse_text_executor = ReverseTextExecutor(id="reverse-text") - - # Configure the agent stage that lowercases the text. - chat_client = AzureOpenAIChatClient(credential=AzureCliCredential()) - lower_agent = AgentExecutor( - chat_client.create_agent( - instructions=("You transform text to lowercase. Reply with ONLY the transformed text.") - ), - id="lower_agent", - ) - - # Bridge to the agent and terminalization stage. - submit_lower = SubmitToLowerAgent(id="submit_lower", agent_id=lower_agent.id) - finalize = FinalizeFromAgent(id="finalize") - - # Build the workflow with checkpointing enabled. - return ( - WorkflowBuilder(max_iterations=5) - .add_edge(upper_case_executor, reverse_text_executor) # Uppercase -> Reverse - .add_edge(reverse_text_executor, submit_lower) # Reverse -> Build Agent request - .add_edge(submit_lower, lower_agent) # Submit to AgentExecutor - .add_edge(lower_agent, finalize) # Agent output -> Finalize - .set_start_executor(upper_case_executor) # Entry point - .with_checkpointing(checkpoint_storage=checkpoint_storage) # Enable persistence - .build() - ) - - -def _render_checkpoint_summary(checkpoints: list["WorkflowCheckpoint"]) -> None: - """Display human-friendly checkpoint metadata using framework summaries.""" - - if not checkpoints: - return - - print("\nCheckpoint summary:") - for cp in sorted(checkpoints, key=lambda c: c.timestamp): - summary = get_checkpoint_summary(cp) - msg_count = sum(len(v) for v in cp.messages.values()) - state_keys = sorted(summary.executor_ids) - orig = cp.shared_state.get("original_input") - upper = cp.shared_state.get("upper_output") - - line = ( - f"- {summary.checkpoint_id} | iter={summary.iteration_count} | messages={msg_count} | states={state_keys}" - ) - if summary.status: - line += f" | status={summary.status}" - line += f" | shared_state: original_input='{orig}', upper_output='{upper}'" - print(line) + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + """Restore the executor's internal state from a checkpoint.""" + self._composite_number_pairs = state.get("composite_number_pairs", {}) async def main(): - # Clear existing checkpoints in this sample directory for a clean run. - checkpoint_dir = Path(TEMP_DIR) - for file in checkpoint_dir.glob("*.json"): # noqa: ASYNC240 - file.unlink() + # Create workflow executors + start_executor = StartExecutor(id="start") + worker_executor = WorkerExecutor(id="worker") - # Backing store for checkpoints written by with_checkpointing. - checkpoint_storage = FileCheckpointStorage(storage_path=TEMP_DIR) + # Build workflow with checkpointing enabled + workflow_builder = ( + WorkflowBuilder() + .set_start_executor(start_executor) + .add_edge(start_executor, worker_executor) + .add_edge(worker_executor, worker_executor) # Self-loop for iterative processing + ) + checkpoint_storage = InMemoryCheckpointStorage() + workflow_builder = workflow_builder.with_checkpointing(checkpoint_storage=checkpoint_storage) - workflow = create_workflow(checkpoint_storage=checkpoint_storage) + # Run workflow with automatic checkpoint recovery + latest_checkpoint: WorkflowCheckpoint | None = None + while True: + workflow = workflow_builder.build() - # Run the full workflow once and observe events as they stream. - print("Running workflow with initial message...") - async for event in workflow.run_stream(message="hello world"): - print(f"Event: {event}") + # Start from checkpoint or fresh execution + print(f"\n** Workflow {workflow.id} started **") + event_stream = ( + workflow.run_stream(message=10) + if latest_checkpoint is None + else workflow.run_stream(checkpoint_id=latest_checkpoint.checkpoint_id) + ) - # Inspect checkpoints written during the run. - all_checkpoints = await checkpoint_storage.list_checkpoints() - if not all_checkpoints: - print("No checkpoints found!") - return - - # All checkpoints created by this run share the same workflow_id. - workflow_id = all_checkpoints[0].workflow_id - - _render_checkpoint_summary(all_checkpoints) - - # Offer an interactive selection of checkpoints to resume from. - sorted_cps = sorted([cp for cp in all_checkpoints if cp.workflow_id == workflow_id], key=lambda c: c.timestamp) - - print("\nAvailable checkpoints to resume from:") - for idx, cp in enumerate(sorted_cps): - summary = get_checkpoint_summary(cp) - line = f" [{idx}] id={summary.checkpoint_id} iter={summary.iteration_count}" - if summary.status: - line += f" status={summary.status}" - msg_count = sum(len(v) for v in cp.messages.values()) - line += f" messages={msg_count}" - print(line) - - user_input = input( # noqa: ASYNC250 - "\nEnter checkpoint index (or paste checkpoint id) to resume from, or press Enter to skip resume: " - ).strip() - - if not user_input: - print("No checkpoint selected. Exiting without resuming.") - return - - chosen_cp_id: str | None = None - - # Try as index first - if user_input.isdigit(): - idx = int(user_input) - if 0 <= idx < len(sorted_cps): - chosen_cp_id = sorted_cps[idx].checkpoint_id - # Fall back to direct id match - if chosen_cp_id is None: - for cp in sorted_cps: - if cp.checkpoint_id.startswith(user_input): # allow prefix match for convenience - chosen_cp_id = cp.checkpoint_id + output: str | None = None + async for event in event_stream: + if isinstance(event, WorkflowOutputEvent): + output = event.data + break + if isinstance(event, SuperStepCompletedEvent) and random() < 0.5: + # Randomly simulate system interruptions + # The `SuperStepCompletedEvent` ensures we only interrupt after + # the current super-step is fully complete and checkpointed. + # If we interrupt mid-step, the workflow may resume from an earlier point. + print("\n** Simulating workflow interruption. Stopping execution. **") break - if chosen_cp_id is None: - print("Input did not match any checkpoint. Exiting without resuming.") - return + # Find the latest checkpoint to resume from + all_checkpoints = await checkpoint_storage.list_checkpoints() + if not all_checkpoints: + raise RuntimeError("No checkpoints available to resume from.") + latest_checkpoint = all_checkpoints[-1] + print( + f"Checkpoint {latest_checkpoint.checkpoint_id}: " + f"(iter={latest_checkpoint.iteration_count}, messages={latest_checkpoint.messages})" + ) - # You can reuse the same workflow graph definition and resume from a prior checkpoint. - # This second workflow instance does not enable checkpointing to show that resumption - # reads from stored state but need not write new checkpoints. - new_workflow = create_workflow(checkpoint_storage=checkpoint_storage) - - print(f"\nResuming from checkpoint: {chosen_cp_id}") - async for event in new_workflow.run_stream(checkpoint_id=chosen_cp_id, checkpoint_storage=checkpoint_storage): - print(f"Resumed Event: {event}") - - """ - Sample Output: - - Running workflow with initial message... - UpperCaseExecutor: 'hello world' -> 'HELLO WORLD' - Event: ExecutorInvokeEvent(executor_id=upper_case_executor) - Event: ExecutorCompletedEvent(executor_id=upper_case_executor) - ReverseTextExecutor: 'HELLO WORLD' -> 'DLROW OLLEH' - Event: ExecutorInvokeEvent(executor_id=reverse_text_executor) - Event: ExecutorCompletedEvent(executor_id=reverse_text_executor) - LowerAgent (shared_state): original_input='hello world', upper_output='HELLO WORLD' - Event: ExecutorInvokeEvent(executor_id=submit_lower) - Event: ExecutorInvokeEvent(executor_id=lower_agent) - Event: ExecutorInvokeEvent(executor_id=finalize) - - Checkpoint summary: - - dfc63e72-8e8d-454f-9b6d-0d740b9062e6 | label='after_initial_execution' | iter=0 | messages=1 | states=['upper_case_executor'] | shared_state: original_input='hello world', upper_output='HELLO WORLD' - - a78c345a-e5d9-45ba-82c0-cb725452d91b | label='superstep_1' | iter=1 | messages=1 | states=['reverse_text_executor', 'upper_case_executor'] | shared_state: original_input='hello world', upper_output='HELLO WORLD' - - 637c1dbd-a525-4404-9583-da03980537a2 | label='superstep_2' | iter=2 | messages=0 | states=['finalize', 'lower_agent', 'reverse_text_executor', 'submit_lower', 'upper_case_executor'] | shared_state: original_input='hello world', upper_output='HELLO WORLD' - - Available checkpoints to resume from: - [0] id=dfc63e72-... iter=0 messages=1 label='after_initial_execution' - [1] id=a78c345a-... iter=1 messages=1 label='superstep_1' - [2] id=637c1dbd-... iter=2 messages=0 label='superstep_2' - - Enter checkpoint index (or paste checkpoint id) to resume from, or press Enter to skip resume: 1 - - Resuming from checkpoint: a78c345a-e5d9-45ba-82c0-cb725452d91b - LowerAgent (shared_state): original_input='hello world', upper_output='HELLO WORLD' - Resumed Event: ExecutorInvokeEvent(executor_id=submit_lower) - Resumed Event: ExecutorInvokeEvent(executor_id=lower_agent) - Resumed Event: ExecutorInvokeEvent(executor_id=finalize) - """ # noqa: E501 + if output is not None: + print(f"\nWorkflow completed successfully with output: {output}") + break if __name__ == "__main__": diff --git a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py index 62c88bf49f..2d90db8c2c 100644 --- a/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py +++ b/python/samples/getting_started/workflows/checkpoint/sub_workflow_checkpoint.py @@ -7,6 +7,7 @@ import uuid from dataclasses import dataclass, field, replace from datetime import datetime, timedelta from pathlib import Path +from typing import Any, override from agent_framework import ( Executor, @@ -205,6 +206,8 @@ class LaunchCoordinator(Executor): def __init__(self) -> None: super().__init__(id="launch_coordinator") + # Track pending requests to match responses + self._pending_requests: dict[str, SubWorkflowRequestMessage] = {} @handler async def kick_off(self, topic: str, ctx: WorkflowContext[DraftTask]) -> None: @@ -244,11 +247,9 @@ class LaunchCoordinator(Executor): if not isinstance(request.source_event.data, ReviewRequest): raise TypeError(f"Expected 'ReviewRequest', got {type(request.source_event.data)}") - # Record the request to response matching + # Record the request for response matching review_request = request.source_event.data - executor_state = await ctx.get_executor_state() or {} - executor_state[review_request.id] = request - await ctx.set_executor_state(executor_state) + self._pending_requests[review_request.id] = request # Send the request without modification await ctx.request_info(request_data=review_request, response_type=str) @@ -265,17 +266,25 @@ class LaunchCoordinator(Executor): Note that the response must be sent back using SubWorkflowResponseMessage to route the response back to the sub-workflow. """ - executor_state = await ctx.get_executor_state() or {} - request_message = executor_state.pop(original_request.id, None) - - # Save the executor state back to the context - await ctx.set_executor_state(executor_state) + request_message = self._pending_requests.pop(original_request.id, None) if request_message is None: raise ValueError("No matching pending request found for the resource response") await ctx.send_message(request_message.create_response(response)) + @override + async def on_checkpoint_save(self) -> dict[str, Any]: + """Capture any additional state needed for checkpointing.""" + return { + "pending_requests": self._pending_requests, + } + + @override + async def on_checkpoint_restore(self, state: dict[str, Any]) -> None: + """Restore any additional state needed from checkpointing.""" + self._pending_requests = state.get("pending_requests", {}) + # --------------------------------------------------------------------------- # Workflow construction helpers @@ -356,9 +365,7 @@ async def main() -> None: workflow2 = build_parent_workflow(storage) request_info_event: RequestInfoEvent | None = None - async for event in workflow2.run_stream( - resume_checkpoint.checkpoint_id, - ): + async for event in workflow2.run_stream(checkpoint_id=resume_checkpoint.checkpoint_id): if isinstance(event, RequestInfoEvent): request_info_event = event diff --git a/python/uv.lock b/python/uv.lock index f0657d5f68..0e7cce14db 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -45,7 +45,7 @@ overrides = [ [[package]] name = "a2a-sdk" -version = "0.3.12" +version = "0.3.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -54,9 +54,9 @@ dependencies = [ { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/38/b473e04e21960d8eee7b04e35ac35c8e117f3cc2b333e0fa0371b48308a3/a2a_sdk-0.3.12.tar.gz", hash = "sha256:498a14c6b175185d24b4d800ea1ce1daa7dade48cdb85bc63c6db18acca37f6d", size = 227806, upload-time = "2025-11-12T21:38:23.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/69/b872519749ace0e90d69d0cbffdb289d3a5befe2adf844c90fc08a5e5559/a2a_sdk-0.3.14.tar.gz", hash = "sha256:2f30f76389cdec9aa8d4d6f79b6e932004f7e8689475bead40d02e049fa3091e", size = 228170, upload-time = "2025-11-18T08:59:08.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/68/3c89949d8692deaab48ac077543fdff500317ee06ee16c7292ddff66a54f/a2a_sdk-0.3.12-py3-none-any.whl", hash = "sha256:8f1cb56e1faa3edc6a228075391b136c1518061b4f0b78ff0e373f65f858d736", size = 140393, upload-time = "2025-11-12T21:38:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/a353a8fd94addd8e7ab649f8fe45713ec6f963fcdf6b8fe29cdf902b592f/a2a_sdk-0.3.14-py3-none-any.whl", hash = "sha256:0df8c596a83e2fccf0ed335592125931fec1bcc974c442ed52f848defd0d9e8e", size = 140734, upload-time = "2025-11-18T08:59:06.743Z" }, ] [[package]] @@ -190,7 +190,7 @@ requires-dist = [ [[package]] name = "agent-framework-ag-ui" -version = "1.0.0b251114" +version = "1.0.0b251117" source = { editable = "packages/ag-ui" } dependencies = [ { name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -415,7 +415,7 @@ math = [ tau2 = [ { name = "loguru", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -519,7 +519,7 @@ source = { editable = "packages/redis" } dependencies = [ { name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "redis", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "redisvl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] @@ -764,7 +764,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.72.1" +version = "0.74.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -776,9 +776,9 @@ dependencies = [ { name = "sniffio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/feb750a21461090ecf48bbebcaa261cd09003cc1d14e2fa9643ad59edd4d/anthropic-0.72.1.tar.gz", hash = "sha256:a6d1d660e1f4af91dddc732f340786d19acaffa1ae8e69442e56be5fa6539d51", size = 415395, upload-time = "2025-11-11T16:53:29.001Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f9/baa1b885c8664b446e6a13003938046901e54ffd70b532bbebd01256e34b/anthropic-0.74.0.tar.gz", hash = "sha256:114ec10cb394b6764e199da06335da4747b019c5629e53add33572f66964ad99", size = 428958, upload-time = "2025-11-18T15:29:47.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/05/d9d45edad1aa28330cea09a3b35e1590f7279f91bb5ab5237c70a0884ea3/anthropic-0.72.1-py3-none-any.whl", hash = "sha256:81e73cca55e8924776c8c4418003defe6bf9eaf0cd92beb94c8dbf537b95316f", size = 357373, upload-time = "2025-11-11T16:53:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/61/27/8c404b290ec650e634eacc674df943913722ec21097b0476d68458250c2f/anthropic-0.74.0-py3-none-any.whl", hash = "sha256:df29b8dfcdbd2751fa31177f643d8d8f66c5315fe06bdc42f9139e9f00d181d5", size = 371474, upload-time = "2025-11-18T15:29:45.748Z" }, ] [[package]] @@ -899,7 +899,7 @@ wheels = [ [[package]] name = "azure-ai-projects" -version = "2.0.0b1" +version = "2.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -907,9 +907,9 @@ dependencies = [ { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/33/69b9db7f232a43216bf6fb58172335a269abdc923e78e7ecacc71590aa6e/azure_ai_projects-2.0.0b1.tar.gz", hash = "sha256:259413c5129b4f4902da335c36c2a0389b003953fa0116d9d054f54c3f42c3f4", size = 335085, upload-time = "2025-11-12T03:59:59.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/96/ec17f99f5ced3d82876e89b4f950d8c7466c84d79016c5905b1c03b6c484/azure_ai_projects-2.0.0b2.tar.gz", hash = "sha256:4444cc49c799359b9c25d7f59c126862053cb591b63e69ffc640774b4ceb2b73", size = 369393, upload-time = "2025-11-15T06:17:46.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/69/b0f93d40bf24ecf3785097e601e389f9d8b5d33b979db7eee6ec5b92df40/azure_ai_projects-2.0.0b1-py3-none-any.whl", hash = "sha256:88eca153c63c8d93bc1776858ae036f1796ef0b9552b77736c446e575e3b132a", size = 221008, upload-time = "2025-11-12T04:00:01.281Z" }, + { url = "https://files.pythonhosted.org/packages/a9/41/d9a2b3eb33b4ffd9acfaa115cfd456e32d0c754227d6d78ec5d039ff75c2/azure_ai_projects-2.0.0b2-py3-none-any.whl", hash = "sha256:642496fdf9846c91f3557d39899d3893f0ce8f910334320686fc8f617492351d", size = 234023, upload-time = "2025-11-15T06:17:48.141Z" }, ] [[package]] @@ -1250,14 +1250,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -1357,7 +1357,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -1436,101 +1436,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.11.3" +version = "7.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/68/b53157115ef76d50d1d916d6240e5cd5b3c14dba8ba1b984632b8221fc2e/coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5", size = 216377, upload-time = "2025-11-10T00:10:27.317Z" }, - { url = "https://files.pythonhosted.org/packages/14/c1/d2f9d8e37123fe6e7ab8afcaab8195f13bc84a8b2f449a533fd4812ac724/coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7", size = 216892, upload-time = "2025-11-10T00:10:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/83/73/18f05d8010149b650ed97ee5c9f7e4ae68c05c7d913391523281e41c2495/coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb", size = 243650, upload-time = "2025-11-10T00:10:32.392Z" }, - { url = "https://files.pythonhosted.org/packages/63/3c/c0cbb296c0ecc6dcbd70f4b473fcd7fe4517bbef8b09f4326d78f38adb87/coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1", size = 245478, upload-time = "2025-11-10T00:10:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9a/dad288cf9faa142a14e75e39dc646d968b93d74e15c83e9b13fd628f2cb3/coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c", size = 247337, upload-time = "2025-11-10T00:10:35.655Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ba/f6148ebf5547b3502013175e41bf3107a4e34b7dd19f9793a6ce0e1cd61f/coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31", size = 244328, upload-time = "2025-11-10T00:10:37.459Z" }, - { url = "https://files.pythonhosted.org/packages/e6/4d/b93784d0b593c5df89a0d48cbbd2d0963e0ca089eaf877405849792e46d3/coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2", size = 245381, upload-time = "2025-11-10T00:10:39.229Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/6735bfd4f0f736d457642ee056a570d704c9d57fdcd5c91ea5d6b15c944e/coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507", size = 243390, upload-time = "2025-11-10T00:10:40.984Z" }, - { url = "https://files.pythonhosted.org/packages/db/3d/7ba68ed52d1873d450aefd8d2f5a353e67b421915cb6c174e4222c7b918c/coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832", size = 243654, upload-time = "2025-11-10T00:10:42.496Z" }, - { url = "https://files.pythonhosted.org/packages/14/26/be2720c4c7bf73c6591ae4ab503a7b5a31c7a60ced6dba855cfcb4a5af7e/coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e", size = 244272, upload-time = "2025-11-10T00:10:44.39Z" }, - { url = "https://files.pythonhosted.org/packages/90/20/086f5697780df146dbc0df4ae9b6db2b23ddf5aa550f977b2825137728e9/coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb", size = 218969, upload-time = "2025-11-10T00:10:45.863Z" }, - { url = "https://files.pythonhosted.org/packages/98/5c/cc6faba945ede5088156da7770e30d06c38b8591785ac99bcfb2074f9ef6/coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8", size = 219903, upload-time = "2025-11-10T00:10:47.676Z" }, - { url = "https://files.pythonhosted.org/packages/92/92/43a961c0f57b666d01c92bcd960c7f93677de5e4ee7ca722564ad6dee0fa/coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1", size = 216504, upload-time = "2025-11-10T00:10:49.524Z" }, - { url = "https://files.pythonhosted.org/packages/5d/5c/dbfc73329726aef26dbf7fefef81b8a2afd1789343a579ea6d99bf15d26e/coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06", size = 217006, upload-time = "2025-11-10T00:10:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/e0/878c84fb6661964bc435beb1e28c050650aa30e4c1cdc12341e298700bda/coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80", size = 247415, upload-time = "2025-11-10T00:10:52.805Z" }, - { url = "https://files.pythonhosted.org/packages/56/9e/0677e78b1e6a13527f39c4b39c767b351e256b333050539861c63f98bd61/coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa", size = 249332, upload-time = "2025-11-10T00:10:54.35Z" }, - { url = "https://files.pythonhosted.org/packages/54/90/25fc343e4ce35514262451456de0953bcae5b37dda248aed50ee51234cee/coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297", size = 251443, upload-time = "2025-11-10T00:10:55.832Z" }, - { url = "https://files.pythonhosted.org/packages/13/56/bc02bbc890fd8b155a64285c93e2ab38647486701ac9c980d457cdae857a/coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362", size = 247554, upload-time = "2025-11-10T00:10:57.829Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ab/0318888d091d799a82d788c1e8d8bd280f1d5c41662bbb6e11187efe33e8/coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87", size = 249139, upload-time = "2025-11-10T00:10:59.465Z" }, - { url = "https://files.pythonhosted.org/packages/79/d8/3ee50929c4cd36fcfcc0f45d753337001001116c8a5b8dd18d27ea645737/coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200", size = 247209, upload-time = "2025-11-10T00:11:01.432Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/3cf06e327401c293e60c962b4b8a2ceb7167c1a428a02be3adbd1d7c7e4c/coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4", size = 246936, upload-time = "2025-11-10T00:11:02.964Z" }, - { url = "https://files.pythonhosted.org/packages/99/0b/ffc03dc8f4083817900fd367110015ef4dd227b37284104a5eb5edc9c106/coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060", size = 247835, upload-time = "2025-11-10T00:11:04.405Z" }, - { url = "https://files.pythonhosted.org/packages/17/4d/dbe54609ee066553d0bcdcdf108b177c78dab836292bee43f96d6a5674d1/coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7", size = 218994, upload-time = "2025-11-10T00:11:05.966Z" }, - { url = "https://files.pythonhosted.org/packages/94/11/8e7155df53f99553ad8114054806c01a2c0b08f303ea7e38b9831652d83d/coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55", size = 219926, upload-time = "2025-11-10T00:11:07.936Z" }, - { url = "https://files.pythonhosted.org/packages/1f/93/bea91b6a9e35d89c89a1cd5824bc72e45151a9c2a9ca0b50d9e9a85e3ae3/coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc", size = 218599, upload-time = "2025-11-10T00:11:09.578Z" }, - { url = "https://files.pythonhosted.org/packages/c2/39/af056ec7a27c487e25c7f6b6e51d2ee9821dba1863173ddf4dc2eebef4f7/coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f", size = 216676, upload-time = "2025-11-10T00:11:11.566Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f8/21126d34b174d037b5d01bea39077725cbb9a0da94a95c5f96929c695433/coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e", size = 217034, upload-time = "2025-11-10T00:11:13.12Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0fd35f35658cdd11f7686303214bd5908225838f374db47f9e457c8d6df8/coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a", size = 248531, upload-time = "2025-11-10T00:11:15.023Z" }, - { url = "https://files.pythonhosted.org/packages/8f/59/0bfc5900fc15ce4fd186e092451de776bef244565c840c9c026fd50857e1/coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1", size = 251290, upload-time = "2025-11-10T00:11:16.628Z" }, - { url = "https://files.pythonhosted.org/packages/71/88/d5c184001fa2ac82edf1b8f2cd91894d2230d7c309e937c54c796176e35b/coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd", size = 252375, upload-time = "2025-11-10T00:11:18.249Z" }, - { url = "https://files.pythonhosted.org/packages/5c/29/f60af9f823bf62c7a00ce1ac88441b9a9a467e499493e5cc65028c8b8dd2/coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5", size = 248946, upload-time = "2025-11-10T00:11:20.202Z" }, - { url = "https://files.pythonhosted.org/packages/67/16/4662790f3b1e03fce5280cad93fd18711c35980beb3c6f28dca41b5230c6/coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e", size = 250310, upload-time = "2025-11-10T00:11:21.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dd6c2e28308a83e5fc1ee602f8204bd3aa5af685c104cb54499230cf56db/coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044", size = 248461, upload-time = "2025-11-10T00:11:23.384Z" }, - { url = "https://files.pythonhosted.org/packages/16/fe/b71af12be9f59dc9eb060688fa19a95bf3223f56c5af1e9861dfa2275d2c/coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7", size = 248039, upload-time = "2025-11-10T00:11:25.07Z" }, - { url = "https://files.pythonhosted.org/packages/11/b8/023b2003a2cd96bdf607afe03d9b96c763cab6d76e024abe4473707c4eb8/coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405", size = 249903, upload-time = "2025-11-10T00:11:26.992Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/5f1076311aa67b1fa4687a724cc044346380e90ce7d94fec09fd384aa5fd/coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e", size = 219201, upload-time = "2025-11-10T00:11:28.619Z" }, - { url = "https://files.pythonhosted.org/packages/4f/24/d21688f48fe9fcc778956680fd5aaf69f4e23b245b7c7a4755cbd421d25b/coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055", size = 220012, upload-time = "2025-11-10T00:11:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9e/d5eb508065f291456378aa9b16698b8417d87cb084c2b597f3beb00a8084/coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f", size = 218652, upload-time = "2025-11-10T00:11:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" }, - { url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" }, - { url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" }, - { url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" }, - { url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" }, - { url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" }, - { url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" }, - { url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" }, - { url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" }, - { url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/634ec396e45aded1772dccf6c236e3e7c9604bc47b816e928f32ce7987d1/coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c", size = 216746, upload-time = "2025-11-10T00:12:23.089Z" }, - { url = "https://files.pythonhosted.org/packages/28/76/1079547f9d46f9c7c7d0dad35b6873c98bc5aa721eeabceafabd722cd5e7/coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203", size = 217077, upload-time = "2025-11-10T00:12:24.863Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/6ad80d6ae0d7cb743b9a98df8bb88b1ff3dc54491508a4a97549c2b83400/coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240", size = 248122, upload-time = "2025-11-10T00:12:26.553Z" }, - { url = "https://files.pythonhosted.org/packages/20/1d/784b87270784b0b88e4beec9d028e8d58f73ae248032579c63ad2ac6f69a/coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83", size = 250638, upload-time = "2025-11-10T00:12:28.555Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/b6dd31e23e004e9de84d1a8672cd3d73e50f5dae65dbd0f03fa2cdde6100/coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902", size = 251972, upload-time = "2025-11-10T00:12:30.246Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ef/f9c64d76faac56b82daa036b34d4fe9ab55eb37f22062e68e9470583e688/coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428", size = 248147, upload-time = "2025-11-10T00:12:32.195Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/5b666f90a8f8053bd264a1ce693d2edef2368e518afe70680070fca13ecd/coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75", size = 249995, upload-time = "2025-11-10T00:12:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/eb/7b/871e991ffb5d067f8e67ffb635dabba65b231d6e0eb724a4a558f4a702a5/coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704", size = 247948, upload-time = "2025-11-10T00:12:36.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/8b/ce454f0af9609431b06dbe5485fc9d1c35ddc387e32ae8e374f49005748b/coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b", size = 247770, upload-time = "2025-11-10T00:12:38.167Z" }, - { url = "https://files.pythonhosted.org/packages/61/8f/79002cb58a61dfbd2085de7d0a46311ef2476823e7938db80284cedd2428/coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131", size = 249431, upload-time = "2025-11-10T00:12:40.354Z" }, - { url = "https://files.pythonhosted.org/packages/58/cc/d06685dae97468ed22999440f2f2f5060940ab0e7952a7295f236d98cce7/coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a", size = 219508, upload-time = "2025-11-10T00:12:42.231Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ed/770cd07706a3598c545f62d75adf2e5bd3791bffccdcf708ec383ad42559/coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86", size = 220325, upload-time = "2025-11-10T00:12:44.065Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ac/6a1c507899b6fb1b9a56069954365f655956bcc648e150ce64c2b0ecbed8/coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e", size = 218899, upload-time = "2025-11-10T00:12:46.18Z" }, - { url = "https://files.pythonhosted.org/packages/9a/58/142cd838d960cd740654d094f7b0300d7b81534bb7304437d2439fb685fb/coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df", size = 217471, upload-time = "2025-11-10T00:12:48.392Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2c/2f44d39eb33e41ab3aba80571daad32e0f67076afcf27cb443f9e5b5a3ee/coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001", size = 217742, upload-time = "2025-11-10T00:12:50.182Z" }, - { url = "https://files.pythonhosted.org/packages/32/76/8ebc66c3c699f4de3174a43424c34c086323cd93c4930ab0f835731c443a/coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de", size = 259120, upload-time = "2025-11-10T00:12:52.451Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/78a3302b9595f331b86e4f12dfbd9252c8e93d97b8631500888f9a3a2af7/coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926", size = 261229, upload-time = "2025-11-10T00:12:54.667Z" }, - { url = "https://files.pythonhosted.org/packages/07/59/1a9c0844dadef2a6efac07316d9781e6c5a3f3ea7e5e701411e99d619bfd/coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd", size = 263642, upload-time = "2025-11-10T00:12:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/37/86/66c15d190a8e82eee777793cabde730640f555db3c020a179625a2ad5320/coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac", size = 258193, upload-time = "2025-11-10T00:12:58.687Z" }, - { url = "https://files.pythonhosted.org/packages/c7/c7/4a4aeb25cb6f83c3ec4763e5f7cc78da1c6d4ef9e22128562204b7f39390/coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46", size = 261107, upload-time = "2025-11-10T00:13:00.502Z" }, - { url = "https://files.pythonhosted.org/packages/ed/91/b986b5035f23cf0272446298967ecdd2c3c0105ee31f66f7e6b6948fd7f8/coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64", size = 258717, upload-time = "2025-11-10T00:13:02.747Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c7/6c084997f5a04d050c513545d3344bfa17bd3b67f143f388b5757d762b0b/coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f", size = 257541, upload-time = "2025-11-10T00:13:04.689Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c5/38e642917e406930cb67941210a366ccffa767365c8f8d9ec0f465a8b218/coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820", size = 259872, upload-time = "2025-11-10T00:13:06.559Z" }, - { url = "https://files.pythonhosted.org/packages/b7/67/5e812979d20c167f81dbf9374048e0193ebe64c59a3d93d7d947b07865fa/coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237", size = 220289, upload-time = "2025-11-10T00:13:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/24/3a/b72573802672b680703e0df071faadfab7dcd4d659aaaffc4626bc8bbde8/coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9", size = 221398, upload-time = "2025-11-10T00:13:10.734Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4e/649628f28d38bad81e4e8eb3f78759d20ac173e3c456ac629123815feb40/coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd", size = 219435, upload-time = "2025-11-10T00:13:12.712Z" }, - { url = "https://files.pythonhosted.org/packages/19/8f/92bdd27b067204b99f396a1414d6342122f3e2663459baf787108a6b8b84/coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe", size = 208478, upload-time = "2025-11-10T00:13:14.908Z" }, + { url = "https://files.pythonhosted.org/packages/26/4a/0dc3de1c172d35abe512332cfdcc43211b6ebce629e4cc42e6cd25ed8f4d/coverage-7.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:32b75c2ba3f324ee37af3ccee5b30458038c50b349ad9b88cee85096132a575b", size = 217409, upload-time = "2025-11-18T13:31:53.122Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/086198b98db0109ad4f84241e8e9ea7e5fb2db8c8ffb787162d40c26cc76/coverage-7.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb2a1b6ab9fe833714a483a915de350abc624a37149649297624c8d57add089c", size = 217927, upload-time = "2025-11-18T13:31:54.458Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5f/34614dbf5ce0420828fc6c6f915126a0fcb01e25d16cf141bf5361e6aea6/coverage-7.12.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5734b5d913c3755e72f70bf6cc37a0518d4f4745cde760c5d8e12005e62f9832", size = 244678, upload-time = "2025-11-18T13:31:55.805Z" }, + { url = "https://files.pythonhosted.org/packages/55/7b/6b26fb32e8e4a6989ac1d40c4e132b14556131493b1d06bc0f2be169c357/coverage-7.12.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b527a08cdf15753279b7afb2339a12073620b761d79b81cbe2cdebdb43d90daa", size = 246507, upload-time = "2025-11-18T13:31:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/06/42/7d70e6603d3260199b90fb48b537ca29ac183d524a65cc31366b2e905fad/coverage-7.12.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bb44c889fb68004e94cab71f6a021ec83eac9aeabdbb5a5a88821ec46e1da73", size = 248366, upload-time = "2025-11-18T13:31:58.362Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4a/d86b837923878424c72458c5b25e899a3c5ca73e663082a915f5b3c4d749/coverage-7.12.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b59b501455535e2e5dde5881739897967b272ba25988c89145c12d772810ccb", size = 245366, upload-time = "2025-11-18T13:31:59.572Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c2/2adec557e0aa9721875f06ced19730fdb7fc58e31b02b5aa56f2ebe4944d/coverage-7.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8842f17095b9868a05837b7b1b73495293091bed870e099521ada176aa3e00e", size = 246408, upload-time = "2025-11-18T13:32:00.784Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4b/8bd1f1148260df11c618e535fdccd1e5aaf646e55b50759006a4f41d8a26/coverage-7.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5a6f20bf48b8866095c6820641e7ffbe23f2ac84a2efc218d91235e404c7777", size = 244416, upload-time = "2025-11-18T13:32:01.963Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/3a248dd6a83df90414c54a4e121fd081fb20602ca43955fbe1d60e2312a9/coverage-7.12.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5f3738279524e988d9da2893f307c2093815c623f8d05a8f79e3eff3a7a9e553", size = 244681, upload-time = "2025-11-18T13:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/76/30/aa833827465a5e8c938935f5d91ba055f70516941078a703740aaf1aa41f/coverage-7.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0d68c1f7eabbc8abe582d11fa393ea483caf4f44b0af86881174769f185c94d", size = 245300, upload-time = "2025-11-18T13:32:04.686Z" }, + { url = "https://files.pythonhosted.org/packages/38/24/f85b3843af1370fb3739fa7571819b71243daa311289b31214fe3e8c9d68/coverage-7.12.0-cp310-cp310-win32.whl", hash = "sha256:7670d860e18b1e3ee5930b17a7d55ae6287ec6e55d9799982aa103a2cc1fa2ef", size = 220008, upload-time = "2025-11-18T13:32:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a2/c7da5b9566f7164db9eefa133d17761ecb2c2fde9385d754e5b5c80f710d/coverage-7.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:f999813dddeb2a56aab5841e687b68169da0d3f6fc78ccf50952fa2463746022", size = 220943, upload-time = "2025-11-18T13:32:07.166Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, + { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, + { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, + { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, + { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, + { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, + { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, + { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, + { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, + { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, + { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, + { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, + { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, + { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, + { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, + { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, + { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, + { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, + { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, + { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, + { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, + { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, ] [package.optional-dependencies] @@ -1729,7 +1729,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -2375,11 +2375,11 @@ wheels = [ [[package]] name = "httpdbg" -version = "2.1.3" +version = "2.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/c0/a54d8705ae57e76679cf21dbc6dba3eb4c5cb9f99fcd9cb99e159fb12a9d/httpdbg-2.1.3.tar.gz", hash = "sha256:da32fd7cab8032927ba4717c6c9108dd4aeb0d9a42636d34a43ab11541daac26", size = 80694, upload-time = "2025-11-02T13:48:13.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/75/5938d86adcf3b6b53b85da6e5d3af53af1d5cc4620fdffa14f09923bb952/httpdbg-2.1.4.tar.gz", hash = "sha256:61078378e03af7d2e2edb505ce74d81f4d11bb7781422eae101f76428ab0f7a2", size = 80686, upload-time = "2025-11-15T13:46:20.264Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6e/567ace955933023403e4861d161de8b559d712b559e445cc6d9a95d8e26c/httpdbg-2.1.3-py3-none-any.whl", hash = "sha256:9faa4d66f308670ddde0c6b05281066cb10b56846e6c4d3eb712123c28ea019d", size = 88173, upload-time = "2025-11-02T13:48:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/fb/90/cd5f00f5556fb99f1c1220336a84c7ede4309d954ced6b3d5f204561b617/httpdbg-2.1.4-py3-none-any.whl", hash = "sha256:943596ae9c4f89ed3957cc7f1f6da93a537caeef45f3880125c0ceacf96bfcd3", size = 88168, upload-time = "2025-11-15T13:46:18.935Z" }, ] [[package]] @@ -2813,7 +2813,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.79.3" +version = "1.80.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -2829,9 +2829,9 @@ dependencies = [ { name = "tiktoken", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tokenizers", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/0a/587c3f895f5d6c842d6cd630204c8bf7de677fc69ce2bd26e812c02b6e0b/litellm-1.79.3.tar.gz", hash = "sha256:4da4716f8da3e1b77838262c36d3016146860933e0489171658a9d4a3fd59b1b", size = 11319885, upload-time = "2025-11-09T02:33:17.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/8c/48d533affdbc6d485b7ad4221cd3b40b8c12f9f5568edfe0be0b11e7b945/litellm-1.80.0.tar.gz", hash = "sha256:eeac733eb6b226f9e5fb020f72fe13a32b3354b001dc62bcf1bc4d9b526d6231", size = 11591976, upload-time = "2025-11-16T00:03:51.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/ad/3e030c925c99b9a2f1573bf376259338b502ed1aa25ae768bf1f79d8b1bf/litellm-1.79.3-py3-none-any.whl", hash = "sha256:16314049d109e5cadb2abdccaf2e07ea03d2caa3a9b3f54f34b5b825092b4eeb", size = 10412553, upload-time = "2025-11-09T02:33:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/ea/53/aa31e4d057b3746b3c323ca993003d6cf15ef987e7fe7ceb53681695ae87/litellm-1.80.0-py3-none-any.whl", hash = "sha256:fd0009758f4772257048d74bf79bb64318859adb4ea49a8b66fdbc718cd80b6e", size = 10492975, upload-time = "2025-11-16T00:03:49.182Z" }, ] [package.optional-dependencies] @@ -2864,20 +2864,20 @@ proxy = [ [[package]] name = "litellm-enterprise" -version = "0.1.20" +version = "0.1.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/ae/5478c23dad4e63b8fb01387a1d17b05c8b62a0182f76101a86eca74104c1/litellm_enterprise-0.1.20.tar.gz", hash = "sha256:f6b8dd75b53bd835c68caf6402a8bae744a150db7bb6b0e617178c6056ac6c01", size = 61379, upload-time = "2025-09-19T02:13:03.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/3e/19ccb3302aa3b96acb072b038c594202ac5cff3cf99bb2fe986b690be4c7/litellm_enterprise-0.1.21.tar.gz", hash = "sha256:2e9d9603ecbbced887d5028afd73663c4db7159a0a213f2e69844e8f9e7692b5", size = 143974, upload-time = "2025-11-15T18:30:32.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/e2/3d113fe5262577d92ca2ace2454ad042be9c01c62bf1e44be4e2d12b2cc5/litellm_enterprise-0.1.20-py3-none-any.whl", hash = "sha256:744a79956a8cd7748ef4c3f40d5a564c61519834e706beafbc0b931162773ae8", size = 120792, upload-time = "2025-09-19T02:13:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/0aa480b636307a8e1c2dcfd5be2d045d500967c04db00f50189a572329ab/litellm_enterprise-0.1.21-py3-none-any.whl", hash = "sha256:382cf030650a6318155f4b03e0149245706e429345906cde04cb31905eae8398", size = 107004, upload-time = "2025-11-15T18:30:31.958Z" }, ] [[package]] name = "litellm-proxy-extras" -version = "0.4.3" +version = "0.4.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/59/6a9963955f6499c9ec0e65a9445add92f4854d3b03c920127a406ae61dd3/litellm_proxy_extras-0.4.3.tar.gz", hash = "sha256:420400d0db186319695526f6765d3d481206fe025b70bc74a1ce895a7d720bee", size = 16920, upload-time = "2025-11-08T18:22:16.296Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/89/296bc16e4c42938953fa768555b3308aa25adcbe2f2a218e3043da1b094d/litellm_proxy_extras-0.4.5.tar.gz", hash = "sha256:ad79a4061e06f9a9127e517d153dfbb8a1a91de11873a633de5a984cbcd40070", size = 17864, upload-time = "2025-11-15T03:00:16.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/d6/954a8ce8810d7daee5ec1334d74bbc3da889d89cba9825a4750fab7f1187/litellm_proxy_extras-0.4.3-py3-none-any.whl", hash = "sha256:e7ab09aa78d04d02dc48975620defa36784e1a0baa6d04a078b98b5717fcae24", size = 35518, upload-time = "2025-11-08T18:22:15.143Z" }, + { url = "https://files.pythonhosted.org/packages/97/05/70783bb4dbbb2a57d97c0d0b91122f1b83b8471a654a6dcd66cbb67ca83a/litellm_proxy_extras-0.4.5-py3-none-any.whl", hash = "sha256:917954c2f6d2f940bea8355695d8a17057816f126e898a4fca73b9d56cee09e6", size = 36427, upload-time = "2025-11-15T03:00:15.275Z" }, ] [[package]] @@ -3001,7 +3001,7 @@ dependencies = [ { name = "fonttools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "kiwisolver", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3067,7 +3067,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.21.1" +version = "1.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3085,9 +3085,9 @@ dependencies = [ { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "uvicorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/25/4df633e7574254ada574822db2245bbee424725d1b01bccae10bf128794e/mcp-1.21.1.tar.gz", hash = "sha256:540e6ac4b12b085c43f14879fde04cbdb10148a09ea9492ff82d8c7ba651a302", size = 469071, upload-time = "2025-11-13T20:33:46.139Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/bb/147bf5d47b8d6f3fb315204cd2f567b1ad85511acd8d59e85cccf7cce65c/mcp-1.21.2.tar.gz", hash = "sha256:b6497b7f7a77aabe263931a3335956dfd45fe8b1e2168931e455914ba1a7f4c9", size = 470799, upload-time = "2025-11-17T13:56:06.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/af/01fb42df59ad15925ffc1e2e609adafddd3ac4572f606faae0dc8b55ba0c/mcp-1.21.1-py3-none-any.whl", hash = "sha256:dd35abe36d68530a8a1291daa25d50276d8731e545c0434d6e250a3700dd2a6d", size = 174852, upload-time = "2025-11-13T20:33:44.502Z" }, + { url = "https://files.pythonhosted.org/packages/67/0f/669ecbe78a0ba192afcc0b026ae62d1005779e91bad27ab9d703401510bf/mcp-1.21.2-py3-none-any.whl", hash = "sha256:59413ef15db757a785e3859548c1a7ffc7be57bf162c3c24afc0e04fd9f4181c", size = 174854, upload-time = "2025-11-17T13:56:04.987Z" }, ] [package.optional-dependencies] @@ -3164,44 +3164,48 @@ wheels = [ [[package]] name = "ml-dtypes" -version = "0.5.3" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/bb/1f32124ab6d3a279ea39202fe098aea95b2d81ef0ce1d48612b6bf715e82/ml_dtypes-0.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0a1d68a7cb53e3f640b2b6a34d12c0542da3dd935e560fdf463c0c77f339fc20", size = 667409, upload-time = "2025-07-29T18:38:17.321Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ac/e002d12ae19136e25bb41c7d14d7e1a1b08f3c0e99a44455ff6339796507/ml_dtypes-0.5.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd5a6c711b5350f3cbc2ac28def81cd1c580075ccb7955e61e9d8f4bfd40d24", size = 4960702, upload-time = "2025-07-29T18:38:19.616Z" }, - { url = "https://files.pythonhosted.org/packages/dd/12/79e9954e6b3255a4b1becb191a922d6e2e94d03d16a06341ae9261963ae8/ml_dtypes-0.5.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdcf26c2dbc926b8a35ec8cbfad7eff1a8bd8239e12478caca83a1fc2c400dc2", size = 4933471, upload-time = "2025-07-29T18:38:21.809Z" }, - { url = "https://files.pythonhosted.org/packages/d5/aa/d1eff619e83cd1ddf6b561d8240063d978e5d887d1861ba09ef01778ec3a/ml_dtypes-0.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:aecbd7c5272c82e54d5b99d8435fd10915d1bc704b7df15e4d9ca8dc3902be61", size = 206330, upload-time = "2025-07-29T18:38:23.663Z" }, - { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, - { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, - { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, - { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/1bcc98a66de7b2455dfb292f271452cac9edc4e870796e0d87033524d790/ml_dtypes-0.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5103856a225465371fe119f2fef737402b705b810bd95ad5f348e6e1a6ae21af", size = 663781, upload-time = "2025-07-29T18:38:42.984Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2c/bd2a79ba7c759ee192b5601b675b180a3fd6ccf48ffa27fe1782d280f1a7/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cae435a68861660af81fa3c5af16b70ca11a17275c5b662d9c6f58294e0f113", size = 4956217, upload-time = "2025-07-29T18:38:44.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/f3/091ba84e5395d7fe5b30c081a44dec881cd84b408db1763ee50768b2ab63/ml_dtypes-0.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6936283b56d74fbec431ca57ce58a90a908fdbd14d4e2d22eea6d72bb208a7b7", size = 4933109, upload-time = "2025-07-29T18:38:46.405Z" }, - { url = "https://files.pythonhosted.org/packages/bc/24/054036dbe32c43295382c90a1363241684c4d6aaa1ecc3df26bd0c8d5053/ml_dtypes-0.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:d0f730a17cf4f343b2c7ad50cee3bd19e969e793d2be6ed911f43086460096e4", size = 208187, upload-time = "2025-07-29T18:38:48.24Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/7dc3ec6794a4a9004c765e0c341e32355840b698f73fd2daff46f128afc1/ml_dtypes-0.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2db74788fc01914a3c7f7da0763427280adfc9cd377e9604b6b64eb8097284bd", size = 161559, upload-time = "2025-07-29T18:38:50.493Z" }, - { url = "https://files.pythonhosted.org/packages/12/91/e6c7a0d67a152b9330445f9f0cf8ae6eee9b83f990b8c57fe74631e42a90/ml_dtypes-0.5.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93c36a08a6d158db44f2eb9ce3258e53f24a9a4a695325a689494f0fdbc71770", size = 689321, upload-time = "2025-07-29T18:38:52.03Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6c/b7b94b84a104a5be1883305b87d4c6bd6ae781504474b4cca067cb2340ec/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e44a3761f64bc009d71ddb6d6c71008ba21b53ab6ee588dadab65e2fa79eafc", size = 5274495, upload-time = "2025-07-29T18:38:53.797Z" }, - { url = "https://files.pythonhosted.org/packages/5b/38/6266604dffb43378055394ea110570cf261a49876fc48f548dfe876f34cc/ml_dtypes-0.5.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdf40d2aaabd3913dec11840f0d0ebb1b93134f99af6a0a4fd88ffe924928ab4", size = 5285422, upload-time = "2025-07-29T18:38:56.603Z" }, - { url = "https://files.pythonhosted.org/packages/7c/88/8612ff177d043a474b9408f0382605d881eeb4125ba89d4d4b3286573a83/ml_dtypes-0.5.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:aec640bd94c4c85c0d11e2733bd13cbb10438fb004852996ec0efbc6cacdaf70", size = 661182, upload-time = "2025-07-29T18:38:58.414Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2b/0569a5e88b29240d373e835107c94ae9256fb2191d3156b43b2601859eff/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bda32ce212baa724e03c68771e5c69f39e584ea426bfe1a701cb01508ffc7035", size = 4956187, upload-time = "2025-07-29T18:39:00.611Z" }, - { url = "https://files.pythonhosted.org/packages/51/66/273c2a06ae44562b104b61e6b14444da00061fd87652506579d7eb2c40b1/ml_dtypes-0.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c205cac07d24a29840c163d6469f61069ce4b065518519216297fc2f261f8db9", size = 4930911, upload-time = "2025-07-29T18:39:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/93/ab/606be3e87dc0821bd360c8c1ee46108025c31a4f96942b63907bb441b87d/ml_dtypes-0.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:cd7c0bb22d4ff86d65ad61b5dd246812e8993fbc95b558553624c33e8b6903ea", size = 216664, upload-time = "2025-07-29T18:39:03.927Z" }, - { url = "https://files.pythonhosted.org/packages/30/a2/e900690ca47d01dffffd66375c5de8c4f8ced0f1ef809ccd3b25b3e6b8fa/ml_dtypes-0.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:9d55ea7f7baf2aed61bf1872116cefc9d0c3693b45cae3916897ee27ef4b835e", size = 160203, upload-time = "2025-07-29T18:39:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/53/21/783dfb51f40d2660afeb9bccf3612b99f6a803d980d2a09132b0f9d216ab/ml_dtypes-0.5.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:e12e29764a0e66a7a31e9b8bf1de5cc0423ea72979f45909acd4292de834ccd3", size = 689324, upload-time = "2025-07-29T18:39:07.567Z" }, - { url = "https://files.pythonhosted.org/packages/09/f7/a82d249c711abf411ac027b7163f285487f5e615c3e0716c61033ce996ab/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19f6c3a4f635c2fc9e2aa7d91416bd7a3d649b48350c51f7f715a09370a90d93", size = 5275917, upload-time = "2025-07-29T18:39:09.339Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3c/541c4b30815ab90ebfbb51df15d0b4254f2f9f1e2b4907ab229300d5e6f2/ml_dtypes-0.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ab039ffb40f3dc0aeeeba84fd6c3452781b5e15bef72e2d10bcb33e4bbffc39", size = 5285284, upload-time = "2025-07-29T18:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, ] [[package]] @@ -3433,11 +3437,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.11.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/a2/25208347aa4c2d82a265cf4bc0873aaf5069f525c0438146821e7fc19ef5/narwhals-2.11.0.tar.gz", hash = "sha256:d23f3ea7efc6b4d0355444a72de6b8fa3011175585246c3400c894a7583964af", size = 589233, upload-time = "2025-11-10T16:28:35.675Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/f8/e1c28f24b641871c14ccae7ba6381f3c7827789a06e947ce975ae8a9075a/narwhals-2.12.0.tar.gz", hash = "sha256:075b6d56f3a222613793e025744b129439ecdff9292ea6615dd983af7ba6ea44", size = 590404, upload-time = "2025-11-17T10:53:28.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/a1/4d21933898e23b011ae0528151b57a9230a62960d0919bf2ee48c7f5c20a/narwhals-2.11.0-py3-none-any.whl", hash = "sha256:a9795e1e44aa94e5ba6406ef1c5ee4c172414ced4f1aea4a79e5894f0c7378d4", size = 423069, upload-time = "2025-11-10T16:28:33.522Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9a/c6f79de7ba3a0a8473129936b7b90aa461d3d46fec6f1627672b1dccf4e9/narwhals-2.12.0-py3-none-any.whl", hash = "sha256:baeba5d448a30b04c299a696bd9ee5ff73e4742143e06c49ca316b46539a7cbb", size = 425014, upload-time = "2025-11-17T10:53:26.65Z" }, ] [[package]] @@ -3518,7 +3522,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.4" +version = "2.3.5" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'darwin'", @@ -3531,81 +3535,81 @@ resolution-markers = [ "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, - { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, - { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, - { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, - { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, - { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, - { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, - { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, - { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, - { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, ] [[package]] @@ -3619,7 +3623,7 @@ wheels = [ [[package]] name = "openai" -version = "2.8.0" +version = "2.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -3631,9 +3635,9 @@ dependencies = [ { name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0c/b9321e12f89e236f5e9a46346c30fb801818e22ba33b798a5aca84be895c/openai-2.8.0.tar.gz", hash = "sha256:4851908f6d6fcacbd47ba659c5ac084f7725b752b6bfa1e948b6fbfc111a6bad", size = 602412, upload-time = "2025-11-13T18:15:25.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490, upload-time = "2025-11-17T22:39:59.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/e1/0a6560bab7fb7b5a88d35a505b859c6d969cb2fa2681b568eb5d95019dec/openai-2.8.0-py3-none-any.whl", hash = "sha256:ba975e347f6add2fe13529ccb94d54a578280e960765e5224c34b08d7e029ddf", size = 1022692, upload-time = "2025-11-13T18:15:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688, upload-time = "2025-11-17T22:39:57.675Z" }, ] [[package]] @@ -3817,14 +3821,14 @@ wheels = [ [[package]] name = "orderedmultidict" -version = "1.0.1" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/4e/3823a27d764bb8388711f4cb6f24e58453e92d6928f4163fdb01e3a3789f/orderedmultidict-1.0.1.tar.gz", hash = "sha256:04070bbb5e87291cc9bfa51df413677faf2141c73c61d2a5f7b26bea3cd882ad", size = 20706, upload-time = "2019-07-10T20:11:47.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973, upload-time = "2025-11-18T08:00:42.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/16/5e95c70bda8fe6ea715005c0db8e602400bdba50ae3c72cb380eba551289/orderedmultidict-1.0.1-py2.py3-none-any.whl", hash = "sha256:43c839a17ee3cdd62234c47deca1a8508a3f2ca1d0678a3bf791c87cf84adbf3", size = 11699, upload-time = "2019-07-10T20:11:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897, upload-time = "2025-11-18T08:00:41.44Z" }, ] [[package]] @@ -3932,7 +3936,7 @@ version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4124,15 +4128,15 @@ wheels = [ [[package]] name = "plotly" -version = "6.4.0" +version = "6.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "narwhals", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/e6/b768650072837505804bed4790c5449ba348a3b720e27ca7605414e998cd/plotly-6.4.0.tar.gz", hash = "sha256:68c6db2ed2180289ef978f087841148b7efda687552276da15a6e9b92107052a", size = 7012379, upload-time = "2025-11-04T17:59:26.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/05/1199e2a03ce6637960bc1e951ca0f928209a48cfceb57355806a88f214cf/plotly-6.5.0.tar.gz", hash = "sha256:d5d38224883fd38c1409bef7d6a8dc32b74348d39313f3c52ca998b8e447f5c8", size = 7013624, upload-time = "2025-11-17T18:39:24.523Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/ae/89b45ccccfeebc464c9233de5675990f75241b8ee4cd63227800fdf577d1/plotly-6.4.0-py3-none-any.whl", hash = "sha256:a1062eafbdc657976c2eedd276c90e184ccd6c21282a5e9ee8f20efca9c9a4c5", size = 9892458, upload-time = "2025-11-04T17:59:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c3/3031c931098de393393e1f93a38dc9ed6805d86bb801acc3cf2d5bd1e6b7/plotly-6.5.0-py3-none-any.whl", hash = "sha256:5ac851e100367735250206788a2b1325412aa4a4917a4fe3e6f0bc5aa6f3d90a", size = 9893174, upload-time = "2025-11-17T18:39:20.351Z" }, ] [[package]] @@ -4207,7 +4211,7 @@ wheels = [ [[package]] name = "posthog" -version = "7.0.0" +version = "7.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -4217,9 +4221,9 @@ dependencies = [ { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/4d/16d777528149cd0e06306973081b5b070506abcd0fe831c6cb6966260d59/posthog-7.0.0.tar.gz", hash = "sha256:94973227f5fe5e7d656d305ff48c8bff3d505fd1e78b6fcd7ccc9dfe8d3401c2", size = 126504, upload-time = "2025-11-11T18:13:06.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/d4/b9afe855a8a7a1bf4459c28ae4c300b40338122dc850acabefcf2c3df24d/posthog-7.0.1.tar.gz", hash = "sha256:21150562c2630a599c1d7eac94bc5c64eb6f6acbf3ff52ccf1e57345706db05a", size = 126985, upload-time = "2025-11-15T12:44:22.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/9a/dc29b9ff4e5233a3c071b6b4c85dba96f4fcb9169c460bc81abd98555fb3/posthog-7.0.0-py3-none-any.whl", hash = "sha256:676d8a5197a17bf7bd00e31020a5f232988f249f57aab532f0d01c6243835934", size = 144727, upload-time = "2025-11-11T18:13:05.444Z" }, + { url = "https://files.pythonhosted.org/packages/05/0c/8b6b20b0be71725e6e8a32dcd460cdbf62fe6df9bc656a650150dc98fedd/posthog-7.0.1-py3-none-any.whl", hash = "sha256:efe212d8d88a9ba80a20c588eab4baf4b1a5e90e40b551160a5603bb21e96904", size = 145234, upload-time = "2025-11-15T12:44:21.247Z" }, ] [[package]] @@ -4976,21 +4980,21 @@ wheels = [ [[package]] name = "qdrant-client" -version = "1.15.1" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "grpcio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "httpx", extra = ["http2"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "portalocker", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/8b/76c7d325e11d97cb8eb5e261c3759e9ed6664735afbf32fdded5b580690c/qdrant_client-1.15.1.tar.gz", hash = "sha256:631f1f3caebfad0fd0c1fba98f41be81d9962b7bf3ca653bed3b727c0e0cbe0e", size = 295297, upload-time = "2025-07-31T19:35:19.627Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/16/366541897d270ee3f9c3f87da145baa8a5c9cc5190e0e53e8bbec1267cff/qdrant_client-1.16.0.tar.gz", hash = "sha256:0716aa0b7cca39745829c2e8ea0beb275fe2990e743ad803eabd6218e4b35c1b", size = 284128, upload-time = "2025-11-17T13:19:52.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/33/d8df6a2b214ffbe4138db9a1efe3248f67dc3c671f82308bea1582ecbbb7/qdrant_client-1.15.1-py3-none-any.whl", hash = "sha256:2b975099b378382f6ca1cfb43f0d59e541be6e16a5892f282a4b8de7eff5cb63", size = 337331, upload-time = "2025-07-31T19:35:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/3a69bb56835c4b2e9fa780655790937011ac389b0408b9a1147eaa2cee22/qdrant_client-1.16.0-py3-none-any.whl", hash = "sha256:6b932393e84e4c0233e5b2eb96b0918e968725855adae4d9c541761f4c50cf11", size = 328579, upload-time = "2025-11-17T13:19:51.092Z" }, ] [[package]] @@ -5013,7 +5017,7 @@ dependencies = [ { name = "jsonpath-ng", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "ml-dtypes", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "python-ulid", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5176,124 +5180,124 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.28.0" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359, upload-time = "2025-11-16T14:50:39.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/f8/13bb772dc7cbf2c3c5b816febc34fa0cb2c64a08e0569869585684ce6631/rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a", size = 362820, upload-time = "2025-10-22T22:21:15.074Z" }, - { url = "https://files.pythonhosted.org/packages/84/91/6acce964aab32469c3dbe792cb041a752d64739c534e9c493c701ef0c032/rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207", size = 348499, upload-time = "2025-10-22T22:21:17.658Z" }, - { url = "https://files.pythonhosted.org/packages/f1/93/c05bb1f4f5e0234db7c4917cb8dd5e2e0a9a7b26dc74b1b7bee3c9cfd477/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba", size = 379356, upload-time = "2025-10-22T22:21:19.847Z" }, - { url = "https://files.pythonhosted.org/packages/5c/37/e292da436f0773e319753c567263427cdf6c645d30b44f09463ff8216cda/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85", size = 390151, upload-time = "2025-10-22T22:21:21.569Z" }, - { url = "https://files.pythonhosted.org/packages/76/87/a4e3267131616e8faf10486dc00eaedf09bd61c87f01e5ef98e782ee06c9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d", size = 524831, upload-time = "2025-10-22T22:21:23.394Z" }, - { url = "https://files.pythonhosted.org/packages/e1/c8/4a4ca76f0befae9515da3fad11038f0fce44f6bb60b21fe9d9364dd51fb0/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7", size = 404687, upload-time = "2025-10-22T22:21:25.201Z" }, - { url = "https://files.pythonhosted.org/packages/6a/65/118afe854424456beafbbebc6b34dcf6d72eae3a08b4632bc4220f8240d9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa", size = 382683, upload-time = "2025-10-22T22:21:26.536Z" }, - { url = "https://files.pythonhosted.org/packages/f7/bc/0625064041fb3a0c77ecc8878c0e8341b0ae27ad0f00cf8f2b57337a1e63/rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476", size = 398927, upload-time = "2025-10-22T22:21:27.864Z" }, - { url = "https://files.pythonhosted.org/packages/5d/1a/fed7cf2f1ee8a5e4778f2054153f2cfcf517748875e2f5b21cf8907cd77d/rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04", size = 411590, upload-time = "2025-10-22T22:21:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/c1/64/a8e0f67fa374a6c472dbb0afdaf1ef744724f165abb6899f20e2f1563137/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8", size = 559843, upload-time = "2025-10-22T22:21:30.917Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ea/e10353f6d7c105be09b8135b72787a65919971ae0330ad97d87e4e199880/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4", size = 584188, upload-time = "2025-10-22T22:21:32.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/b0/a19743e0763caf0c89f6fc6ba6fbd9a353b24ffb4256a492420c5517da5a/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457", size = 550052, upload-time = "2025-10-22T22:21:34.702Z" }, - { url = "https://files.pythonhosted.org/packages/de/bc/ec2c004f6c7d6ab1e25dae875cdb1aee087c3ebed5b73712ed3000e3851a/rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e", size = 215110, upload-time = "2025-10-22T22:21:36.645Z" }, - { url = "https://files.pythonhosted.org/packages/6c/de/4ce8abf59674e17187023933547d2018363e8fc76ada4f1d4d22871ccb6e/rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8", size = 223850, upload-time = "2025-10-22T22:21:38.006Z" }, - { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" }, - { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" }, - { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" }, - { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" }, - { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" }, - { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" }, - { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" }, - { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, - { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, - { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, - { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, - { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, - { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, - { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, - { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, - { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, - { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, - { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, - { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, - { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, - { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, - { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, - { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, - { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, - { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, - { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, - { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, - { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" }, - { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" }, - { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7a/c5b2ff381b74bc742768e8d870f26babac4ef256ba160bdbf8d57af56461/rpds_py-0.29.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4ae4b88c6617e1b9e5038ab3fccd7bac0842fdda2b703117b2aa99bc85379113", size = 372385, upload-time = "2025-11-16T14:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/28/36/531f1eb4d5bed4a9c150f363a7ec4a98d2dc746151bba5473bc38ee85dec/rpds_py-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d9128ec9d8cecda6f044001fde4fb71ea7c24325336612ef8179091eb9596b9", size = 362869, upload-time = "2025-11-16T14:47:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/7e9c0493a2015d9c82807a2d5f023ea9774e27a4c15b33ef1cdb7456138d/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37812c3da8e06f2bb35b3cf10e4a7b68e776a706c13058997238762b4e07f4f", size = 391582, upload-time = "2025-11-16T14:47:39.746Z" }, + { url = "https://files.pythonhosted.org/packages/15/38/42a981c3592ef46fbd7e17adbf8730cc5ec87e6aa1770c658c44bbb52960/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66786c3fb1d8de416a7fa8e1cb1ec6ba0a745b2b0eee42f9b7daa26f1a495545", size = 405685, upload-time = "2025-11-16T14:47:41.472Z" }, + { url = "https://files.pythonhosted.org/packages/12/45/628b8c15856c3849c3f52ec6dac93c046ed5faeed4a435af03b70525fd29/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58f5c77f1af888b5fd1876c9a0d9858f6f88a39c9dd7c073a88e57e577da66d", size = 527067, upload-time = "2025-11-16T14:47:43.036Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ba/6b56d09badeabd95098016d72a437d4a0fd82d4672ce92a7607df5d70a42/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:799156ef1f3529ed82c36eb012b5d7a4cf4b6ef556dd7cc192148991d07206ae", size = 412532, upload-time = "2025-11-16T14:47:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/f1/39/2f1f3db92888314b50b8f9641f679188bd24b3665a8cb9923b7201ae8011/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453783477aa4f2d9104c4b59b08c871431647cb7af51b549bbf2d9eb9c827756", size = 392736, upload-time = "2025-11-16T14:47:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/60/43/3c3b1dcd827e50f2ae28786d846b8a351080d8a69a3b49bc10ae44cc39b1/rpds_py-0.29.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:24a7231493e3c4a4b30138b50cca089a598e52c34cf60b2f35cebf62f274fdea", size = 406300, upload-time = "2025-11-16T14:47:47.268Z" }, + { url = "https://files.pythonhosted.org/packages/da/02/bc96021b67f8525e6bcdd68935c4543ada61e1f3dcb067ed037d68b8c6d2/rpds_py-0.29.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7033c1010b1f57bb44d8067e8c25aa6fa2e944dbf46ccc8c92b25043839c3fd2", size = 423641, upload-time = "2025-11-16T14:47:48.878Z" }, + { url = "https://files.pythonhosted.org/packages/38/e9/c435ddb602ced19a80b8277a41371734f33ad3f91cc4ceb4d82596800a3c/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2", size = 574153, upload-time = "2025-11-16T14:47:50.435Z" }, + { url = "https://files.pythonhosted.org/packages/84/82/dc3c32e1f89ecba8a59600d4cd65fe0ad81b6c636ccdbf6cd177fd6a7bac/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b", size = 600304, upload-time = "2025-11-16T14:47:51.599Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/785290e0b7142470735dc1b1f68fb33aae29e5296f062c88396eedf796c8/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a", size = 562211, upload-time = "2025-11-16T14:47:53.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/58/4eeddcb0737c6875f3e30c65dc9d7e7a10dfd5779646a990fa602c6d56c5/rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef", size = 221803, upload-time = "2025-11-16T14:47:54.404Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/b35a8dbdcbeb32505500547cdafaa9f8863e85f8faac50ef34464ec5a256/rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950", size = 235530, upload-time = "2025-11-16T14:47:56.061Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344, upload-time = "2025-11-16T14:47:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041, upload-time = "2025-11-16T14:47:58.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775, upload-time = "2025-11-16T14:48:00.197Z" }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624, upload-time = "2025-11-16T14:48:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894, upload-time = "2025-11-16T14:48:03.167Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720, upload-time = "2025-11-16T14:48:04.413Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945, upload-time = "2025-11-16T14:48:06.252Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385, upload-time = "2025-11-16T14:48:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943, upload-time = "2025-11-16T14:48:10.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204, upload-time = "2025-11-16T14:48:11.499Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587, upload-time = "2025-11-16T14:48:12.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287, upload-time = "2025-11-16T14:48:14.108Z" }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394, upload-time = "2025-11-16T14:48:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713, upload-time = "2025-11-16T14:48:16.636Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157, upload-time = "2025-11-16T14:48:17.891Z" }, + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000, upload-time = "2025-11-16T14:48:19.141Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575, upload-time = "2025-11-16T14:48:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159, upload-time = "2025-11-16T14:48:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602, upload-time = "2025-11-16T14:48:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808, upload-time = "2025-11-16T14:48:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015, upload-time = "2025-11-16T14:48:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325, upload-time = "2025-11-16T14:48:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160, upload-time = "2025-11-16T14:48:29.338Z" }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309, upload-time = "2025-11-16T14:48:30.691Z" }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644, upload-time = "2025-11-16T14:48:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605, upload-time = "2025-11-16T14:48:33.252Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593, upload-time = "2025-11-16T14:48:34.643Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853, upload-time = "2025-11-16T14:48:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895, upload-time = "2025-11-16T14:48:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321, upload-time = "2025-11-16T14:48:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710, upload-time = "2025-11-16T14:48:41.063Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582, upload-time = "2025-11-16T14:48:42.423Z" }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172, upload-time = "2025-11-16T14:48:43.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586, upload-time = "2025-11-16T14:48:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339, upload-time = "2025-11-16T14:48:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201, upload-time = "2025-11-16T14:48:48.615Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095, upload-time = "2025-11-16T14:48:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077, upload-time = "2025-11-16T14:48:51.515Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548, upload-time = "2025-11-16T14:48:53.237Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661, upload-time = "2025-11-16T14:48:54.769Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937, upload-time = "2025-11-16T14:48:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496, upload-time = "2025-11-16T14:48:57.691Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126, upload-time = "2025-11-16T14:48:59.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771, upload-time = "2025-11-16T14:49:00.872Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994, upload-time = "2025-11-16T14:49:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886, upload-time = "2025-11-16T14:49:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262, upload-time = "2025-11-16T14:49:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826, upload-time = "2025-11-16T14:49:07.301Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234, upload-time = "2025-11-16T14:49:08.782Z" }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008, upload-time = "2025-11-16T14:49:10.253Z" }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569, upload-time = "2025-11-16T14:49:12.478Z" }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188, upload-time = "2025-11-16T14:49:13.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587, upload-time = "2025-11-16T14:49:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641, upload-time = "2025-11-16T14:49:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683, upload-time = "2025-11-16T14:49:18.342Z" }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730, upload-time = "2025-11-16T14:49:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361, upload-time = "2025-11-16T14:49:21.574Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227, upload-time = "2025-11-16T14:49:23.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248, upload-time = "2025-11-16T14:49:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731, upload-time = "2025-11-16T14:49:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343, upload-time = "2025-11-16T14:49:28.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406, upload-time = "2025-11-16T14:49:29.943Z" }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162, upload-time = "2025-11-16T14:49:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719, upload-time = "2025-11-16T14:49:33.804Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498, upload-time = "2025-11-16T14:49:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743, upload-time = "2025-11-16T14:49:36.704Z" }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317, upload-time = "2025-11-16T14:49:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979, upload-time = "2025-11-16T14:49:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288, upload-time = "2025-11-16T14:49:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157, upload-time = "2025-11-16T14:49:43.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741, upload-time = "2025-11-16T14:49:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508, upload-time = "2025-11-16T14:49:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125, upload-time = "2025-11-16T14:49:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992, upload-time = "2025-11-16T14:49:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425, upload-time = "2025-11-16T14:49:52.691Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282, upload-time = "2025-11-16T14:49:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968, upload-time = "2025-11-16T14:49:55.857Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714, upload-time = "2025-11-16T14:49:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136, upload-time = "2025-11-16T14:49:59.162Z" }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250, upload-time = "2025-11-16T14:50:00.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940, upload-time = "2025-11-16T14:50:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392, upload-time = "2025-11-16T14:50:03.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796, upload-time = "2025-11-16T14:50:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843, upload-time = "2025-11-16T14:50:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956, upload-time = "2025-11-16T14:50:09.029Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288, upload-time = "2025-11-16T14:50:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382, upload-time = "2025-11-16T14:50:12.827Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919, upload-time = "2025-11-16T14:50:14.734Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963, upload-time = "2025-11-16T14:50:16.205Z" }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644, upload-time = "2025-11-16T14:50:18.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847, upload-time = "2025-11-16T14:50:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281, upload-time = "2025-11-16T14:50:21.64Z" }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213, upload-time = "2025-11-16T14:50:23.219Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808, upload-time = "2025-11-16T14:50:25.262Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600, upload-time = "2025-11-16T14:50:26.956Z" }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634, upload-time = "2025-11-16T14:50:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064, upload-time = "2025-11-16T14:50:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871, upload-time = "2025-11-16T14:50:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702, upload-time = "2025-11-16T14:50:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054, upload-time = "2025-11-16T14:50:37.733Z" }, ] [[package]] @@ -5367,7 +5371,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "threadpoolctl", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, @@ -5483,7 +5487,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", ] dependencies = [ - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } wheels = [ @@ -5556,7 +5560,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, - { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } @@ -6185,28 +6189,28 @@ wheels = [ [[package]] name = "uv" -version = "0.9.9" +version = "0.9.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/4a/dd4d1a772afd0ad4167a932864e145ba3010d2a148e34171070bfcb85528/uv-0.9.9.tar.gz", hash = "sha256:dc5885fda74cec4cf8eea4115a6e0e431462c6c6bf1bd925abd72699d6b54f51", size = 3724446, upload-time = "2025-11-12T18:45:24.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/42/18d158032c594110e7dab012931a3cd6a09f4dec801854dc877a3c6e865a/uv-0.9.10.tar.gz", hash = "sha256:84128ca76a665fe0584bff6ef913741d5615a4187c1aaed81739d519e669cfbd", size = 3738676, upload-time = "2025-11-17T17:02:36.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/d2/dcf1ee2b977ebbe12c8666b56f49d1138b565fc475c0e80c00c50da6d321/uv-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:ea700f6e43389a3bd6aa90c02f3010b61ef987c3b025842281a8bd513e26cf3a", size = 20481964, upload-time = "2025-11-12T18:44:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/80/d5/d9e18da60593d8d127a435fe5451033dba2ec6d11baea06d6cbad5e2e6b0/uv-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7ea663b3e5e5b20a17efbc6c7f8db602abf72447d7cced0882a0dff71c2de1ef", size = 19589253, upload-time = "2025-11-12T18:44:26.35Z" }, - { url = "https://files.pythonhosted.org/packages/cc/47/436863f6d99cfc3e41408e1d28d07fb3d20227d5ff66f52666564a5649f5/uv-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e8303e17b7d2a2dc65ebc4cc65cc0b2be493566b4f7421279b008ecb10adfc5f", size = 18149442, upload-time = "2025-11-12T18:44:29.45Z" }, - { url = "https://files.pythonhosted.org/packages/15/04/b22cd0716369f63265c76ab254e98573cb65e2ee7908f5ffa90e1c2e18fc/uv-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:036e8d38f87ffbebcd478e6b61a2c4f8733f77fbdf34140b78e0f5ab238810cf", size = 19960485, upload-time = "2025-11-12T18:44:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cd/de0f6d6292a812159a134c7ed0b1692ad1ea7baf6de3c66e48c2500bd919/uv-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa5fb4ee5f85fe4364a2895bf849c98a4537f6a96a8da22922fb3eb149ef7aaf", size = 20085388, upload-time = "2025-11-12T18:44:36.036Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bf/86ddcc9042d003c2edba8c534787bf5b8c15da026903084faaeb6cee4a7c/uv-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bcb3e003d6b12cfb03a6223914b648de595a0b79ae2c0259411224966f3fd60", size = 20978689, upload-time = "2025-11-12T18:44:39.231Z" }, - { url = "https://files.pythonhosted.org/packages/fd/bb/d8f8ddfbc2c429a75df28b37594c9b8dfdf0f00f091175d5dabc6791ed09/uv-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f423deb65d2c3aed8f896cd012f0fdaca47aff200fe35a81d9e0dfd543104c56", size = 22602188, upload-time = "2025-11-12T18:44:42.62Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4d/bf42ae81d0ccee4d5bbc401465da1a20b82944435a36eebb953e836ea6a8/uv-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb2eca9575bb993fdd4309c75f6253772b826b5a1778b83479e32e9097a35340", size = 22187774, upload-time = "2025-11-12T18:44:46.038Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f9/e559d46b77a33c1ef5d10e5d6223ac6a60bbc681a11c9352782b3d391001/uv-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bb84164437e71a55674898a1db34a1874489f362e90f0ce1d2be3c5ef214453", size = 21309545, upload-time = "2025-11-12T18:44:49.844Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4a/d5357825bb47ff73762d247b1a553a966fef6802e3ab829fe60934cbf339/uv-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afdd00ddc25e12ed756e069090011ca55f127753e1192e51f45fa288a024f3df", size = 21287121, upload-time = "2025-11-12T18:44:53.745Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/9925ec558b9b7435d8646e74f8831aa10165e8768b6d9b0c702655b164fb/uv-0.9.9-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4f2e2a818ce64b917aada5a752a92bc5665ed90f3ac1348689c4d92abe4af3f5", size = 20085994, upload-time = "2025-11-12T18:44:57.663Z" }, - { url = "https://files.pythonhosted.org/packages/11/ec/8fe7499790805913a2a8199e814aa78c1ab63db97ac654c741a2a5d493ca/uv-0.9.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:25892e6a4e5e1b9eb3cac8571a66c2f6f7be201ce114e443ef64e007dceeb640", size = 21118665, upload-time = "2025-11-12T18:45:00.949Z" }, - { url = "https://files.pythonhosted.org/packages/dc/49/8b93a53411789a35010bfc9f359391081c7bc2861d4d5c1d8d98b3d07cbb/uv-0.9.9-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fa149da37045afde21d3167a5057ca8c5abbe65194f08ea59dfbd5f4faa25b13", size = 20064311, upload-time = "2025-11-12T18:45:04.425Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/3c15283ffec67bd8302c34eaf871e50d71fceacfffc8ee26ff02b0adea69/uv-0.9.9-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0b93153f1262873d6fc725f3a76264eb06e26a2651af17a1e797ae52e19eacb1", size = 20474039, upload-time = "2025-11-12T18:45:07.55Z" }, - { url = "https://files.pythonhosted.org/packages/08/ec/73bc3fb4613ad2b21b92a2c23d5bc6dc31c1acb1ca6a70bdc55e7c426ef6/uv-0.9.9-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1263f03571f3fda44061862c14b92c992102d03f5e1015f3886d9252f9436d60", size = 21506473, upload-time = "2025-11-12T18:45:11.183Z" }, - { url = "https://files.pythonhosted.org/packages/85/5c/5b20529430140cc39255c0884da734610ccaaf2fd15f2cfabd29f6193d01/uv-0.9.9-py3-none-win32.whl", hash = "sha256:1d25f1aca2f8a3b24f3fdf9b029a9a923c429a828be7c9eee9fa073addedbc36", size = 19272132, upload-time = "2025-11-12T18:45:14.352Z" }, - { url = "https://files.pythonhosted.org/packages/f2/38/562295348cf2eb567fd5ea44512a645ea5bec2661a7e07b7f14fda54cb07/uv-0.9.9-py3-none-win_amd64.whl", hash = "sha256:1201765ae39643ef66bc6decfc44c5f8540fcaeae8b0914553b32e670f1941da", size = 21316052, upload-time = "2025-11-12T18:45:18.897Z" }, - { url = "https://files.pythonhosted.org/packages/9d/62/47e8d16da92ffb095388e45cc3f6e6c2ba1404d80590fb9528305517c7f3/uv-0.9.9-py3-none-win_arm64.whl", hash = "sha256:2695624ee43a8932c3fb414a98e4aed3b4f60306a24acd68e2b288dd5a58c370", size = 19821476, upload-time = "2025-11-12T18:45:22.462Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d2/68f07f3b96539b986d36a6f2b03f9f8484373f7d8f05949221be1a1f9993/uv-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:a02b27e00afe0d7908fbb9ec61ad5338508e02356138a8a718af239fe773dd2e", size = 20522030, upload-time = "2025-11-17T17:01:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/763da49b0dfafb5465376e29e176f56c3f1ca79a38ef59983fe5d62dd371/uv-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c2ddc0bca2e4b7e31623a17d60ea5f8d1ee4ff3ee27ddbf884e089e57cd0c93", size = 19627118, upload-time = "2025-11-17T17:01:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e8/5b5b5a719830ee5505bddf35082f6dc41569dab8fb6761626e5034c27748/uv-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8c8969d54c9f603107445a23f36fba03a0dfa4c75b3ada2faf94ed371f26f3a4", size = 18174169, upload-time = "2025-11-17T17:01:54.034Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/4c074cab3bf483a172bec905e1696269b0222d73fd8dc890388da8b110c9/uv-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c9a326f04b670f1af2d9fa90e50af57a9a872a8dc90bb377d0c27b2030438ffc", size = 19950337, upload-time = "2025-11-17T17:01:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/45/9d/a8930ce9f5247629335de32dd97f19b955f86fb7524ea268f17181c3bccc/uv-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55ecddf19314f7da5b8dea6bcfcc86f5769dd7d22106c731e8e6cfbf3fa6b98d", size = 20123165, upload-time = "2025-11-17T17:01:58.668Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/3b136c1effa53ab84bc72f66cbc3aca0013fad6581d9c03c2989635b3be1/uv-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763cf33c7c5ab490323fab667b24f66412c3c3ca58e86b56e13fc0109d031a1b", size = 21024525, upload-time = "2025-11-17T17:02:01.022Z" }, + { url = "https://files.pythonhosted.org/packages/64/f8/7aab6deb6362431edd670c2885d802ced2cb045fd4f73df462c9c8b05f32/uv-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5e7a86eb5edbb9064b1401522eb10c5bd25ff56c04cd0ed253d0cd088a448bef", size = 22578222, upload-time = "2025-11-17T17:02:03.333Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5c/658647132d2c2396bd484b9ee42ab9a0a3f439a14b59e91c48b66b6b7403/uv-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c133e34feca0823d952a1d323d1d47f2b35e13154de5ca293d188f5b8792a62", size = 22205447, upload-time = "2025-11-17T17:02:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/80/22/b4c766d855aaefd3797c6e296af4b006d560b78cec5f3041902cd6d7abce/uv-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:92d4b58ef810eaf5edf95c607d613ff240ab903ee599af1d687f891ab64b4129", size = 21247210, upload-time = "2025-11-17T17:02:08.312Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0c/4b705b613237519e8640baaa1fe4809ddef2482ad7b82ea807c71e5ddb5d/uv-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21981bc859802c94d4b8f026b8734d39e8146baac703f1e3eab2e2d87d65ca8c", size = 21389310, upload-time = "2025-11-17T17:02:11.117Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/81df7494107d8473a754fea74e51c8727192a71c11fc4c92aa901e36a255/uv-0.9.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:110fd2062dccc4825f96cef0007f6bbb6f5da117af50415bed6f6dcd5e1e3603", size = 20062308, upload-time = "2025-11-17T17:02:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b5/0919100138d2cdb2f4f058a0e7c6fc059aad1914b1f6637c7fcfe2e670d9/uv-0.9.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b0a0c4594871de9e8b12c9e10982dc83e2414b712a34e84860fac9fbc8510c5a", size = 21182524, upload-time = "2025-11-17T17:02:16.844Z" }, + { url = "https://files.pythonhosted.org/packages/43/f5/deb8be34797f71ae72f690da26b388682cc478bda630822fff7bc90e80f3/uv-0.9.10-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:516d1c5549912696ba68376d2652661ed327199a9ec15763f7baa29bc75b35ec", size = 20084575, upload-time = "2025-11-17T17:02:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/4e51d384669103a9ba8ede1ecf1c99c932148bdb365794ac586f90e5296f/uv-0.9.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:780a7af4a7dfb0894a8d367b5c3a48f926480f465a34aa4a8633d6e4f521e517", size = 20495061, upload-time = "2025-11-17T17:02:21.978Z" }, + { url = "https://files.pythonhosted.org/packages/78/9f/c16fb7e0290744a22987ffb9700c69fe1e459e199e04f3e881ce05d032e4/uv-0.9.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:4e256d3cc542f73435b27a3d0bf2a6b0f9d2dd6dd5c8df1a8028834deb896819", size = 21601257, upload-time = "2025-11-17T17:02:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f9/ea01841fe2660add4b8fbc2b3a34e8d4f812d9bdaeeeead5470e1142b4be/uv-0.9.10-py3-none-win32.whl", hash = "sha256:dfcc3cd5765158e82a0f52066462e378887aac01b347b588907fe3290af92356", size = 19338075, upload-time = "2025-11-17T17:02:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/43/c3/72dbdf10ea0e70ca47248aaa25122df9dfa7a7e85e96ba6031cafc67cb02/uv-0.9.10-py3-none-win_amd64.whl", hash = "sha256:9ee9027eb98cf5a99d9a3d5ddab7048745e2e49d572869044f66772e17f57792", size = 21367277, upload-time = "2025-11-17T17:02:30.672Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/b38be12b524dbd28dcdc99cfd88f6f3f792823258b96efb6d691025872cb/uv-0.9.10-py3-none-win_arm64.whl", hash = "sha256:d4903e73f5e281ce5c3def60014bef93d2d1a148901653e222470ce54987f4a1", size = 19770409, upload-time = "2025-11-17T17:02:34.057Z" }, ] [[package]]